@xpufx/paseo-x-comms 0.3.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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/client/attribution.ts +7 -0
  4. package/client/conversations.ts +229 -0
  5. package/client/main.tsx +885 -0
  6. package/client/peer-label.ts +98 -0
  7. package/client/peer-status.tsx +201 -0
  8. package/client/settings-prototype.tsx +395 -0
  9. package/client/tool-call.ts +115 -0
  10. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  11. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  12. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  13. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  14. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  15. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  16. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  17. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  18. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  19. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  20. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  21. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  22. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  23. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  24. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  25. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  26. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  27. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  28. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  29. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  31. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  33. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  34. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  35. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  36. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  37. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  38. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  39. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  40. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  41. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  42. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  43. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  44. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  45. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  47. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  50. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  51. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  52. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  53. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  54. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  55. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  56. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  57. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  58. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  59. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  60. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  61. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  62. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  63. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  64. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  65. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  66. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  67. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  68. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  69. package/client/via-x-comms.tsx +15 -0
  70. package/client/x-comms-conversation.tsx +449 -0
  71. package/client/x-comms-panel.tsx +6 -0
  72. package/client/x-comms-pill.tsx +193 -0
  73. package/client/x-comms-timeline.tsx +176 -0
  74. package/client/x-comms-tool-call.tsx +85 -0
  75. package/docs/mesh.md +115 -0
  76. package/mcp/LICENSE +202 -0
  77. package/mcp/README.md +287 -0
  78. package/mcp/mcp-config.example.json +8 -0
  79. package/mcp/package-lock.json +1186 -0
  80. package/mcp/package.json +43 -0
  81. package/mcp/paseo-cross-daemon-comms.example.json +5 -0
  82. package/mcp/paseo-x-comms.bundled.mjs +36964 -0
  83. package/mcp/paseo-x-comms.mjs +630 -0
  84. package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
  85. package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
  86. package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
  87. package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
  88. package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
  89. package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
  90. package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
  91. package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
  92. package/mcp/test/fixtures/fake-paseo.mjs +92 -0
  93. package/mcp/test/register-ts-hooks.mjs +6 -0
  94. package/mcp/test/resolve-ts-hooks.mjs +46 -0
  95. package/package.json +58 -0
  96. package/paseo-plugin.json +7 -0
  97. package/server/conversations-snapshot.ts +249 -0
  98. package/server/handlers.ts +1081 -0
  99. package/server/injection.ts +152 -0
  100. package/server/local-send.ts +153 -0
  101. package/server/mcp-client.ts +47 -0
  102. package/server/outbox.ts +233 -0
  103. package/server/peer-channel.ts +151 -0
  104. package/server/peer-status.ts +233 -0
  105. package/server/presence.ts +204 -0
  106. package/server/registry.ts +215 -0
  107. package/server/relay-status.ts +9 -0
  108. package/server/server-status.ts +138 -0
  109. package/server/settings.ts +81 -0
  110. package/server/snapshot.ts +176 -0
  111. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  112. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  113. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  114. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  115. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  116. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  117. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  118. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  119. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  120. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  121. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  122. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  123. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  124. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  125. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  126. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  127. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  128. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  129. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  130. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  131. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  132. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  133. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  134. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  135. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  136. package/shared/conversations-snapshot.ts +39 -0
  137. package/shared/envelope.ts +92 -0
  138. package/shared/outbox.ts +21 -0
  139. package/shared/registry.ts +366 -0
  140. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  141. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  142. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  143. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  144. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  145. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  146. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  147. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  148. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  149. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  150. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  151. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  152. package/shared/version.ts +2 -0
