@rk0429/agentic-relay 22.0.0 → 22.2.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 (63) hide show
  1. package/dist/application/chat/chat-daemon-service.d.ts +10 -3
  2. package/dist/application/chat/chat-daemon-service.js +7 -31
  3. package/dist/application/chat/chat-daemon-service.js.map +1 -1
  4. package/dist/application/chat/chat-inbound-handler.js +21 -3
  5. package/dist/application/chat/chat-inbound-handler.js.map +1 -1
  6. package/dist/application/chat/chat-outbound-handler.d.ts +2 -1
  7. package/dist/application/chat/chat-outbound-handler.js +22 -5
  8. package/dist/application/chat/chat-outbound-handler.js.map +1 -1
  9. package/dist/application/chat/chat-question-service.d.ts +1 -1
  10. package/dist/application/chat/chat-question-service.js +1 -1
  11. package/dist/application/chat/chat-question-service.js.map +1 -1
  12. package/dist/application/chat/chat-setup-service.d.ts +6 -2
  13. package/dist/application/chat/chat-setup-service.js +55 -18
  14. package/dist/application/chat/chat-setup-service.js.map +1 -1
  15. package/dist/application/housekeeping.js +6 -2
  16. package/dist/application/housekeeping.js.map +1 -1
  17. package/dist/bin/relay.js +31 -4
  18. package/dist/bin/relay.js.map +1 -1
  19. package/dist/core/types.d.ts +1 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/domain/chat/message-cleaner.d.ts +1 -1
  23. package/dist/domain/chat/message-cleaner.js +10 -1
  24. package/dist/domain/chat/message-cleaner.js.map +1 -1
  25. package/dist/domain/chat/message-router.js +9 -5
  26. package/dist/domain/chat/message-router.js.map +1 -1
  27. package/dist/domain/chat/platform-connection.d.ts +1 -1
  28. package/dist/domain/chat/platform-connection.js +1 -1
  29. package/dist/domain/chat/platform-connection.js.map +1 -1
  30. package/dist/{core/chat-types.js → domain/chat/platform-types.js} +1 -1
  31. package/dist/domain/chat/platform-types.js.map +1 -0
  32. package/dist/domain/chat/ports.d.ts +1 -1
  33. package/dist/domain/chat/session-bridge.d.ts +1 -1
  34. package/dist/domain/chat/session-bridge.js +1 -1
  35. package/dist/domain/chat/session-bridge.js.map +1 -1
  36. package/dist/domain/chat/types.d.ts +3 -1
  37. package/dist/infrastructure/chat/adapters/discord-adapter.d.ts +1 -1
  38. package/dist/infrastructure/chat/adapters/discord-adapter.js +1 -1
  39. package/dist/infrastructure/chat/adapters/discord-adapter.js.map +1 -1
  40. package/dist/infrastructure/chat/adapters/slack-adapter.d.ts +1 -1
  41. package/dist/infrastructure/chat/adapters/slack-adapter.js +1 -1
  42. package/dist/infrastructure/chat/adapters/slack-adapter.js.map +1 -1
  43. package/dist/infrastructure/chat/chat-ipc-client.d.ts +1 -1
  44. package/dist/infrastructure/chat/chat-ipc-server.js +1 -1
  45. package/dist/infrastructure/chat/chat-ipc-server.js.map +1 -1
  46. package/dist/infrastructure/chat/session-bridge-repository.js +1 -1
  47. package/dist/infrastructure/chat/session-bridge-repository.js.map +1 -1
  48. package/dist/interfaces/cli/chat-cli.js +3 -2
  49. package/dist/interfaces/cli/chat-cli.js.map +1 -1
  50. package/dist/interfaces/cli/relay-cli-args.d.ts +1 -0
  51. package/dist/interfaces/cli/relay-cli-args.js +7 -1
  52. package/dist/interfaces/cli/relay-cli-args.js.map +1 -1
  53. package/dist/interfaces/mcp/chat-tools.d.ts +2 -1
  54. package/dist/interfaces/mcp/chat-tools.js +5 -3
  55. package/dist/interfaces/mcp/chat-tools.js.map +1 -1
  56. package/dist/interfaces/mcp/relay-mcp-server.js +1 -1
  57. package/package.json +3 -1
  58. package/scripts/check-chat-boundary.mjs +220 -0
  59. package/scripts/check-chat-boundary.test.ts +139 -0
  60. package/scripts/check-circular-deps.mjs +199 -0
  61. package/scripts/check-circular-deps.test.ts +62 -0
  62. package/dist/core/chat-types.js.map +0 -1
  63. /package/dist/{core/chat-types.d.ts → domain/chat/platform-types.d.ts} +0 -0
