@cassiomc1/forgeloop 1.10.2 → 1.11.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 (84) hide show
  1. package/.cursor/rules/project-loop.mdc +2 -2
  2. package/.forgeloop/forgeloop.gitignore +1 -0
  3. package/.github/copilot-instructions.md +2 -2
  4. package/AGENTS.md +1 -0
  5. package/AGENT_COMPATIBILITY.md +7 -0
  6. package/CLAUDE.md +1 -0
  7. package/DOCS_INDEX.md +10 -0
  8. package/LOOP_SYSTEM_DESIGN.md +12 -0
  9. package/ORCHESTRATOR_INTEGRATION.md +9 -0
  10. package/PROTOCOL_INTEGRATION.md +17 -0
  11. package/README.md +12 -10
  12. package/THIRD_PARTY_NOTICES.md +11 -0
  13. package/THREAT_MODEL.md +21 -0
  14. package/benchmarks/repository-index/README.md +73 -0
  15. package/benchmarks/repository-index/queries.json +12 -0
  16. package/benchmarks/repository-index/run-hot-path.mjs +142 -0
  17. package/benchmarks/repository-index/run-persistent-transport.mjs +169 -0
  18. package/completions/_forgeloop +7 -1
  19. package/completions/forgeloop.bash +13 -1
  20. package/completions/forgeloop.fish +40 -1
  21. package/docs/AGENT_PROTOCOL_SUMMARY.md +8 -1
  22. package/docs/CLI_REFERENCE.md +114 -2
  23. package/docs/DOCUMENTATION_GUIDE.md +10 -5
  24. package/docs/GETTING_STARTED.md +17 -0
  25. package/docs/MCP.md +16 -1
  26. package/docs/PACKAGE_CONTENTS.md +7 -1
  27. package/docs/PERSISTENT_SEARCH_TRANSPORT.md +289 -0
  28. package/docs/RECIPES.md +30 -0
  29. package/docs/RELEASE_CHECKLIST.md +34 -0
  30. package/docs/REPOSITORY_INDEX.md +553 -0
  31. package/docs/TROUBLESHOOTING.md +147 -0
  32. package/docs/UNIVERSAL_INTEGRATION.md +30 -0
  33. package/docs/diagrams/README.md +10 -0
  34. package/package.json +11 -2
  35. package/scripts/update-tgrep-manifest.mjs +86 -0
  36. package/scripts/verify-tgrep-manifest.mjs +17 -0
  37. package/src/cli.js +35 -0
  38. package/src/commands/doctor.js +76 -1
  39. package/src/commands/index-rebuild.js +1 -0
  40. package/src/commands/index-setup.js +1 -0
  41. package/src/commands/index-start.js +1 -0
  42. package/src/commands/index-status.js +1 -0
  43. package/src/commands/index-stop.js +1 -0
  44. package/src/commands/init.js +39 -1
  45. package/src/commands/repository-index.js +111 -0
  46. package/src/commands/search.js +1 -0
  47. package/src/commands/update.js +32 -4
  48. package/src/core/cli-command-definitions.js +97 -3
  49. package/src/core/command-executors.js +35 -2
  50. package/src/core/command-input.js +23 -0
  51. package/src/core/error-codes.js +195 -0
  52. package/src/core/filesystem.js +10 -1
  53. package/src/core/integration-invocation-policy.js +27 -0
  54. package/src/core/integration-resources.js +16 -1
  55. package/src/core/protocol-info.js +23 -0
  56. package/src/integration.d.ts +90 -0
  57. package/src/integration.js +21 -0
  58. package/src/persistent-transport/client.js +293 -0
  59. package/src/persistent-transport/constants.js +24 -0
  60. package/src/persistent-transport/errors.js +38 -0
  61. package/src/persistent-transport/framing.js +61 -0
  62. package/src/persistent-transport/lifecycle.js +116 -0
  63. package/src/persistent-transport/ownership.js +184 -0
  64. package/src/persistent-transport/paths.js +31 -0
  65. package/src/persistent-transport/protocol.js +95 -0
  66. package/src/persistent-transport/server.js +256 -0
  67. package/src/persistent-transport/state.js +49 -0
  68. package/src/repository-index/args.js +59 -0
  69. package/src/repository-index/binary-manager.js +413 -0
  70. package/src/repository-index/constants.js +45 -0
  71. package/src/repository-index/errors.js +38 -0
  72. package/src/repository-index/lifecycle.js +17 -0
  73. package/src/repository-index/lock.js +113 -0
  74. package/src/repository-index/manifest.js +132 -0
  75. package/src/repository-index/metrics.js +30 -0
  76. package/src/repository-index/normalize-json.js +187 -0
  77. package/src/repository-index/paths.js +39 -0
  78. package/src/repository-index/platform.js +20 -0
  79. package/src/repository-index/process.js +140 -0
  80. package/src/repository-index/readiness.js +62 -0
  81. package/src/repository-index/search.js +262 -0
  82. package/src/repository-index/server.js +432 -0
  83. package/src/repository-index/status.js +397 -0
  84. package/src/repository-index/tgrep-manifest.json +38 -0
