@xpufx/paseo-mcp-tools 0.1.3

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/client/__snapshots__/attribution.test.tsx.snap +170 -0
  4. package/client/commands.ts +113 -0
  5. package/client/mcp-query.tsx +39 -0
  6. package/client/pill.tsx +886 -0
  7. package/client/timeline.tsx +136 -0
  8. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  9. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  10. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  11. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  12. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  13. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  14. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  15. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  16. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  17. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  18. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  19. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  20. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  21. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  22. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  23. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  24. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  25. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  26. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  27. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  28. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  29. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  31. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  32. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  33. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  34. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  35. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  36. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  37. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  38. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  39. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  40. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  41. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  42. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  44. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  46. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  47. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  48. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  49. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  50. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  51. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  52. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  53. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  54. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  55. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  56. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  57. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  58. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  59. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  60. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  61. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  62. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  63. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  64. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  65. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  66. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  67. package/docs/TEST_METHODOLOGY.md +29 -0
  68. package/package.json +41 -0
  69. package/paseo-plugin.json +4 -0
  70. package/scripts/version.mjs +4 -0
  71. package/server/discovery/extract.ts +277 -0
  72. package/server/discovery/types.ts +23 -0
  73. package/server/health/health.ts +274 -0
  74. package/server/inject.ts +32 -0
  75. package/server/mcp.ts +365 -0
  76. package/server/providers/README.md +51 -0
  77. package/server/providers/antigravity.ts +22 -0
  78. package/server/providers/catalog.ts +6 -0
  79. package/server/providers/claude.ts +21 -0
  80. package/server/providers/codex.ts +85 -0
  81. package/server/providers/family.ts +24 -0
  82. package/server/providers/index.ts +11 -0
  83. package/server/providers/opencode.ts +95 -0
  84. package/server/providers/paseo.ts +112 -0
  85. package/server/providers/pi.ts +39 -0
  86. package/server/settings.ts +19 -0
  87. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  88. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  89. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  90. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  91. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  92. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  93. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  94. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  95. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  96. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  97. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  98. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  100. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  101. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  102. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  103. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  104. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  105. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  106. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  107. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  108. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  109. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  110. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  111. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  112. package/shared/mcp.ts +207 -0
  113. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  114. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  115. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  116. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  117. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  118. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  119. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  120. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  121. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  122. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  123. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  124. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  125. package/shared/version.ts +2 -0