@@ -0,0 +1,220 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { builtinModules } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
9
+ const TARGET_DIRECTORY_SEGMENTS = [
10
+ ["src", "domain", "chat"],
11
+ ["src", "application", "chat"],
12
+ ];
13
+ const STATIC_IMPORT_RE =
14
+ /^[\t ]*import(?:\s+type)?(?:[\s\S]*?\sfrom\s+|\s+)["']([^"'\n]+)["'];?/gm;
15
+ const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
16
+
17
+ export const CHAT_BOUNDARY_ALLOWLIST = [];
18
+
19
+ const builtinModuleNames = new Set(
20
+ builtinModules.flatMap((name) =>
21
+ name.startsWith("node:") ? [name, name.slice(5)] : [name, `node:${name}`],
22
+ ),
23
+ );
24
+
25
+ export async function checkChatBoundary(options = {}) {
26
+ const projectRoot = options.projectRoot ?? PROJECT_ROOT;
27
+ const targetFiles = await collectTargetFiles(projectRoot);
28
+ const violations = [];
29
+ const matchedAllowlist = new Set();
30
+
31
+ for (const filePath of targetFiles) {
32
+ const source = await readFile(filePath, "utf8");
33
+ const sourceRelativePath = toPosixPath(path.relative(projectRoot, filePath));
34
+
35
+ for (const entry of findImports(source)) {
36
+ const allowlistKey = `${sourceRelativePath}::${entry.specifier}`;
37
+ if (
38
+ CHAT_BOUNDARY_ALLOWLIST.some(
39
+ (item) =>
40
+ item.importer === sourceRelativePath && item.specifier === entry.specifier,
41
+ )
42
+ ) {
43
+ matchedAllowlist.add(allowlistKey);
44
+ continue;
45
+ }
46
+
47
+ if (isAllowedImport(filePath, entry.specifier, projectRoot)) {
48
+ continue;
49
+ }
50
+
51
+ violations.push({
52
+ file: sourceRelativePath,
53
+ line: entry.line,
54
+ specifier: entry.specifier,
55
+ });
56
+ }
57
+ }
58
+
59
+ const warnings = CHAT_BOUNDARY_ALLOWLIST.filter(
60
+ (entry) => !matchedAllowlist.has(`${entry.importer}::${entry.specifier}`),
61
+ ).map((entry) => ({
62
+ importer: entry.importer,
63
+ specifier: entry.specifier,
64
+ reason: entry.reason,
65
+ }));
66
+
67
+ return { violations, warnings };
68
+ }
69
+
70
+ async function collectTargetFiles(projectRoot) {
71
+ const files = [];
72
+ for (const directory of TARGET_DIRECTORY_SEGMENTS.map((segments) =>
73
+ path.join(projectRoot, ...segments),
74
+ )) {
75
+ files.push(...(await walk(directory)));
76
+ }
77
+ return files.filter((filePath) => filePath.endsWith(".ts") && !filePath.endsWith(".test.ts"));
78
+ }
79
+
80
+ async function walk(directory) {
81
+ const entries = await readdir(directory, { withFileTypes: true });
82
+ const files = [];
83
+
84
+ for (const entry of entries) {
85
+ const fullPath = path.join(directory, entry.name);
86
+ if (entry.isDirectory()) {
87
+ files.push(...(await walk(fullPath)));
88
+ continue;
89
+ }
90
+ if (entry.isFile()) {
91
+ files.push(fullPath);
92
+ }
93
+ }
94
+
95
+ return files;
96
+ }
97
+
98
+ function findImports(source) {
99
+ return [STATIC_IMPORT_RE, DYNAMIC_IMPORT_RE]
100
+ .flatMap((pattern) =>
101
+ [...source.matchAll(pattern)].map((match) => ({
102
+ specifier: match[1],
103
+ line: lineNumberAt(source, match.index ?? 0),
104
+ index: match.index ?? 0,
105
+ })),
106
+ )
107
+ .sort((left, right) => left.index - right.index)
108
+ .map(({ specifier, line }) => ({ specifier, line }));
109
+ }
110
+
111
+ function lineNumberAt(source, index) {
112
+ return source.slice(0, index).split("\n").length;
113
+ }
114
+
115
+ function isAllowedImport(sourceFilePath, specifier, projectRoot) {
116
+ if (builtinModuleNames.has(specifier)) {
117
+ return true;
118
+ }
119
+
120
+ if (!specifier.startsWith(".")) {
121
+ return true;
122
+ }
123
+
124
+ const resolvedTargetPath = resolveImport(sourceFilePath, specifier);
125
+ if (!resolvedTargetPath) {
126
+ return false;
127
+ }
128
+
129
+ const sourceRelativePath = toPosixPath(path.relative(projectRoot, sourceFilePath));
130
+ const targetRelativePath = toPosixPath(path.relative(projectRoot, resolvedTargetPath));
131
+ const sourceDirectory = toPosixPath(path.dirname(sourceRelativePath));
132
+ const targetDirectory = toPosixPath(path.dirname(targetRelativePath));
133
+ const sourceIsApplicationChat = sourceRelativePath.startsWith("src/application/chat/");
134
+
135
+ if (specifier.startsWith("./") && sourceDirectory === targetDirectory) {
136
+ return true;
137
+ }
138
+
139
+ if (targetRelativePath === "src/core/types.ts") {
140
+ return true;
141
+ }
142
+
143
+ if (targetRelativePath.startsWith("src/domain/chat/")) {
144
+ return true;
145
+ }
146
+
147
+ if (sourceIsApplicationChat && targetRelativePath.startsWith("src/application/chat/")) {
148
+ return true;
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ function resolveImport(sourceFilePath, specifier) {
155
+ const candidateBasePath = path.resolve(path.dirname(sourceFilePath), specifier);
156
+ return buildResolutionCandidates(candidateBasePath).find(fileExists) ?? null;
157
+ }
158
+
159
+ function buildResolutionCandidates(candidateBasePath) {
160
+ const extension = path.extname(candidateBasePath);
161
+
162
+ if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
163
+ return [candidateBasePath];
164
+ }
165
+
166
+ if (extension === ".js") {
167
+ return [candidateBasePath.slice(0, -3) + ".ts"];
168
+ }
169
+
170
+ if (extension === ".mjs") {
171
+ return [candidateBasePath.slice(0, -4) + ".mts"];
172
+ }
173
+
174
+ if (extension === ".cjs") {
175
+ return [candidateBasePath.slice(0, -4) + ".cts"];
176
+ }
177
+
178
+ return [
179
+ `${candidateBasePath}.ts`,
180
+ `${candidateBasePath}.mts`,
181
+ `${candidateBasePath}.cts`,
182
+ path.join(candidateBasePath, "index.ts"),
183
+ path.join(candidateBasePath, "index.mts"),
184
+ path.join(candidateBasePath, "index.cts"),
185
+ ];
186
+ }
187
+
188
+ function fileExists(filePath) {
189
+ return Boolean(filePath) && existsSync(filePath);
190
+ }
191
+
192
+ function toPosixPath(filePath) {
193
+ return filePath.split(path.sep).join("/");
194
+ }
195
+
196
+ async function main() {
197
+ const { violations, warnings } = await checkChatBoundary();
198
+
199
+ for (const warning of warnings) {
200
+ console.error(
201
+ `warning: stale allowlist entry: ${warning.importer}:${warning.specifier} (${warning.reason})`,
202
+ );
203
+ }
204
+
205
+ if (violations.length > 0) {
206
+ for (const violation of violations) {
207
+ console.error(
208
+ `AF-1 violation: ${violation.file}:${violation.line} imports "${violation.specifier}"`,
209
+ );
210
+ }
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+
215
+ console.error("✓ AF-1: chat boundary isolation check passed");
216
+ }
217
+
218
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
219
+ await main();
220
+ }
@@ -0,0 +1,139 @@
1
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { CHAT_BOUNDARY_ALLOWLIST, checkChatBoundary } from "./check-chat-boundary.mjs";
6
+
7
+ const workspaces: string[] = [];
8
+
9
+ afterEach(async () => {
10
+ const { rm } = await import("node:fs/promises");
11
+ await Promise.all(workspaces.map((workspace) => rm(workspace, { recursive: true, force: true })));
12
+ workspaces.length = 0;
13
+ });
14
+
15
+ describe("checkChatBoundary", () => {
16
+ it("accepts allowed imports including src/core/types.ts", async () => {
17
+ const projectRoot = await createBoundaryFixture({
18
+ "src/application/chat/chat-daemon-service.ts": [
19
+ 'import { execFile } from "node:child_process";',
20
+ 'import { ValidationError } from "../../core/types.js";',
21
+ 'import { ChatOutboundHandler } from "./chat-outbound-handler.js";',
22
+ 'import type { ChatPlatformAdapter } from "../../domain/chat/ports.js";',
23
+ "void execFile;",
24
+ "void ValidationError;",
25
+ "void ChatOutboundHandler;",
26
+ ].join("\n"),
27
+ "src/application/chat/chat-outbound-handler.ts": [
28
+ 'import { MessageSplitter } from "../../domain/chat/message-splitter.js";',
29
+ "void MessageSplitter;",
30
+ ].join("\n"),
31
+ "src/core/types.ts": "export class ValidationError extends Error {}\n",
32
+ "src/domain/chat/message-splitter.ts": "export class MessageSplitter {}\n",
33
+ "src/domain/chat/ports.ts": "export interface ChatPlatformAdapter {}\n",
34
+ });
35
+
36
+ const result = await checkChatBoundary({ projectRoot });
37
+
38
+ expect(result.violations).toEqual([]);
39
+ expect(result.warnings).toEqual([]);
40
+ });
41
+
42
+ it("reports imports that escape the allowed chat boundary", async () => {
43
+ const projectRoot = await createBoundaryFixture({
44
+ "src/application/chat/chat-daemon-service.ts": [
45
+ 'import { ValidationError } from "../../core/errors.js";',
46
+ "void ValidationError;",
47
+ ].join("\n"),
48
+ "src/core/errors.ts": "export class ValidationError extends Error {}\n",
49
+ "src/domain/chat/bad.ts": [
50
+ 'import { resolveChatConfig } from "../config.js";',
51
+ "void resolveChatConfig;",
52
+ ].join("\n"),
53
+ "src/domain/config.ts": "export function resolveChatConfig() { return {}; }\n",
54
+ });
55
+
56
+ const result = await checkChatBoundary({ projectRoot });
57
+
58
+ expect(result.violations).toEqual([
59
+ {
60
+ file: "src/domain/chat/bad.ts",
61
+ line: 1,
62
+ specifier: "../config.js",
63
+ },
64
+ {
65
+ file: "src/application/chat/chat-daemon-service.ts",
66
+ line: 1,
67
+ specifier: "../../core/errors.js",
68
+ },
69
+ ]);
70
+ });
71
+
72
+ it("reports dynamic imports that escape the allowed chat boundary", async () => {
73
+ const projectRoot = await createBoundaryFixture({
74
+ "src/application/chat/chat-daemon-service.ts": [
75
+ 'async function loadConfig() {',
76
+ ' return import("../../domain/config.js");',
77
+ "}",
78
+ "void loadConfig;",
79
+ ].join("\n"),
80
+ "src/domain/config.ts": "export function resolveChatConfig() { return {}; }\n",
81
+ "src/domain/chat/message-splitter.ts": "export class MessageSplitter {}\n",
82
+ });
83
+
84
+ const result = await checkChatBoundary({ projectRoot });
85
+
86
+ expect(result.violations).toEqual([
87
+ {
88
+ file: "src/application/chat/chat-daemon-service.ts",
89
+ line: 2,
90
+ specifier: "../../domain/config.js",
91
+ },
92
+ ]);
93
+ });
94
+
95
+ it("warns on stale allowlist entries", async () => {
96
+ CHAT_BOUNDARY_ALLOWLIST.push({
97
+ importer: "src/application/chat/chat-daemon-service.ts",
98
+ specifier: "../../domain/config.js",
99
+ reason: "temporary exception for test",
100
+ });
101
+
102
+ try {
103
+ const projectRoot = await createBoundaryFixture({
104
+ "src/application/chat/chat-daemon-service.ts": [
105
+ 'import { ValidationError } from "../../core/types.js";',
106
+ "void ValidationError;",
107
+ ].join("\n"),
108
+ "src/core/types.ts": "export class ValidationError extends Error {}\n",
109
+ "src/domain/chat/message-splitter.ts": "export class MessageSplitter {}\n",
110
+ });
111
+
112
+ const result = await checkChatBoundary({ projectRoot });
113
+
114
+ expect(result.violations).toEqual([]);
115
+ expect(result.warnings).toEqual([
116
+ {
117
+ importer: "src/application/chat/chat-daemon-service.ts",
118
+ specifier: "../../domain/config.js",
119
+ reason: "temporary exception for test",
120
+ },
121
+ ]);
122
+ } finally {
123
+ CHAT_BOUNDARY_ALLOWLIST.splice(0);
124
+ }
125
+ });
126
+ });
127
+
128
+ async function createBoundaryFixture(files: Record<string, string>) {
129
+ const projectRoot = await mkdtemp(path.join(os.tmpdir(), "agentic-relay-boundary-"));
130
+ workspaces.push(projectRoot);
131
+
132
+ for (const [relativePath, content] of Object.entries(files)) {
133
+ const absolutePath = path.join(projectRoot, relativePath);
134
+ await mkdir(path.dirname(absolutePath), { recursive: true });
135
+ await writeFile(absolutePath, content, "utf8");
136
+ }
137
+
138
+ return projectRoot;
139
+ }
@@ -0,0 +1,199 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
8
+ const STATIC_IMPORT_RE =
9
+ /^[\t ]*import(?!\s+type\b)(?:[\s\S]*?\sfrom\s+|\s+)["']([^"'\n]+)["'];?/gm;
10
+ const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
11
+
12
+ export async function checkCircularDeps(options = {}) {
13
+ const projectRoot = options.projectRoot ?? PROJECT_ROOT;
14
+ const graph = await buildDependencyGraph(projectRoot);
15
+ const cycles = findCircularDependencies(graph);
16
+ return { graph, cycles };
17
+ }
18
+
19
+ export async function buildDependencyGraph(projectRoot = PROJECT_ROOT) {
20
+ const srcRoot = path.join(projectRoot, "src");
21
+ const files = await walk(srcRoot);
22
+ const graph = new Map();
23
+
24
+ for (const filePath of files) {
25
+ if (!filePath.endsWith(".ts") || filePath.endsWith(".test.ts") || filePath.endsWith(".d.ts")) {
26
+ continue;
27
+ }
28
+
29
+ const source = await readFile(filePath, "utf8");
30
+ const dependencies = new Set();
31
+ for (const specifier of findImportSpecifiers(source)) {
32
+ if (!specifier.startsWith(".")) {
33
+ continue;
34
+ }
35
+ const resolvedTargetPath = resolveImport(filePath, specifier);
36
+ if (!resolvedTargetPath?.startsWith(srcRoot)) {
37
+ continue;
38
+ }
39
+ if (resolvedTargetPath.endsWith(".test.ts")) {
40
+ continue;
41
+ }
42
+ dependencies.add(toPosixPath(path.relative(projectRoot, resolvedTargetPath)));
43
+ }
44
+
45
+ graph.set(toPosixPath(path.relative(projectRoot, filePath)), dependencies);
46
+ }
47
+
48
+ return graph;
49
+ }
50
+
51
+ export function findCircularDependencies(graph) {
52
+ const visiting = new Set();
53
+ const visited = new Set();
54
+ const stack = [];
55
+ const cycles = [];
56
+ const seenCycles = new Set();
57
+
58
+ const nodes = [...graph.keys()].sort();
59
+
60
+ const visit = (node) => {
61
+ visiting.add(node);
62
+ stack.push(node);
63
+
64
+ for (const dependency of [...(graph.get(node) ?? [])].sort()) {
65
+ if (!graph.has(dependency)) {
66
+ continue;
67
+ }
68
+
69
+ if (visiting.has(dependency)) {
70
+ const startIndex = stack.indexOf(dependency);
71
+ const cycle = stack.slice(startIndex).concat(dependency);
72
+ const cycleKey = canonicalizeCycle(cycle.slice(0, -1));
73
+ if (!seenCycles.has(cycleKey)) {
74
+ seenCycles.add(cycleKey);
75
+ cycles.push(cycle);
76
+ }
77
+ continue;
78
+ }
79
+
80
+ if (!visited.has(dependency)) {
81
+ visit(dependency);
82
+ }
83
+ }
84
+
85
+ stack.pop();
86
+ visiting.delete(node);
87
+ visited.add(node);
88
+ };
89
+
90
+ for (const node of nodes) {
91
+ if (!visited.has(node)) {
92
+ visit(node);
93
+ }
94
+ }
95
+
96
+ return cycles;
97
+ }
98
+
99
+ async function walk(directory) {
100
+ const entries = await readdir(directory, { withFileTypes: true });
101
+ const files = [];
102
+
103
+ for (const entry of entries) {
104
+ if (entry.name === "dist" || entry.name === "node_modules") {
105
+ continue;
106
+ }
107
+
108
+ const fullPath = path.join(directory, entry.name);
109
+ if (entry.isDirectory()) {
110
+ files.push(...(await walk(fullPath)));
111
+ continue;
112
+ }
113
+ if (entry.isFile()) {
114
+ files.push(fullPath);
115
+ }
116
+ }
117
+
118
+ return files;
119
+ }
120
+
121
+ function findImportSpecifiers(source) {
122
+ const specifiers = new Set();
123
+
124
+ for (const match of source.matchAll(STATIC_IMPORT_RE)) {
125
+ specifiers.add(match[1]);
126
+ }
127
+
128
+ for (const match of source.matchAll(DYNAMIC_IMPORT_RE)) {
129
+ specifiers.add(match[1]);
130
+ }
131
+
132
+ return [...specifiers];
133
+ }
134
+
135
+ function resolveImport(sourceFilePath, specifier) {
136
+ const candidateBasePath = path.resolve(path.dirname(sourceFilePath), specifier);
137
+ return buildResolutionCandidates(candidateBasePath).find(fileExists) ?? null;
138
+ }
139
+
140
+ function buildResolutionCandidates(candidateBasePath) {
141
+ const extension = path.extname(candidateBasePath);
142
+
143
+ if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
144
+ return [candidateBasePath];
145
+ }
146
+
147
+ if (extension === ".js") {
148
+ return [candidateBasePath.slice(0, -3) + ".ts"];
149
+ }
150
+
151
+ if (extension === ".mjs") {
152
+ return [candidateBasePath.slice(0, -4) + ".mts"];
153
+ }
154
+
155
+ if (extension === ".cjs") {
156
+ return [candidateBasePath.slice(0, -4) + ".cts"];
157
+ }
158
+
159
+ return [
160
+ `${candidateBasePath}.ts`,
161
+ `${candidateBasePath}.mts`,
162
+ `${candidateBasePath}.cts`,
163
+ path.join(candidateBasePath, "index.ts"),
164
+ path.join(candidateBasePath, "index.mts"),
165
+ path.join(candidateBasePath, "index.cts"),
166
+ ];
167
+ }
168
+
169
+ function canonicalizeCycle(cycle) {
170
+ const rotations = cycle.map((_, index) =>
171
+ [...cycle.slice(index), ...cycle.slice(0, index)].join("->"),
172
+ );
173
+ return rotations.sort()[0];
174
+ }
175
+
176
+ function fileExists(filePath) {
177
+ return Boolean(filePath) && existsSync(filePath);
178
+ }
179
+
180
+ function toPosixPath(filePath) {
181
+ return filePath.split(path.sep).join("/");
182
+ }
183
+
184
+ async function main() {
185
+ const { cycles } = await checkCircularDeps();
186
+ if (cycles.length > 0) {
187
+ for (const cycle of cycles) {
188
+ console.error(`AF-2 cycle: ${cycle.join(" -> ")}`);
189
+ }
190
+ process.exitCode = 1;
191
+ return;
192
+ }
193
+
194
+ console.error("✓ AF-2: no circular dependencies found");
195
+ }
196
+
197
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
198
+ await main();
199
+ }
@@ -0,0 +1,62 @@
1
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { checkCircularDeps } from "./check-circular-deps.mjs";
6
+
7
+ const workspaces: string[] = [];
8
+
9
+ afterEach(async () => {
10
+ const { rm } = await import("node:fs/promises");
11
+ await Promise.all(workspaces.map((workspace) => rm(workspace, { recursive: true, force: true })));
12
+ workspaces.length = 0;
13
+ });
14
+
15
+ describe("checkCircularDeps", () => {
16
+ it("passes when relative imports do not form a cycle", async () => {
17
+ const projectRoot = await createCircularFixture({
18
+ "src/a.ts": 'import "./b.js";\n',
19
+ "src/b.ts": 'await import("./c.js");\n',
20
+ "src/c.ts": "export const c = 1;\n",
21
+ });
22
+
23
+ const result = await checkCircularDeps({ projectRoot });
24
+
25
+ expect(result.cycles).toEqual([]);
26
+ });
27
+
28
+ it("detects cycles across static and dynamic imports", async () => {
29
+ const projectRoot = await createCircularFixture({
30
+ "src/a.ts": 'await import("./b.js");\n',
31
+ "src/b.ts": 'import "./a.js";\n',
32
+ });
33
+
34
+ const result = await checkCircularDeps({ projectRoot });
35
+
36
+ expect(result.cycles).toEqual([["src/a.ts", "src/b.ts", "src/a.ts"]]);
37
+ });
38
+
39
+ it("ignores import type edges when detecting cycles", async () => {
40
+ const projectRoot = await createCircularFixture({
41
+ "src/a.ts": 'import type { B } from "./b.js";\nexport type A = { b: B };\n',
42
+ "src/b.ts": 'import type { A } from "./a.js";\nexport type B = { a: A };\n',
43
+ });
44
+
45
+ const result = await checkCircularDeps({ projectRoot });
46
+
47
+ expect(result.cycles).toEqual([]);
48
+ });
49
+ });
50
+
51
+ async function createCircularFixture(files: Record<string, string>) {
52
+ const projectRoot = await mkdtemp(path.join(os.tmpdir(), "agentic-relay-circular-"));
53
+ workspaces.push(projectRoot);
54
+
55
+ for (const [relativePath, content] of Object.entries(files)) {
56
+ const absolutePath = path.join(projectRoot, relativePath);
57
+ await mkdir(path.dirname(absolutePath), { recursive: true });
58
+ await writeFile(absolutePath, content, "utf8");
59
+ }
60
+
61
+ return projectRoot;
62
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"chat-types.js","sourceRoot":"","sources":["../../src/core/chat-types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,YAGX;AAHD,WAAY,YAAY;IACtB,+BAAe,CAAA;IACf,mCAAmB,CAAA;AACrB,CAAC,EAHW,YAAY,KAAZ,YAAY,QAGvB;AAED,MAAM,CAAN,IAAY,eAMX;AAND,WAAY,eAAe;IACzB,gDAA6B,CAAA;IAC7B,4CAAyB,CAAA;IACzB,0CAAuB,CAAA;IACvB,gDAA6B,CAAA;IAC7B,oCAAiB,CAAA;AACnB,CAAC,EANW,eAAe,KAAf,eAAe,QAM1B"}