@@ -0,0 +1,61 @@
1
+ import { PERSISTENT_TRANSPORT_DEFAULTS } from "./constants.js";
2
+ import { PERSISTENT_TRANSPORT_ERROR_CODES, persistentTransportError } from "./errors.js";
3
+
4
+ export function encodeFrame(value, { maxFrameBytes = PERSISTENT_TRANSPORT_DEFAULTS.maxResponseFrameBytes } = {}) {
5
+ let payload;
6
+ try {
7
+ payload = Buffer.from(JSON.stringify(value), "utf8");
8
+ } catch (error) {
9
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.FRAME_INVALID, `Unable to encode transport frame: ${error.message}`);
10
+ }
11
+ if (payload.length > maxFrameBytes) {
12
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.FRAME_TOO_LARGE, `Transport frame exceeds ${maxFrameBytes} bytes`);
13
+ }
14
+ const frame = Buffer.allocUnsafe(4 + payload.length);
15
+ frame.writeUInt32BE(payload.length, 0);
16
+ payload.copy(frame, 4);
17
+ return frame;
18
+ }
19
+
20
+ export class FrameDecoder {
21
+ #buffer = Buffer.alloc(0);
22
+ #maxFrameBytes;
23
+
24
+ constructor({ maxFrameBytes = PERSISTENT_TRANSPORT_DEFAULTS.maxRequestFrameBytes } = {}) {
25
+ this.#maxFrameBytes = maxFrameBytes;
26
+ }
27
+
28
+ push(chunk) {
29
+ if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
30
+ this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
31
+ const frames = [];
32
+ while (this.#buffer.length >= 4) {
33
+ const length = this.#buffer.readUInt32BE(0);
34
+ if (length > this.#maxFrameBytes) {
35
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.FRAME_TOO_LARGE, `Transport frame exceeds ${this.#maxFrameBytes} bytes`, { length });
36
+ }
37
+ if (this.#buffer.length < 4 + length) break;
38
+ frames.push(this.#buffer.subarray(4, 4 + length));
39
+ this.#buffer = this.#buffer.subarray(4 + length);
40
+ }
41
+ return frames;
42
+ }
43
+
44
+ end() {
45
+ if (this.#buffer.length !== 0) {
46
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.FRAME_INVALID, "Transport stream ended with a truncated frame");
47
+ }
48
+ }
49
+
50
+ get bufferedBytes() {
51
+ return this.#buffer.length;
52
+ }
53
+ }
54
+
55
+ export function parseFrame(frame) {
56
+ try {
57
+ return JSON.parse(frame.toString("utf8"));
58
+ } catch (error) {
59
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.FRAME_INVALID, `Transport frame is not valid JSON: ${error.message}`);
60
+ }
61
+ }
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile, unlink } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { acquireRepositoryIndexLock } from "../repository-index/lock.js";
7
+ import { processIsAlive, processCommandLine } from "../repository-index/status.js";
8
+ import { getPackageRoot } from "../core/templates.js";
9
+ import { getPersistentTransportPaths } from "./paths.js";
10
+ import { PERSISTENT_TRANSPORT_DEFAULTS, PERSISTENT_TRANSPORT_PROTOCOL_VERSION } from "./constants.js";
11
+ import { PERSISTENT_TRANSPORT_ERROR_CODES, persistentTransportError } from "./errors.js";
12
+ import { readPersistentTransportState, removePersistentTransportState } from "./state.js";
13
+ import { inspectPersistentTransportOwnership, terminateOwnedPersistentTransport } from "./ownership.js";
14
+
15
+ async function readPackageVersion() {
16
+ const packageJson = await readFile(path.join(getPackageRoot(), "package.json"), "utf8");
17
+ return JSON.parse(packageJson).version ?? null;
18
+ }
19
+
20
+ async function removeEndpoint(endpoint, platform = process.platform) {
21
+ if (platform === "win32" || typeof endpoint !== "string") return;
22
+ try {
23
+ await unlink(endpoint);
24
+ } catch (error) {
25
+ if (error.code !== "ENOENT") throw error;
26
+ }
27
+ }
28
+
29
+ export async function ensurePersistentTransportDirectory({ homeDirectory = os.homedir() } = {}) {
30
+ const paths = getPersistentTransportPaths({ homeDirectory });
31
+ await mkdir(paths.root, { recursive: true, mode: 0o700 });
32
+ return paths;
33
+ }
34
+
35
+ export async function inspectPersistentTransport({ homeDirectory = os.homedir(), processApi = process, processInspector = {} } = {}) {
36
+ const paths = getPersistentTransportPaths({ homeDirectory });
37
+ const { state, invalid } = await readPersistentTransportState(paths.statePath);
38
+ if (invalid) return { status: "STALE", running: false, owned: false, paths, state: null, reason: "STATE_INVALID" };
39
+ if (!state) return { status: "NOT_RUNNING", running: false, owned: false, paths, state: null };
40
+ const ownership = await inspectPersistentTransportOwnership(state, { processApi, processInspector, expectedEndpoint: paths.endpoint });
41
+ if (!ownership.running && ownership.reason === "PROCESS_EXITED") return { status: "STALE", running: false, owned: true, paths, state, reason: ownership.reason };
42
+ if (!ownership.owned) return { status: "OWNERSHIP_UNVERIFIED", running: false, owned: false, paths, state, reason: ownership.reason };
43
+ const packageVersion = await readPackageVersion();
44
+ if (state.protocolVersion !== PERSISTENT_TRANSPORT_PROTOCOL_VERSION || (packageVersion && state.forgeLoopVersion !== packageVersion)) {
45
+ return { status: "INCOMPATIBLE", running: true, owned: true, paths, state, reason: "VERSION_MISMATCH", packageVersion, ownershipMode: ownership.ownershipMode };
46
+ }
47
+ return { status: "READY", running: true, owned: true, paths, state, packageVersion, ownershipMode: ownership.ownershipMode };
48
+ }
49
+
50
+ export async function getPersistentTransportStatus(options = {}) {
51
+ const inspection = await inspectPersistentTransport(options);
52
+ return {
53
+ schemaVersion: 1,
54
+ status: inspection.status,
55
+ running: inspection.running,
56
+ owned: inspection.owned,
57
+ protocolVersion: inspection.state?.protocolVersion ?? null,
58
+ forgeLoopVersion: inspection.state?.forgeLoopVersion ?? null,
59
+ };
60
+ }
61
+
62
+ export async function cleanPersistentTransportState(inspection, { processApi = process } = {}) {
63
+ if (!inspection?.state || inspection.owned !== true) return;
64
+ if (inspection.owned && inspection.running) {
65
+ await terminateOwnedPersistentTransport(inspection.state, { processApi, expectedEndpoint: inspection.paths.endpoint });
66
+ const deadline = Date.now() + 1_000;
67
+ while (Date.now() < deadline && processIsAlive(inspection.state.pid, processApi)) {
68
+ await new Promise((resolve) => setTimeout(resolve, 25));
69
+ }
70
+ }
71
+ await removePersistentTransportState(inspection.paths.statePath, inspection.state.nonce);
72
+ await removeEndpoint(inspection.paths.endpoint);
73
+ }
74
+
75
+ export async function startPersistentSearchHost({ homeDirectory = os.homedir(), idleTimeoutMs = PERSISTENT_TRANSPORT_DEFAULTS.idleTimeoutMs, env = {}, spawnImpl = spawn } = {}) {
76
+ const paths = await ensurePersistentTransportDirectory({ homeDirectory });
77
+ const entrypoint = path.join(getPackageRoot(), "src", "persistent-transport", "server.js");
78
+ const args = [entrypoint, "--persistent-transport-server", `--scope-id=${paths.scopeId}`, `--home-directory=${homeDirectory}`, `--idle-timeout-ms=${String(idleTimeoutMs)}`];
79
+ let child;
80
+ try {
81
+ child = spawnImpl(process.execPath, args, {
82
+ cwd: getPackageRoot(),
83
+ env: {
84
+ ...process.env,
85
+ ...env,
86
+ HOME: homeDirectory,
87
+ USERPROFILE: homeDirectory,
88
+ FORGELOOP_PERSISTENT_TRANSPORT_HOME: homeDirectory,
89
+ FORGELOOP_PERSISTENT_TRANSPORT_SCOPE: paths.scopeId,
90
+ },
91
+ detached: true,
92
+ stdio: "ignore",
93
+ shell: false,
94
+ windowsHide: true,
95
+ });
96
+ } catch (cause) {
97
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.START_FAILED, `Unable to start persistent search host: ${cause.message}`, { cause });
98
+ }
99
+ child.unref?.();
100
+ return { child, paths, entrypoint };
101
+ }
102
+
103
+ export async function acquirePersistentTransportStartupLock({ homeDirectory = os.homedir(), tryOnly = false } = {}) {
104
+ const paths = await ensurePersistentTransportDirectory({ homeDirectory });
105
+ return acquireRepositoryIndexLock(paths.lockPath, "persistent-search-host-startup", { timeoutMs: PERSISTENT_TRANSPORT_DEFAULTS.startupTimeoutMs, tryOnly });
106
+ }
107
+
108
+ export async function removeDeadPersistentTransport({ homeDirectory = os.homedir() } = {}) {
109
+ const inspection = await inspectPersistentTransport({ homeDirectory });
110
+ if (inspection.status !== "STALE") return inspection;
111
+ await removePersistentTransportState(inspection.paths.statePath, inspection.state?.nonce ?? null);
112
+ await removeEndpoint(inspection.paths.endpoint);
113
+ return { ...inspection, removed: true };
114
+ }
115
+
116
+ export { processIsAlive, processCommandLine };
@@ -0,0 +1,184 @@
1
+ import net from "node:net";
2
+
3
+ import { processCommandLine, processIsAlive } from "../repository-index/status.js";
4
+ import { PERSISTENT_TRANSPORT_DEFAULTS } from "./constants.js";
5
+ import { PERSISTENT_TRANSPORT_ERROR_CODES, persistentTransportError } from "./errors.js";
6
+ import { encodeFrame, FrameDecoder, parseFrame } from "./framing.js";
7
+ import { createRequest, validateResponse } from "./protocol.js";
8
+
9
+ const ENDPOINT_PROBE_TIMEOUT_MS = 1_000;
10
+
11
+ function comparable(value) {
12
+ if (process.platform !== "win32") return value;
13
+ return value.toLowerCase().replaceAll("\\", "/");
14
+ }
15
+
16
+ function endpointRequest(state, method = null, params = {}, timeoutMs = ENDPOINT_PROBE_TIMEOUT_MS) {
17
+ return new Promise((resolve, reject) => {
18
+ if (typeof state.endpoint !== "string" || state.endpoint.length === 0) {
19
+ reject(persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.OWNERSHIP_UNVERIFIED, "Persistent search host endpoint is invalid"));
20
+ return;
21
+ }
22
+ const socket = net.createConnection(state.endpoint);
23
+ const decoder = new FrameDecoder({ maxFrameBytes: PERSISTENT_TRANSPORT_DEFAULTS.maxResponseFrameBytes });
24
+ const handshakeRequest = createRequest("handshake", { scopeId: state.scopeId });
25
+ let activeRequest = handshakeRequest;
26
+ let handshakeResult = null;
27
+ let settled = false;
28
+ const timer = setTimeout(() => finish(reject, persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.TIMEOUT, "Timed out verifying the persistent search host endpoint")), timeoutMs);
29
+ const finish = (fn, value) => {
30
+ if (settled) return;
31
+ settled = true;
32
+ clearTimeout(timer);
33
+ socket.off("data", onData);
34
+ socket.off("error", onError);
35
+ socket.off("end", onEnd);
36
+ socket.destroy();
37
+ fn(value);
38
+ };
39
+ const send = (request) => {
40
+ activeRequest = request;
41
+ try {
42
+ socket.write(encodeFrame(request, { maxFrameBytes: PERSISTENT_TRANSPORT_DEFAULTS.maxRequestFrameBytes }));
43
+ } catch (error) {
44
+ finish(reject, error);
45
+ }
46
+ };
47
+ const onData = (chunk) => {
48
+ try {
49
+ for (const frame of decoder.push(chunk)) {
50
+ const response = validateResponse(parseFrame(frame), activeRequest.id);
51
+ if (!response.ok) {
52
+ finish(reject, persistentTransportError(response.error.code, response.error.message));
53
+ return;
54
+ }
55
+ if (activeRequest === handshakeRequest) {
56
+ handshakeResult = response.result;
57
+ if (!handshakeResult || handshakeResult.pid !== state.pid
58
+ || handshakeResult.nonce !== state.nonce
59
+ || handshakeResult.scopeId !== state.scopeId
60
+ || handshakeResult.protocolVersion !== state.protocolVersion
61
+ || handshakeResult.forgeLoopVersion !== state.forgeLoopVersion) {
62
+ finish(reject, persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.OWNERSHIP_UNVERIFIED, "Persistent search host endpoint identity does not match its state"));
63
+ return;
64
+ }
65
+ if (method === null) {
66
+ finish(resolve, response);
67
+ return;
68
+ }
69
+ send(createRequest(method, params));
70
+ continue;
71
+ }
72
+ finish(resolve, response);
73
+ return;
74
+ }
75
+ } catch (error) {
76
+ finish(reject, error);
77
+ }
78
+ };
79
+ const onError = (cause) => finish(reject, persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.UNAVAILABLE, `Persistent search host endpoint failed: ${cause.code ?? cause.message}`));
80
+ const onEnd = () => finish(reject, persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.UNAVAILABLE, "Persistent search host endpoint closed the connection"));
81
+ socket.on("data", onData);
82
+ socket.once("error", onError);
83
+ socket.once("end", onEnd);
84
+ socket.once("connect", () => {
85
+ socket.setNoDelay?.(true);
86
+ send(handshakeRequest);
87
+ });
88
+ });
89
+ }
90
+
91
+ async function endpointMatchesState(state, expectedEndpoint) {
92
+ if (typeof expectedEndpoint === "string" && comparable(state.endpoint) !== comparable(expectedEndpoint)) return false;
93
+ try {
94
+ await endpointRequest(state);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function validPersistentTransportState(state) {
102
+ if (!state || state.schemaVersion !== 1) return false;
103
+ if (!Number.isInteger(state.pid) || state.pid <= 0) return false;
104
+ if (typeof state.nonce !== "string" || typeof state.scopeId !== "string") return false;
105
+ if (typeof state.entrypoint !== "string") return false;
106
+ return typeof state.endpoint === "string" && state.endpoint.length > 0;
107
+ }
108
+
109
+ function ownershipResult(state, owned, ownershipMode = null) {
110
+ return {
111
+ owned,
112
+ running: owned,
113
+ pid: state.pid,
114
+ ownershipMode,
115
+ reason: owned ? null : "PROCESS_IDENTITY_UNVERIFIED",
116
+ commandLine: null,
117
+ };
118
+ }
119
+
120
+ async function inspectEndpointIdentity(state, expectedEndpoint) {
121
+ const owned = await endpointMatchesState(state, expectedEndpoint);
122
+ return ownershipResult(state, owned, owned ? "ENDPOINT_HANDSHAKE" : null);
123
+ }
124
+
125
+ function inspectCommandLineIdentity(state, commandLine) {
126
+ const command = comparable(commandLine);
127
+ const entrypoint = comparable(state.entrypoint);
128
+ const scopeMarker = comparable(state.scopeId);
129
+ const owned = command.includes(entrypoint) && command.includes("--persistent-transport-server") && command.includes(scopeMarker);
130
+ return ownershipResult(state, owned, owned ? "PROCESS_COMMAND_LINE" : null);
131
+ }
132
+
133
+ export async function inspectPersistentTransportOwnership(state, {
134
+ processApi = process,
135
+ processInspector = {},
136
+ expectedEndpoint = null,
137
+ } = {}) {
138
+ if (!validPersistentTransportState(state)) {
139
+ return { owned: false, running: false, reason: "STATE_INVALID" };
140
+ }
141
+ if (typeof expectedEndpoint === "string" && comparable(state.endpoint) !== comparable(expectedEndpoint)) {
142
+ return { owned: false, running: false, reason: "STATE_ENDPOINT_MISMATCH" };
143
+ }
144
+ const alive = (processInspector.isAlive ?? ((pid) => processIsAlive(pid, processApi)))(state.pid);
145
+ if (!alive) return { owned: true, running: false, reason: "PROCESS_EXITED", pid: state.pid };
146
+ if (process.platform === "win32" && processInspector.commandLine === undefined) {
147
+ return inspectEndpointIdentity(state, expectedEndpoint);
148
+ }
149
+ const commandLine = processInspector.commandLine === undefined
150
+ ? await processCommandLine(state.pid, { platform: process.platform })
151
+ : await processInspector.commandLine(state.pid);
152
+ if (typeof commandLine !== "string" || commandLine.length === 0) {
153
+ return inspectEndpointIdentity(state, expectedEndpoint);
154
+ }
155
+ return inspectCommandLineIdentity(state, commandLine);
156
+ }
157
+
158
+ export async function requireOwnedPersistentTransport(state, options = {}) {
159
+ const inspection = await inspectPersistentTransportOwnership(state, options);
160
+ if (!inspection.owned) {
161
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.OWNERSHIP_UNVERIFIED, "Persistent search host ownership could not be verified");
162
+ }
163
+ return inspection;
164
+ }
165
+
166
+ export async function terminateOwnedPersistentTransport(state, { signal = "SIGTERM", processApi = process, ...options } = {}) {
167
+ const inspection = await requireOwnedPersistentTransport(state, { processApi, ...options });
168
+ if (!inspection.running) return { ...inspection, terminated: false };
169
+ if (inspection.ownershipMode === "ENDPOINT_HANDSHAKE") {
170
+ try {
171
+ const response = await endpointRequest(state, "transport.shutdown", { nonce: state.nonce });
172
+ if (!response.result || response.result.status !== "SHUTTING_DOWN") throw new Error("Persistent search host did not accept shutdown");
173
+ } catch (cause) {
174
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.OWNERSHIP_UNVERIFIED, "Owned persistent search host could not be shut down through its verified endpoint", { cause });
175
+ }
176
+ return { ...inspection, terminated: true, termination: "ENDPOINT_SHUTDOWN" };
177
+ }
178
+ try {
179
+ processApi.kill(state.pid, signal);
180
+ } catch (error) {
181
+ if (error.code !== "ESRCH") throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.OWNERSHIP_UNVERIFIED, "Owned persistent search host could not be terminated", { cause: error });
182
+ }
183
+ return { ...inspection, terminated: true };
184
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { getPersistentTransportRoot } from "./constants.js";
6
+
7
+ function scopeHash(homeDirectory) {
8
+ return createHash("sha256").update(String(homeDirectory)).digest("hex").slice(0, 20);
9
+ }
10
+
11
+ export function getPersistentTransportScopeId({ homeDirectory = os.homedir() } = {}) {
12
+ return `user-${scopeHash(homeDirectory)}`;
13
+ }
14
+
15
+ export function getPersistentTransportPaths({ homeDirectory = os.homedir(), platform = process.platform } = {}) {
16
+ const root = getPersistentTransportRoot({ homeDirectory });
17
+ const scopeId = getPersistentTransportScopeId({ homeDirectory });
18
+ return {
19
+ root,
20
+ scopeId,
21
+ statePath: path.join(root, "state.json"),
22
+ lockPath: path.join(root, "startup.lock"),
23
+ endpoint: platform === "win32"
24
+ ? `\\\\.\\pipe\\forgeloop-persistent-search-${scopeHash(homeDirectory)}`
25
+ // macOS limits Unix-domain socket paths to a small fixed length. Keep
26
+ // the user-scoped endpoint in the local temporary namespace while the
27
+ // authoritative ownership state remains under the user's ForgeLoop
28
+ // directory.
29
+ : path.join(os.tmpdir(), `forgeloop-persistent-search-${scopeHash(homeDirectory)}.sock`),
30
+ };
31
+ }
@@ -0,0 +1,95 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ import { PERSISTENT_TRANSPORT_METHODS, PERSISTENT_TRANSPORT_PROTOCOL_VERSION } from "./constants.js";
5
+ import { PERSISTENT_TRANSPORT_ERROR_CODES, persistentTransportError } from "./errors.js";
6
+
7
+ const MAX_REQUEST_ID_CHARS = 128;
8
+ export const PERSISTENT_SEARCH_QUERY_KEYS = Object.freeze([
9
+ "pattern", "globs", "types", "context", "beforeContext", "afterContext", "maxCount",
10
+ "filesWithMatches", "stats", "fixedStrings", "ignoreCase", "smartCase", "wordRegexp",
11
+ ]);
12
+
13
+ function invalid(message, details = {}) {
14
+ return persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_REQUEST, message, details);
15
+ }
16
+
17
+ export function validateRequest(value) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid("Transport request must be an object");
19
+ if (value.protocolVersion !== PERSISTENT_TRANSPORT_PROTOCOL_VERSION) {
20
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.PROTOCOL_MISMATCH, `Unsupported transport protocol version: ${value.protocolVersion ?? "missing"}`, { expectedVersion: PERSISTENT_TRANSPORT_PROTOCOL_VERSION, actualVersion: value.protocolVersion });
21
+ }
22
+ if (typeof value.id !== "string" || value.id.length === 0 || value.id.length > MAX_REQUEST_ID_CHARS) throw invalid("Transport request id is invalid");
23
+ if (typeof value.method !== "string" || !PERSISTENT_TRANSPORT_METHODS.includes(value.method)) throw invalid(`Unsupported transport method: ${value.method ?? "missing"}`);
24
+ if (value.params !== undefined && (!value.params || typeof value.params !== "object" || Array.isArray(value.params))) throw invalid("Transport request params must be an object");
25
+ return {
26
+ protocolVersion: value.protocolVersion,
27
+ id: value.id,
28
+ method: value.method,
29
+ params: value.params ?? {},
30
+ };
31
+ }
32
+
33
+ export function createRequest(method, params = {}, id = randomUUID()) {
34
+ return { protocolVersion: PERSISTENT_TRANSPORT_PROTOCOL_VERSION, id, method, params };
35
+ }
36
+
37
+ export function createSuccessResponse(id, result) {
38
+ return { protocolVersion: PERSISTENT_TRANSPORT_PROTOCOL_VERSION, id, ok: true, result };
39
+ }
40
+
41
+ export function createErrorResponse(id, error) {
42
+ return {
43
+ protocolVersion: PERSISTENT_TRANSPORT_PROTOCOL_VERSION,
44
+ id: id ?? null,
45
+ ok: false,
46
+ error: {
47
+ code: error?.code ?? PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_RESPONSE,
48
+ message: String(error?.message ?? "Transport request failed").slice(0, 4_000),
49
+ ...(error?.expectedVersion !== undefined ? { expectedVersion: error.expectedVersion } : {}),
50
+ ...(error?.actualVersion !== undefined ? { actualVersion: error.actualVersion } : {}),
51
+ },
52
+ };
53
+ }
54
+
55
+ export function validateResponse(value, expectedId) {
56
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
57
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_RESPONSE, "Transport response must be an object");
58
+ }
59
+ if (value.protocolVersion !== PERSISTENT_TRANSPORT_PROTOCOL_VERSION) {
60
+ throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.PROTOCOL_MISMATCH, "Transport response protocol version is unsupported", { expectedVersion: PERSISTENT_TRANSPORT_PROTOCOL_VERSION, actualVersion: value.protocolVersion });
61
+ }
62
+ if (value.id !== expectedId) throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_RESPONSE, "Transport response id does not match the request");
63
+ if (typeof value.ok !== "boolean") throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_RESPONSE, "Transport response ok flag is invalid");
64
+ if (!value.ok && (!value.error || typeof value.error !== "object" || typeof value.error.code !== "string")) throw persistentTransportError(PERSISTENT_TRANSPORT_ERROR_CODES.INVALID_RESPONSE, "Transport error response is malformed");
65
+ return value;
66
+ }
67
+
68
+ export function assertSearchParams(params) {
69
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw invalid("repository.search params must be an object");
70
+ if (typeof params.repository !== "string" || !path.isAbsolute(params.repository) || params.repository.length > 4_096) throw invalid("repository.search repository identity is invalid");
71
+ if (!params.query || typeof params.query !== "object" || Array.isArray(params.query)) throw invalid("repository.search query must be an object");
72
+ const unknownKeys = Object.keys(params.query).filter((key) => !PERSISTENT_SEARCH_QUERY_KEYS.includes(key));
73
+ if (unknownKeys.length > 0) throw invalid(`repository.search query contains unsupported fields: ${unknownKeys.join(", ")}`);
74
+ if (typeof params.query.pattern !== "string" || params.query.pattern.length === 0 || params.query.pattern.length > 4_096) throw invalid("repository.search query pattern is invalid");
75
+ return params;
76
+ }
77
+
78
+ export function projectSearchQuery(query) {
79
+ const projected = {
80
+ pattern: query.pattern,
81
+ globs: query.globs,
82
+ types: query.types,
83
+ context: query.context,
84
+ beforeContext: query.beforeContext,
85
+ afterContext: query.afterContext,
86
+ maxCount: query.maxCount,
87
+ filesWithMatches: query.filesWithMatches,
88
+ stats: query.stats,
89
+ fixedStrings: query.fixedStrings,
90
+ ignoreCase: query.ignoreCase,
91
+ smartCase: query.smartCase,
92
+ wordRegexp: query.wordRegexp,
93
+ };
94
+ return Object.fromEntries(Object.entries(projected).filter(([, value]) => value !== undefined));
95
+ }