@@ -0,0 +1,418 @@
1
+ import type { PluginLogger } from "./logger";
2
+
3
+ export const DEFAULT_BEACON_LABEL_PREFIX = "beacon:";
4
+
5
+ export const BEACON_COLORS = [
6
+ "violet",
7
+ "sky",
8
+ "emerald",
9
+ "orange",
10
+ "pink",
11
+ "indigo",
12
+ "teal",
13
+ "red",
14
+ "amber",
15
+ "blue",
16
+ ] as const;
17
+
18
+ export type BeaconColor = (typeof BEACON_COLORS)[number];
19
+
20
+ export interface BeaconLabelState {
21
+ name: string;
22
+ color?: string;
23
+ titleSuffix?: string;
24
+ title?: string;
25
+ workspaceId?: string;
26
+ }
27
+
28
+ export interface BeaconSetOptions extends BeaconLabelState {
29
+ workspaceId: string;
30
+ }
31
+
32
+ export interface BeaconBlinkOptions {
33
+ workspaceId?: string;
34
+ a: BeaconLabelState;
35
+ b: BeaconLabelState;
36
+ intervalMs?: number;
37
+ rounds?: number;
38
+ restoreOnDone?: boolean;
39
+ }
40
+
41
+ export interface BeaconClearOptions {
42
+ workspaceId?: string;
43
+ name?: string;
44
+ restoreTitle?: boolean;
45
+ }
46
+
47
+ export interface WorkspaceTitleHandle {
48
+ setTitle?: (title: string) => unknown;
49
+ }
50
+
51
+ export interface BeaconDaemonClient {
52
+ setWorkspaceLabel?: (args: unknown) => unknown;
53
+ updateWorkspaceLabel?: (args: unknown) => unknown;
54
+ removeWorkspaceLabel?: (args: unknown) => unknown;
55
+ [key: string]: unknown;
56
+ }
57
+
58
+ export interface WorkspaceBeaconOptions {
59
+ workspaceHandle?: WorkspaceTitleHandle | null;
60
+ daemonClient?: BeaconDaemonClient | null;
61
+ labelPrefix?: string;
62
+ baseTitle?: string;
63
+ logger?: PluginLogger;
64
+ }
65
+
66
+ export interface BeaconSetResult {
67
+ labelApplied: boolean;
68
+ titleApplied: boolean;
69
+ labelName?: string;
70
+ title?: string;
71
+ }
72
+
73
+ export interface BeaconClearResult {
74
+ labelCleared: boolean;
75
+ titleRestored: boolean;
76
+ }
77
+
78
+ export interface BeaconBlinkHandle {
79
+ stop: () => void;
80
+ done: Promise<void>;
81
+ }
82
+
83
+ export function resolveBeaconLabelName(
84
+ name: string,
85
+ prefix: string = DEFAULT_BEACON_LABEL_PREFIX,
86
+ ): string {
87
+ const trimmed = (name ?? "").trim();
88
+ if (!prefix) return trimmed;
89
+ if (trimmed.toLowerCase().startsWith(prefix.toLowerCase())) return trimmed;
90
+ return `${prefix}${trimmed}`;
91
+ }
92
+
93
+ export function normalizeBeaconColor(color: string | undefined): string | undefined {
94
+ if (!color) return undefined;
95
+ const normalized = color.trim().toLowerCase();
96
+ if ((BEACON_COLORS as readonly string[]).includes(normalized)) return normalized;
97
+ return undefined;
98
+ }
99
+
100
+ function isFunction(value: unknown): value is (...args: never[]) => unknown {
101
+ return typeof value === "function";
102
+ }
103
+
104
+ async function safeInvoke(
105
+ fn: () => unknown,
106
+ logger?: PluginLogger,
107
+ ): Promise<boolean> {
108
+ try {
109
+ await fn();
110
+ return true;
111
+ } catch (err) {
112
+ logger?.debug?.(
113
+ `WorkspaceBeacon host call failed: ${err instanceof Error ? err.message : String(err)}`,
114
+ );
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function toTimerKey(workspaceId: string | undefined): string {
120
+ return workspaceId ?? "__default__";
121
+ }
122
+
123
+ export class WorkspaceBeacon {
124
+ private workspaceHandle?: WorkspaceTitleHandle | null;
125
+ private daemonClient?: BeaconDaemonClient | null;
126
+ private labelPrefix: string;
127
+ private baseTitle?: string;
128
+ private logger?: PluginLogger;
129
+ private originalTitles = new Map<string, string>();
130
+ private blinkTimers = new Map<string, { timer: ReturnType<typeof setInterval>; finish: () => void }>();
131
+
132
+ constructor(options: WorkspaceBeaconOptions = {}) {
133
+ this.workspaceHandle = options.workspaceHandle ?? null;
134
+ this.daemonClient = options.daemonClient ?? null;
135
+ this.labelPrefix = options.labelPrefix ?? DEFAULT_BEACON_LABEL_PREFIX;
136
+ this.baseTitle = options.baseTitle;
137
+ this.logger = options.logger;
138
+ }
139
+
140
+ setOptions(options: Partial<WorkspaceBeaconOptions>): void {
141
+ if ("workspaceHandle" in options) this.workspaceHandle = options.workspaceHandle ?? null;
142
+ if ("daemonClient" in options) this.daemonClient = options.daemonClient ?? null;
143
+ if (options.labelPrefix !== undefined) this.labelPrefix = options.labelPrefix;
144
+ if (options.baseTitle !== undefined) this.baseTitle = options.baseTitle;
145
+ if (options.logger !== undefined) this.logger = options.logger;
146
+ }
147
+
148
+ get activeBlinks(): number {
149
+ return this.blinkTimers.size;
150
+ }
151
+
152
+ async set(options: BeaconSetOptions): Promise<BeaconSetResult> {
153
+ const result: BeaconSetResult = { labelApplied: false, titleApplied: false };
154
+ try {
155
+ const workspaceId = options.workspaceId;
156
+ const labelName = options.name ? resolveBeaconLabelName(options.name, this.labelPrefix) : undefined;
157
+ result.labelName = labelName;
158
+
159
+ if (workspaceId && labelName) {
160
+ result.labelApplied = await this.applyLabel(workspaceId, labelName, options.color);
161
+ }
162
+
163
+ const title = this.resolveTitle(options);
164
+ if (title !== undefined) {
165
+ result.titleApplied = await this.applyTitle(workspaceId, title);
166
+ if (result.titleApplied) result.title = title;
167
+ }
168
+ } catch (err) {
169
+ this.logger?.debug?.(
170
+ `WorkspaceBeacon.set failed: ${err instanceof Error ? err.message : String(err)}`,
171
+ );
172
+ }
173
+ return result;
174
+ }
175
+
176
+ blink(options: BeaconBlinkOptions): BeaconBlinkHandle {
177
+ const intervalMs = Math.max(50, options.intervalMs ?? 3000);
178
+ const totalRounds = options.rounds ?? Number.POSITIVE_INFINITY;
179
+ const workspaceId = options.workspaceId ?? options.a.workspaceId ?? options.b.workspaceId;
180
+ const key = toTimerKey(workspaceId);
181
+ this.stopBlink(key);
182
+ let stopped = false;
183
+ let settled = false;
184
+ let timer: ReturnType<typeof setInterval> | undefined;
185
+ let resolveDone!: () => void;
186
+ const done = new Promise<void>((resolve) => {
187
+ resolveDone = resolve;
188
+ });
189
+ const finish = () => {
190
+ if (settled) return;
191
+ settled = true;
192
+ if (timer) clearInterval(timer);
193
+ const current = this.blinkTimers.get(key);
194
+ if (current?.finish === finish) this.blinkTimers.delete(key);
195
+ if (options.restoreOnDone) {
196
+ void this.clear({
197
+ workspaceId,
198
+ name: options.a.name ?? options.b.name,
199
+ restoreTitle: true,
200
+ }).catch((err) =>
201
+ this.logger?.debug?.(
202
+ `WorkspaceBeacon blink restore failed: ${err instanceof Error ? err.message : String(err)}`,
203
+ ),
204
+ );
205
+ }
206
+ resolveDone();
207
+ };
208
+
209
+ try {
210
+ const states: BeaconLabelState[] = [options.a, options.b];
211
+ const tick = () => {
212
+ if (stopped) {
213
+ finish();
214
+ return;
215
+ }
216
+ if (count >= totalRounds) {
217
+ finish();
218
+ return;
219
+ }
220
+ const state = states[count % 2];
221
+ count += 1;
222
+ void this.set({ ...state, workspaceId: state.workspaceId ?? workspaceId ?? "" }).catch((err) =>
223
+ this.logger?.debug?.(
224
+ `WorkspaceBeacon blink tick failed: ${err instanceof Error ? err.message : String(err)}`,
225
+ ),
226
+ );
227
+ if (count >= totalRounds) {
228
+ finish();
229
+ }
230
+ };
231
+ let count = 0;
232
+ void this.set({ ...states[0], workspaceId: states[0].workspaceId ?? workspaceId ?? "" }).catch(
233
+ (err) =>
234
+ this.logger?.debug?.(
235
+ `WorkspaceBeacon blink tick failed: ${err instanceof Error ? err.message : String(err)}`,
236
+ ),
237
+ );
238
+ count = 1;
239
+ if (count >= totalRounds) {
240
+ finish();
241
+ } else {
242
+ timer = setInterval(tick, intervalMs);
243
+ this.blinkTimers.set(key, { timer, finish });
244
+ }
245
+ } catch (err) {
246
+ this.logger?.debug?.(
247
+ `WorkspaceBeacon blink setup failed: ${err instanceof Error ? err.message : String(err)}`,
248
+ );
249
+ finish();
250
+ }
251
+
252
+ return {
253
+ stop: () => {
254
+ stopped = true;
255
+ finish();
256
+ },
257
+ done,
258
+ };
259
+ }
260
+
261
+ async clear(options: BeaconClearOptions = {}): Promise<BeaconClearResult> {
262
+ const result: BeaconClearResult = { labelCleared: false, titleRestored: false };
263
+ try {
264
+ this.stopBlink(toTimerKey(options.workspaceId));
265
+ if (options.workspaceId && options.name) {
266
+ result.labelCleared = await this.detachLabel(
267
+ options.workspaceId,
268
+ resolveBeaconLabelName(options.name, this.labelPrefix),
269
+ );
270
+ } else if (options.workspaceId && !options.name) {
271
+ result.labelCleared = false;
272
+ }
273
+ if (options.restoreTitle !== false) {
274
+ result.titleRestored = await this.restoreTitle(options.workspaceId);
275
+ }
276
+ } catch (err) {
277
+ this.logger?.debug?.(
278
+ `WorkspaceBeacon.clear failed: ${err instanceof Error ? err.message : String(err)}`,
279
+ );
280
+ }
281
+ return result;
282
+ }
283
+
284
+ stopAll(): void {
285
+ for (const entry of this.blinkTimers.values()) {
286
+ clearInterval(entry.timer);
287
+ try {
288
+ entry.finish();
289
+ } catch (err) {
290
+ this.logger?.debug?.(
291
+ `WorkspaceBeacon stopAll finish failed: ${err instanceof Error ? err.message : String(err)}`,
292
+ );
293
+ }
294
+ }
295
+ this.blinkTimers.clear();
296
+ }
297
+
298
+ private resolveTitle(options: BeaconLabelState): string | undefined {
299
+ if (options.title !== undefined) return options.title;
300
+ if (options.titleSuffix === undefined) return undefined;
301
+ const base = this.originalTitles.get(options.workspaceId ?? "") ?? this.baseTitle ?? "";
302
+ return `${base}${options.titleSuffix}`;
303
+ }
304
+
305
+ private async applyLabel(workspaceId: string, labelName: string, color?: string): Promise<boolean> {
306
+ const client = this.daemonClient;
307
+ if (!client || !isFunction(client.setWorkspaceLabel)) return false;
308
+ const label: Record<string, string> = { name: labelName };
309
+ const normalized = normalizeBeaconColor(color);
310
+ if (normalized) label.color = normalized;
311
+ else if (color) label.color = color;
312
+ return safeInvoke(
313
+ () =>
314
+ (client.setWorkspaceLabel as (args: unknown) => unknown)({
315
+ workspaceId,
316
+ label,
317
+ assigned: true,
318
+ }),
319
+ this.logger,
320
+ );
321
+ }
322
+
323
+ private async detachLabel(workspaceId: string, labelName: string): Promise<boolean> {
324
+ const client = this.daemonClient as BeaconDaemonClient | null | undefined;
325
+ if (!client) return false;
326
+ try {
327
+ if (isFunction(client.setWorkspaceLabel)) {
328
+ return await safeInvoke(
329
+ () =>
330
+ (client.setWorkspaceLabel as (args: unknown) => unknown)({
331
+ workspaceId,
332
+ label: { name: labelName },
333
+ assigned: false,
334
+ }),
335
+ this.logger,
336
+ );
337
+ }
338
+ if (isFunction(client.removeWorkspaceLabel)) {
339
+ return await safeInvoke(
340
+ () =>
341
+ (client.removeWorkspaceLabel as (args: unknown) => unknown)({
342
+ workspaceId,
343
+ name: labelName,
344
+ }),
345
+ this.logger,
346
+ );
347
+ }
348
+ return false;
349
+ } catch (err) {
350
+ this.logger?.debug?.(
351
+ `WorkspaceBeacon detachLabel failed: ${err instanceof Error ? err.message : String(err)}`,
352
+ );
353
+ return false;
354
+ }
355
+ }
356
+
357
+ setOriginalTitle(workspaceId: string, title: string): void {
358
+ this.originalTitles.set(workspaceId, title);
359
+ }
360
+
361
+ hasOriginalTitle(workspaceId: string): boolean {
362
+ return this.originalTitles.has(workspaceId);
363
+ }
364
+
365
+ getOriginalTitle(workspaceId: string): string | undefined {
366
+ return this.originalTitles.get(workspaceId);
367
+ }
368
+
369
+ private async applyTitle(workspaceId: string | undefined, title: string): Promise<boolean> {
370
+ const handle = this.workspaceHandle;
371
+ if (!handle || !isFunction(handle.setTitle)) return false;
372
+ const key = workspaceId ?? "";
373
+ if (!this.originalTitles.has(key)) {
374
+ if (this.baseTitle !== undefined) {
375
+ this.originalTitles.set(key, this.baseTitle);
376
+ }
377
+ }
378
+ return safeInvoke(() => (handle.setTitle as (t: string) => unknown)(title), this.logger);
379
+ }
380
+
381
+ private async restoreTitle(workspaceId: string | undefined): Promise<boolean> {
382
+ const handle = this.workspaceHandle;
383
+ if (!handle || !isFunction(handle.setTitle)) return false;
384
+ const key = workspaceId ?? "";
385
+ const original = this.originalTitles.get(key) ?? this.baseTitle;
386
+ if (original === undefined) return false;
387
+ const ok = await safeInvoke(
388
+ () => (handle.setTitle as (t: string) => unknown)(original),
389
+ this.logger,
390
+ );
391
+ if (ok) {
392
+ this.originalTitles.delete(key);
393
+ if (this.baseTitle === original) {
394
+ this.baseTitle = undefined;
395
+ }
396
+ }
397
+ return ok;
398
+ }
399
+
400
+ private stopBlink(key: string): void {
401
+ const entry = this.blinkTimers.get(key);
402
+ if (entry) {
403
+ clearInterval(entry.timer);
404
+ this.blinkTimers.delete(key);
405
+ try {
406
+ entry.finish();
407
+ } catch (err) {
408
+ this.logger?.debug?.(
409
+ `WorkspaceBeacon stopBlink finish failed: ${err instanceof Error ? err.message : String(err)}`,
410
+ );
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ export function createWorkspaceBeacon(options: WorkspaceBeaconOptions = {}): WorkspaceBeacon {
417
+ return new WorkspaceBeacon(options);
418
+ }
package/shared/mcp.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { z } from "zod";
2
+ import { defineContract, defineSettingsContract } from "paseo-plugin-helper/shared";
3
+
4
+ export const McpSourceSchema = z.object({
5
+ kind: z.enum(["project", "repo", "personal", "global", "paseo", "session"]),
6
+ label: z.string(),
7
+ path: z.string(),
8
+ });
9
+
10
+ export const McpServerSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ transport: z.enum(["stdio", "http", "sse", "unknown"]),
14
+ source: McpSourceSchema,
15
+ command: z.string().nullable(),
16
+ args: z.array(z.string()).nullable().optional(),
17
+ url: z.string().nullable(),
18
+ description: z.string(),
19
+ hasSecrets: z.boolean(),
20
+ configPreview: z.string(),
21
+ });
22
+
23
+ export type McpServer = z.infer<typeof McpServerSchema>;
24
+
25
+ export const PaseoToolSchema = z.object({
26
+ name: z.string(),
27
+ description: z.string(),
28
+ category: z.string(),
29
+ });
30
+
31
+ export const listMcp = defineContract({
32
+ name: "mcp.list",
33
+ description: "Lists live MCP servers for this agent session",
34
+ input: z.object({ agentId: z.string() }),
35
+ output: z.object({
36
+ provider: z.string(),
37
+ cwd: z.string().nullable(),
38
+ servers: z.array(McpServerSchema),
39
+ paseoTools: z.array(PaseoToolSchema),
40
+ error: z.string().nullable(),
41
+ }),
42
+ });
43
+
44
+ export const ToolPropertySchema = z.object({
45
+ type: z.union([z.string(), z.array(z.string())]).optional(),
46
+ description: z.string().optional(),
47
+ default: z.any().optional(),
48
+ enum: z.array(z.any()).optional(),
49
+ items: z.any().optional(),
50
+ }).passthrough();
51
+
52
+ export const ToolInfoSchema = z.object({
53
+ name: z.string(),
54
+ description: z.string().optional(),
55
+ inputSchema: z.object({
56
+ type: z.string().optional(),
57
+ properties: z.record(z.string(), ToolPropertySchema).optional(),
58
+ required: z.array(z.string()).optional(),
59
+ }).passthrough().optional(),
60
+ });
61
+
62
+ export type ToolInfo = z.infer<typeof ToolInfoSchema>;
63
+
64
+ export const HealthResultSchema = z.object({
65
+ serverId: z.string(),
66
+ name: z.string(),
67
+ status: z.enum(["healthy", "degraded", "down", "unknown"]),
68
+ latencyMs: z.number(),
69
+ toolCount: z.number().nullable(),
70
+ tools: z.array(z.string()).nullable(),
71
+ toolDetails: z.array(ToolInfoSchema).nullable().optional(),
72
+ instructions: z.string().nullable(),
73
+ error: z.string().nullable(),
74
+ checkedAt: z.string(),
75
+ });
76
+
77
+ export const checkMcpHealth = defineContract({
78
+ name: "mcp.health",
79
+ description: "Health-checks MCP servers via initialize + tools/list",
80
+ input: z.object({ agentId: z.string(), serverId: z.string().optional() }),
81
+ output: z.object({
82
+ results: z.array(HealthResultSchema),
83
+ error: z.string().nullable(),
84
+ }),
85
+ });
86
+
87
+ // Compact health snapshot persisted to PluginStorage after each health
88
+ // check so other plugins can read fleet status without re-probing.
89
+ export interface McpStatusSnapshot {
90
+ updatedAt: string; // ISO timestamp
91
+ total: number;
92
+ healthy: number;
93
+ degraded: number;
94
+ down: number;
95
+ servers: Array<{
96
+ name: string;
97
+ status: "healthy" | "degraded" | "down" | "unknown";
98
+ latencyMs: number;
99
+ }>;
100
+ }
101
+
102
+ export const callMcpTool = defineContract({
103
+ name: "mcp.call_tool",
104
+ description: "Executes a tool on an MCP server",
105
+ input: z.object({
106
+ agentId: z.string(),
107
+ serverId: z.string(),
108
+ toolName: z.string(),
109
+ arguments: z.record(z.string(), z.any()),
110
+ }),
111
+ output: z.object({
112
+ content: z.array(z.any()),
113
+ isError: z.boolean().optional(),
114
+ error: z.string().nullable().optional(),
115
+ }),
116
+ });
117
+
118
+ export const readMcp = defineContract({
119
+ name: "mcp.read",
120
+ description: "Reads redacted config detail for an MCP server",
121
+ input: z.object({ agentId: z.string(), serverId: z.string() }),
122
+ output: z.object({
123
+ name: z.string(),
124
+ transport: z.string(),
125
+ source: McpSourceSchema,
126
+ path: z.string(),
127
+ raw: z.string(),
128
+ redacted: z.string(),
129
+ }),
130
+ });
131
+
132
+ export const DiagnosticStepSchema = z.object({
133
+ target: z.string(),
134
+ status: z.enum(["found", "missing", "error", "skipped"]),
135
+ details: z.string(),
136
+ contentPreview: z.string().nullable(),
137
+ });
138
+
139
+ export type DiagnosticStep = z.infer<typeof DiagnosticStepSchema>;
140
+
141
+ export const diagnoseMcp = defineContract({
142
+ name: "mcp.diagnose",
143
+ description: "Runs provider probe diagnostics for an agent",
144
+ input: z.object({ agentId: z.string() }),
145
+ output: z.object({
146
+ report: z.string(),
147
+ version: z.string(),
148
+ provider: z.string(),
149
+ cwd: z.string(),
150
+ probeId: z.string().nullable(),
151
+ probeLabel: z.string().nullable(),
152
+ steps: z.array(DiagnosticStepSchema),
153
+ discoveredServerCount: z.number(),
154
+ error: z.string().nullable(),
155
+ }),
156
+ });
157
+
158
+ export const McpToolsSettingsSchema = z.object({
159
+ healthPollingRate: z.enum(["1s", "2s", "5s", "10s", "15s", "30s", "60s", "5m", "paused"]).default("30s"),
160
+ healthDigestRows: z.boolean().default(false),
161
+ gatewayInject: z.boolean().default(true),
162
+ gatewayUrl: z.string().default("http://127.0.0.1:37374/mcp"),
163
+ flairRadius: z.enum(["sharp", "rounded", "pill"]).default("rounded"),
164
+ flairDensity: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"),
165
+ flairSurface: z.enum(["flat", "tinted", "elevated"]).default("flat"),
166
+ flairAccentColor: z.string().default("#6366f1"),
167
+ });
168
+
169
+ export type McpToolsSettings = z.infer<typeof McpToolsSettingsSchema>;
170
+
171
+ export const mcpToolsSettingsContract = defineSettingsContract({
172
+ name: "mcp-tools.settings",
173
+ schema: McpToolsSettingsSchema,
174
+ description: "mcp-tools cache, polling and visual flair settings",
175
+ });
176
+
177
+ // Short source label rendered on everything this plugin puts into
178
+ // shared Paseo surfaces, so plugin output is never mistaken for core.
179
+ export const PLUGIN_ATTRIBUTION = "via mcp-tools";
180
+
181
+ // Splits a gateway multiplexed tool name ("forgejo__list_branches")
182
+ // into its upstream server and tool. Returns null for bare names.
183
+ export function splitNamespacedTool(name: string): { server: string; tool: string } | null {
184
+ const sep = name.indexOf("__");
185
+ if (sep <= 0 || sep === name.length - 2) return null;
186
+ return { server: name.slice(0, sep), tool: name.slice(sep + 2) };
187
+ }
188
+
189
+ export interface McpHealthDigestData {
190
+ [key: string]: number | string;
191
+ healthy: number;
192
+ degraded: number;
193
+ down: number;
194
+ total: number;
195
+ updatedAt: string;
196
+ }
197
+
198
+ // Builds the daemon health digest payload shared by the timeline row
199
+ // and the slash command verdict. Stable id lets replays replace it.
200
+ export function buildHealthDigest(snapshot: {
201
+ healthy: number;
202
+ degraded: number;
203
+ down: number;
204
+ total: number;
205
+ }): McpHealthDigestData {
206
+ return { ...snapshot, updatedAt: new Date().toISOString() };
207
+ }
@@ -0,0 +1,11 @@
1
+ # Vendored paseo-plugin-helper (Track B of issue #71, plugin #100)
2
+
3
+ Pinned helper version: 0.4.0-beta.12
4
+
5
+ Docs: https://github.com/xpufx/paseo/tree/main/packages/paseo-plugin-helper — npm: https://www.npmjs.com/package/paseo-plugin-helper
6
+
7
+ Zero host requirements (no npm, no registry, no build key): the daemon bundles
8
+ this source directly. Layout: `client/`, `server/`, `shared/` (+ `mcp/` under
9
+ server) vendor splits mapping helper `src/` trees (daemon accepts modules only
10
+ under those dirs). Updating: run `make vendor-sync` and bump the pinned version
11
+ above.
@@ -0,0 +1,35 @@
1
+ export class TimeoutError extends Error {
2
+ readonly timeoutMs: number;
3
+
4
+ constructor(message: string, timeoutMs: number) {
5
+ super(message);
6
+ this.name = "TimeoutError";
7
+ this.timeoutMs = timeoutMs;
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Races a promise against a timeout duration in milliseconds.
13
+ * If the timeout expires before the promise resolves, rejects with a TimeoutError.
14
+ * Automatically cleans up the timer on resolution or rejection.
15
+ */
16
+ export async function withTimeout<T>(
17
+ promise: Promise<T>,
18
+ timeoutMs: number,
19
+ label = "Operation"
20
+ ): Promise<T> {
21
+ let timer: any;
22
+ const timeoutPromise = new Promise<never>((_, reject) => {
23
+ timer = setTimeout(() => {
24
+ reject(new TimeoutError(`${label} timed out after ${timeoutMs}ms`, timeoutMs));
25
+ }, timeoutMs);
26
+ });
27
+
28
+ try {
29
+ return await Promise.race([promise, timeoutPromise]);
30
+ } finally {
31
+ if (timer !== undefined) {
32
+ clearTimeout(timer);
33
+ }
34
+ }
35
+ }