@@ -0,0 +1,367 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { parseJsonc } from "./jsonc";
5
+
6
+ export interface McpServerConfig {
7
+ command: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ disabled?: boolean;
11
+ autoApprove?: string[];
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export interface McpConfigTarget {
16
+ /**
17
+ * Absolute or relative path to the configuration file.
18
+ * Supports '~' expansion (e.g. "~/.claude.json").
19
+ */
20
+ path: string;
21
+
22
+ /**
23
+ * The top-level key under which MCP servers are defined.
24
+ * Defaults to "mcpServers".
25
+ */
26
+ key?: string;
27
+
28
+ /**
29
+ * If true, creates a timestamped backup file before modifying an existing file.
30
+ */
31
+ backup?: boolean;
32
+ }
33
+
34
+ export interface UpsertMcpServerOptions {
35
+ /**
36
+ * Target config file path or McpConfigTarget descriptor.
37
+ */
38
+ target: string | McpConfigTarget;
39
+
40
+ /**
41
+ * Unique name of the MCP server entry (e.g. "paseo-gateway", "top", "x-comms").
42
+ */
43
+ serverName: string;
44
+
45
+ /**
46
+ * MCP server configuration object (command, args, env, etc.).
47
+ */
48
+ config: McpServerConfig;
49
+
50
+ /**
51
+ * Override backup option. If true, creates a backup before modifying.
52
+ */
53
+ backup?: boolean;
54
+ }
55
+
56
+ export interface RemoveMcpServerOptions {
57
+ /**
58
+ * Target config file path or McpConfigTarget descriptor.
59
+ */
60
+ target: string | McpConfigTarget;
61
+
62
+ /**
63
+ * Name of the MCP server entry to remove.
64
+ */
65
+ serverName: string;
66
+
67
+ /**
68
+ * Override backup option. If true, creates a backup before removing.
69
+ */
70
+ backup?: boolean;
71
+ }
72
+
73
+ export interface McpMutationResult {
74
+ filePath: string;
75
+ serverName: string;
76
+ changed: boolean;
77
+ action: "created" | "updated" | "unchanged" | "removed" | "not_found";
78
+ backupPath?: string;
79
+ }
80
+
81
+ /**
82
+ * Standard known MCP configuration paths across major developer tools and agent runners.
83
+ * Note: These are pure path resolvers with zero filesystem scanning or heuristics.
84
+ */
85
+ export const McpConfigPaths = {
86
+ /**
87
+ * Claude Desktop configuration path per operating system:
88
+ * - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
89
+ * - Windows: %APPDATA%/Claude/claude_desktop_config.json
90
+ * - Linux: ~/.config/Claude/claude_desktop_config.json
91
+ */
92
+ claudeDesktop(): string {
93
+ const platform = process.platform;
94
+ const home = os.homedir();
95
+ if (platform === "darwin") {
96
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
97
+ }
98
+ if (platform === "win32") {
99
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
100
+ return path.join(appData, "Claude", "claude_desktop_config.json");
101
+ }
102
+ const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
103
+ return path.join(configDir, "Claude", "claude_desktop_config.json");
104
+ },
105
+
106
+ /**
107
+ * Claude Code CLI global configuration: ~/.claude.json
108
+ */
109
+ claudeCode(): string {
110
+ return path.join(os.homedir(), ".claude.json");
111
+ },
112
+
113
+ /**
114
+ * OpenCode configuration path: ~/.config/opencode/opencode.json
115
+ */
116
+ openCode(): string {
117
+ const home = os.homedir();
118
+ const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
119
+ return path.join(configDir, "opencode", "opencode.json");
120
+ },
121
+
122
+ /**
123
+ * Cursor editor MCP configuration: ~/.cursor/mcp.json
124
+ */
125
+ cursor(): string {
126
+ return path.join(os.homedir(), ".cursor", "mcp.json");
127
+ },
128
+
129
+ /**
130
+ * Gemini / Antigravity CLI configuration: ~/.gemini/config/mcp_config.json
131
+ */
132
+ gemini(): string {
133
+ return path.join(os.homedir(), ".gemini", "config", "mcp_config.json");
134
+ },
135
+
136
+ /**
137
+ * Pi CLI agent configuration: ~/.pi/config.json
138
+ */
139
+ pi(): string {
140
+ return path.join(os.homedir(), ".pi", "config.json");
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Expands '~' to the user's home directory.
146
+ */
147
+ export function expandPath(targetPath: string): string {
148
+ if (targetPath === "~") {
149
+ return os.homedir();
150
+ }
151
+ if (targetPath.startsWith("~/") || targetPath.startsWith("~\\")) {
152
+ return path.join(os.homedir(), targetPath.slice(2));
153
+ }
154
+ return path.resolve(targetPath);
155
+ }
156
+
157
+ /**
158
+ * Deep equality check for primitives, arrays, and objects.
159
+ */
160
+ function isDeepEqual(a: unknown, b: unknown): boolean {
161
+ if (a === b) return true;
162
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
163
+ return false;
164
+ }
165
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
166
+
167
+ if (Array.isArray(a) && Array.isArray(b)) {
168
+ if (a.length !== b.length) return false;
169
+ for (let i = 0; i < a.length; i++) {
170
+ if (!isDeepEqual(a[i], b[i])) return false;
171
+ }
172
+ return true;
173
+ }
174
+
175
+ const keysA = Object.keys(a as Record<string, unknown>);
176
+ const keysB = Object.keys(b as Record<string, unknown>);
177
+
178
+ if (keysA.length !== keysB.length) return false;
179
+ for (const key of keysA) {
180
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
181
+ if (!isDeepEqual((a as any)[key], (b as any)[key])) return false;
182
+ }
183
+ return true;
184
+ }
185
+
186
+ /**
187
+ * Normalizes target string or descriptor into { filePath, key, backup }.
188
+ */
189
+ function normalizeTarget(target: string | McpConfigTarget, backupOverride?: boolean): {
190
+ filePath: string;
191
+ key: string;
192
+ backup: boolean;
193
+ } {
194
+ if (typeof target === "string") {
195
+ return {
196
+ filePath: expandPath(target),
197
+ key: "mcpServers",
198
+ backup: Boolean(backupOverride),
199
+ };
200
+ }
201
+ return {
202
+ filePath: expandPath(target.path),
203
+ key: target.key || "mcpServers",
204
+ backup: backupOverride !== undefined ? backupOverride : Boolean(target.backup),
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Reads an existing configuration file safely using JSONC parser.
210
+ * Returns an empty object if the file does not exist.
211
+ */
212
+ function readConfigDocument(filePath: string): Record<string, any> {
213
+ if (!fs.existsSync(filePath)) {
214
+ return {};
215
+ }
216
+ const raw = fs.readFileSync(filePath, "utf8");
217
+ if (!raw.trim()) {
218
+ return {};
219
+ }
220
+ return parseJsonc<Record<string, any>>(raw);
221
+ }
222
+
223
+ /**
224
+ * Writes data atomically to the given file path using a temporary file and POSIX atomic rename.
225
+ */
226
+ function writeConfigAtomic(filePath: string, data: Record<string, any>): void {
227
+ const dir = path.dirname(filePath);
228
+ if (!fs.existsSync(dir)) {
229
+ fs.mkdirSync(dir, { recursive: true });
230
+ }
231
+
232
+ const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
233
+ const serialized = JSON.stringify(data, null, 2) + "\n";
234
+ fs.writeFileSync(tempPath, serialized, "utf8");
235
+ fs.renameSync(tempPath, filePath);
236
+ }
237
+
238
+ /**
239
+ * Retrieves an MCP server definition from a config file.
240
+ * Returns null if the file, key, or server does not exist.
241
+ */
242
+ export function getMcpServer(
243
+ target: string | McpConfigTarget,
244
+ serverName: string
245
+ ): McpServerConfig | null {
246
+ const { filePath, key } = normalizeTarget(target);
247
+ if (!fs.existsSync(filePath)) {
248
+ return null;
249
+ }
250
+ try {
251
+ const doc = readConfigDocument(filePath);
252
+ const servers = doc[key];
253
+ if (servers && typeof servers === "object" && serverName in servers) {
254
+ return servers[serverName] as McpServerConfig;
255
+ }
256
+ return null;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Upserts an MCP server definition into an agent or tool configuration file.
264
+ *
265
+ * Guarantees:
266
+ * 1. Namespaced idempotency: If the server configuration already exists and matches,
267
+ * no write occurs, preventing file watcher thrashing.
268
+ * 2. JSONC safe: Reads files containing comments or trailing commas without crashing.
269
+ * 3. Atomic writes: Uses temporary files and atomic rename to prevent corruption.
270
+ * 4. Optional backups: Can create a timestamped backup before modifying existing files.
271
+ */
272
+ export function upsertMcpServer(options: UpsertMcpServerOptions): McpMutationResult {
273
+ const { filePath, key, backup } = normalizeTarget(options.target, options.backup);
274
+ const { serverName, config } = options;
275
+
276
+ let doc: Record<string, any>;
277
+ const fileExisted = fs.existsSync(filePath);
278
+
279
+ if (fileExisted) {
280
+ doc = readConfigDocument(filePath);
281
+ } else {
282
+ doc = {};
283
+ }
284
+
285
+ if (typeof doc[key] !== "object" || doc[key] === null || Array.isArray(doc[key])) {
286
+ doc[key] = {};
287
+ }
288
+
289
+ const existingConfig = doc[key][serverName];
290
+ const isExisting = existingConfig !== undefined;
291
+
292
+ // Idempotency: Deep equality check
293
+ if (isExisting && isDeepEqual(existingConfig, config)) {
294
+ return {
295
+ filePath,
296
+ serverName,
297
+ changed: false,
298
+ action: "unchanged",
299
+ };
300
+ }
301
+
302
+ let backupPath: string | undefined;
303
+ if (backup && fileExisted) {
304
+ backupPath = `${filePath}.bak.${Date.now()}`;
305
+ fs.copyFileSync(filePath, backupPath);
306
+ }
307
+
308
+ doc[key][serverName] = config;
309
+ writeConfigAtomic(filePath, doc);
310
+
311
+ return {
312
+ filePath,
313
+ serverName,
314
+ changed: true,
315
+ action: isExisting ? "updated" : "created",
316
+ backupPath,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Removes an MCP server definition from a config file.
322
+ *
323
+ * Guarantees:
324
+ * 1. Safe no-op: If the file or server does not exist, no write occurs.
325
+ * 2. Atomic writes: Cleanly removes the key and updates the file atomically.
326
+ * 3. Optional backups: Can create a timestamped backup before removal.
327
+ */
328
+ export function removeMcpServer(options: RemoveMcpServerOptions): McpMutationResult {
329
+ const { filePath, key, backup } = normalizeTarget(options.target, options.backup);
330
+ const { serverName } = options;
331
+
332
+ if (!fs.existsSync(filePath)) {
333
+ return {
334
+ filePath,
335
+ serverName,
336
+ changed: false,
337
+ action: "not_found",
338
+ };
339
+ }
340
+
341
+ const doc = readConfigDocument(filePath);
342
+ if (!doc[key] || typeof doc[key] !== "object" || !(serverName in doc[key])) {
343
+ return {
344
+ filePath,
345
+ serverName,
346
+ changed: false,
347
+ action: "not_found",
348
+ };
349
+ }
350
+
351
+ let backupPath: string | undefined;
352
+ if (backup) {
353
+ backupPath = `${filePath}.bak.${Date.now()}`;
354
+ fs.copyFileSync(filePath, backupPath);
355
+ }
356
+
357
+ delete doc[key][serverName];
358
+ writeConfigAtomic(filePath, doc);
359
+
360
+ return {
361
+ filePath,
362
+ serverName,
363
+ changed: true,
364
+ action: "removed",
365
+ backupPath,
366
+ };
367
+ }
@@ -0,0 +1,85 @@
1
+ export interface McpStdioInjectionConfig {
2
+ type: "stdio";
3
+ command: string;
4
+ args?: string[];
5
+ env?: Record<string, string>;
6
+ alwaysLoad?: boolean;
7
+ }
8
+
9
+ export interface McpHttpInjectionConfig {
10
+ type: "http";
11
+ url: string;
12
+ headers?: Record<string, string>;
13
+ alwaysLoad?: boolean;
14
+ }
15
+
16
+ export interface McpSseInjectionConfig {
17
+ type: "sse";
18
+ url: string;
19
+ headers?: Record<string, string>;
20
+ alwaysLoad?: boolean;
21
+ }
22
+
23
+ export type McpInjectionConfig =
24
+ | McpStdioInjectionConfig
25
+ | McpHttpInjectionConfig
26
+ | McpSseInjectionConfig;
27
+
28
+ export interface AgentCreateInjectionConfig {
29
+ mcpServers?: Record<string, McpInjectionConfig>;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface AgentCreateInjectionRequest {
34
+ config: AgentCreateInjectionConfig;
35
+ env?: Record<string, string>;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export type McpInjectionFilter = (input: {
40
+ request: AgentCreateInjectionRequest;
41
+ }) => boolean;
42
+
43
+ export type McpInjectionHookHandler = (
44
+ input: { request: AgentCreateInjectionRequest },
45
+ context?: unknown,
46
+ ) => AgentCreateInjectionRequest | void | Promise<AgentCreateInjectionRequest | void>;
47
+
48
+ export interface McpInjectionServer {
49
+ // Loose on purpose: the real SDK declares a generic
50
+ // before<Name extends keyof PluginBeforeRequests>(...) whose name param is
51
+ // narrower than string and whose handler uses SDK request types. Typing this
52
+ // boundary with any keeps the SDK object directly assignable with no
53
+ // consumer-side adapter and no SDK imports here. Handler authors should use
54
+ // McpInjectionHookHandler for the precise shape.
55
+ before(
56
+ name: string,
57
+ handler: (input: { request: any }, context?: any) => any,
58
+ ): () => void;
59
+ }
60
+
61
+ export interface RegisterMcpInjectionOptions {
62
+ serverName: string;
63
+ config: McpInjectionConfig;
64
+ filter?: McpInjectionFilter;
65
+ }
66
+
67
+ export function registerMcpInjection(
68
+ server: McpInjectionServer,
69
+ options: RegisterMcpInjectionOptions,
70
+ ): () => void {
71
+ const { serverName, config, filter } = options;
72
+ return server.before("agent.create", ({ request }: { request: AgentCreateInjectionRequest }) => {
73
+ if (filter && !filter({ request })) return;
74
+ return {
75
+ ...request,
76
+ config: {
77
+ ...request.config,
78
+ mcpServers: {
79
+ ...(request.config.mcpServers ?? {}),
80
+ [serverName]: config,
81
+ },
82
+ },
83
+ };
84
+ });
85
+ }
@@ -0,0 +1,91 @@
1
+ import net from "node:net";
2
+
3
+ export interface PingHostOptions {
4
+ timeoutMs?: number;
5
+ }
6
+
7
+ /**
8
+ * Checks whether a TCP port is currently open and accepting connections on a host.
9
+ */
10
+ export function isPortOpen(port: number, host = "127.0.0.1", options?: PingHostOptions): Promise<boolean> {
11
+ const timeoutMs = options?.timeoutMs ?? 1000;
12
+
13
+ return new Promise((resolve) => {
14
+ const socket = new net.Socket();
15
+ let settled = false;
16
+
17
+ const cleanup = () => {
18
+ if (!settled) {
19
+ settled = true;
20
+ socket.destroy();
21
+ }
22
+ };
23
+
24
+ socket.setTimeout(timeoutMs);
25
+
26
+ socket.once("connect", () => {
27
+ cleanup();
28
+ resolve(true);
29
+ });
30
+
31
+ socket.once("timeout", () => {
32
+ cleanup();
33
+ resolve(false);
34
+ });
35
+
36
+ socket.once("error", () => {
37
+ cleanup();
38
+ resolve(false);
39
+ });
40
+
41
+ try {
42
+ socket.connect(port, host);
43
+ } catch {
44
+ cleanup();
45
+ resolve(false);
46
+ }
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Finds an available TCP port starting from startPort.
52
+ * Tries up to maxAttempts ports.
53
+ */
54
+ export function findAvailablePort(startPort = 3000, maxAttempts = 50, host = "127.0.0.1"): Promise<number> {
55
+ return new Promise((resolve, reject) => {
56
+ let currentPort = startPort;
57
+ const endPort = startPort + maxAttempts;
58
+
59
+ function tryNext() {
60
+ if (currentPort >= endPort) {
61
+ return reject(new Error(`No available port found in range [${startPort}, ${endPort})`));
62
+ }
63
+
64
+ const server = net.createServer();
65
+
66
+ server.once("error", () => {
67
+ currentPort++;
68
+ tryNext();
69
+ });
70
+
71
+ server.once("listening", () => {
72
+ const addr = server.address();
73
+ const port = typeof addr === "object" && addr ? addr.port : currentPort;
74
+ server.close(() => {
75
+ resolve(port);
76
+ });
77
+ });
78
+
79
+ server.listen(currentPort, host);
80
+ }
81
+
82
+ tryNext();
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Pings a host and port via TCP to verify connectivity.
88
+ */
89
+ export async function pingHost(host: string, port: number, options?: PingHostOptions): Promise<boolean> {
90
+ return isPortOpen(port, host, options);
91
+ }
@@ -0,0 +1,160 @@
1
+ import { safeSpawn } from "./process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+
6
+ export interface PaseoPluginInfo {
7
+ id: string;
8
+ path: string;
9
+ enabled: boolean;
10
+ status: "running" | "disabled" | "failed" | string;
11
+ source?: "directory" | "git" | string;
12
+ remote?: string;
13
+ ref?: string;
14
+ commit?: string;
15
+ error?: string;
16
+ }
17
+
18
+ export type PluginStatusFilter = "all" | "enabled" | "disabled" | "running" | "failed";
19
+
20
+ export interface ListPluginsOptions {
21
+ /** Filter results by status or enablement. Defaults to "all". */
22
+ filter?: PluginStatusFilter;
23
+ /** In-memory TTL cache duration in milliseconds. Defaults to 5000ms. */
24
+ cacheTtlMs?: number;
25
+ /** If true, bypasses the in-memory cache and queries the daemon fresh. */
26
+ forceRefresh?: boolean;
27
+ }
28
+
29
+ let cachedPlugins: PaseoPluginInfo[] | null = null;
30
+ let lastFetchTime = 0;
31
+
32
+ /**
33
+ * Clears the in-memory plugin list cache.
34
+ */
35
+ export function clearPluginCache(): void {
36
+ cachedPlugins = null;
37
+ lastFetchTime = 0;
38
+ }
39
+
40
+ /**
41
+ * Reads configured plugins fallback from ~/.paseo/config.json when CLI is unavailable.
42
+ */
43
+ function readConfigPluginsFallback(): PaseoPluginInfo[] {
44
+ try {
45
+ const configPath = path.join(os.homedir(), ".paseo", "config.json");
46
+ if (!fs.existsSync(configPath)) return [];
47
+ const raw = fs.readFileSync(configPath, "utf-8");
48
+ const parsed = JSON.parse(raw);
49
+ const pluginsObj = parsed?.plugins;
50
+ if (!pluginsObj || typeof pluginsObj !== "object") return [];
51
+
52
+ return Object.entries(pluginsObj).map(([id, val]: [string, any]) => {
53
+ const isEnabled = val?.enabled !== false;
54
+ return {
55
+ id,
56
+ path: val?.path ?? "",
57
+ enabled: isEnabled,
58
+ status: isEnabled ? "unknown" : "disabled",
59
+ source: val?.source,
60
+ remote: val?.remote,
61
+ ref: val?.ref,
62
+ commit: val?.commit,
63
+ };
64
+ });
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Lists plugins from the Paseo daemon, with optional filtering and TTL caching.
72
+ * Primary mechanism uses 'paseo plugin ls --json', with fallback to ~/.paseo/config.json.
73
+ */
74
+ export async function listPlugins(options: ListPluginsOptions = {}): Promise<PaseoPluginInfo[]> {
75
+ const { filter = "all", cacheTtlMs = 5000, forceRefresh = false } = options;
76
+ const now = Date.now();
77
+
78
+ if (!forceRefresh && cachedPlugins && now - lastFetchTime < cacheTtlMs) {
79
+ return applyFilter(cachedPlugins, filter);
80
+ }
81
+
82
+ let plugins: PaseoPluginInfo[] = [];
83
+
84
+ try {
85
+ const result = await safeSpawn("paseo", ["plugin", "ls", "--json"], { timeoutMs: 3000 });
86
+ if (result.code === 0 && result.stdout.trim()) {
87
+ plugins = JSON.parse(result.stdout.trim());
88
+ } else {
89
+ plugins = readConfigPluginsFallback();
90
+ }
91
+ } catch {
92
+ plugins = readConfigPluginsFallback();
93
+ }
94
+
95
+ cachedPlugins = plugins;
96
+ lastFetchTime = now;
97
+
98
+ return applyFilter(plugins, filter);
99
+ }
100
+
101
+ function applyFilter(plugins: PaseoPluginInfo[], filter: PluginStatusFilter): PaseoPluginInfo[] {
102
+ switch (filter) {
103
+ case "enabled":
104
+ return plugins.filter((p) => p.enabled);
105
+ case "disabled":
106
+ return plugins.filter((p) => !p.enabled);
107
+ case "running":
108
+ return plugins.filter((p) => p.status === "running");
109
+ case "failed":
110
+ return plugins.filter((p) => p.status === "failed");
111
+ case "all":
112
+ default:
113
+ return plugins;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Retrieves metadata for a specific plugin by ID.
119
+ * Returns null if the plugin is not installed or found.
120
+ */
121
+ export async function getPluginInfo(
122
+ pluginId: string,
123
+ options?: { cacheTtlMs?: number; forceRefresh?: boolean },
124
+ ): Promise<PaseoPluginInfo | null> {
125
+ const plugins = await listPlugins(options);
126
+ return plugins.find((p) => p.id === pluginId) ?? null;
127
+ }
128
+
129
+ /**
130
+ * Checks whether a plugin is installed in Paseo.
131
+ */
132
+ export async function isPluginInstalled(
133
+ pluginId: string,
134
+ options?: { cacheTtlMs?: number; forceRefresh?: boolean },
135
+ ): Promise<boolean> {
136
+ const info = await getPluginInfo(pluginId, options);
137
+ return info !== null;
138
+ }
139
+
140
+ /**
141
+ * Checks whether a plugin is installed and marked as enabled in Paseo.
142
+ */
143
+ export async function isPluginEnabled(
144
+ pluginId: string,
145
+ options?: { cacheTtlMs?: number; forceRefresh?: boolean },
146
+ ): Promise<boolean> {
147
+ const info = await getPluginInfo(pluginId, options);
148
+ return info !== null && info.enabled;
149
+ }
150
+
151
+ /**
152
+ * Checks whether a plugin is currently running in the Paseo daemon.
153
+ */
154
+ export async function isPluginRunning(
155
+ pluginId: string,
156
+ options?: { cacheTtlMs?: number; forceRefresh?: boolean },
157
+ ): Promise<boolean> {
158
+ const info = await getPluginInfo(pluginId, options);
159
+ return info !== null && info.status === "running";
160
+ }