@xpufx/paseo-twofado 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/client/approvals.tsx +1525 -0
- package/client/error-boundary.tsx +28 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/package.json +36 -0
- package/paseo-plugin.json +6 -0
- package/server/twofado.ts +536 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/approval.ts +254 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -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
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineContract, defineSettingsContract } from "paseo-plugin-helper/shared";
|
|
3
|
+
|
|
4
|
+
export const notificationTargets = ["telegram", "paseo", "both"] as const;
|
|
5
|
+
export type NotificationTarget = (typeof notificationTargets)[number];
|
|
6
|
+
|
|
7
|
+
export const pendingList = defineContract({
|
|
8
|
+
name: "approval.list",
|
|
9
|
+
input: z.object({ socketPath: z.string().min(1).optional() }),
|
|
10
|
+
output: z.object({
|
|
11
|
+
items: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
argv: z.array(z.string()),
|
|
15
|
+
host: z.string(),
|
|
16
|
+
caller: z.string(),
|
|
17
|
+
cwd: z.string(),
|
|
18
|
+
expiresIn: z.number(),
|
|
19
|
+
step: z.enum(["initial", "confirm"]).default("initial"),
|
|
20
|
+
confirmOf: z.string().optional(),
|
|
21
|
+
kind: z.string().optional(),
|
|
22
|
+
link: z.string().optional(),
|
|
23
|
+
summary: z.string().optional(),
|
|
24
|
+
acked: z.boolean().optional(),
|
|
25
|
+
ackBy: z.string().optional(),
|
|
26
|
+
authUrl: z.string().optional(),
|
|
27
|
+
preview: z
|
|
28
|
+
.object({
|
|
29
|
+
resolvedBinary: z.string().optional(),
|
|
30
|
+
targetCwd: z.string().optional(),
|
|
31
|
+
affectedCount: z.number().optional(),
|
|
32
|
+
samplePaths: z.array(z.string()).optional(),
|
|
33
|
+
riskLevel: z.enum(["low", "medium", "high", "critical"]).optional(),
|
|
34
|
+
riskReason: z.string().optional(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
}),
|
|
40
|
+
description: "List unexpired, undecided 2fado requests",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const verdict = defineContract({
|
|
44
|
+
name: "approval.verdict",
|
|
45
|
+
input: z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
decision: z.enum(["approve", "deny"]),
|
|
48
|
+
socketPath: z.string().min(1).optional(),
|
|
49
|
+
}),
|
|
50
|
+
output: z.object({ recorded: z.boolean() }),
|
|
51
|
+
description: "Record an approve/deny verdict for a 2fado request",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const approvalAck = defineContract({
|
|
55
|
+
name: "approval.ack",
|
|
56
|
+
input: z.object({
|
|
57
|
+
id: z.string().min(1),
|
|
58
|
+
socketPath: z.string().min(1).optional(),
|
|
59
|
+
}),
|
|
60
|
+
output: z.object({ acked: z.boolean() }),
|
|
61
|
+
description: "Acknowledge a notify-only 2fado petition (non-binding visibility signal)",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const recentList = defineContract({
|
|
65
|
+
name: "approval.recent",
|
|
66
|
+
input: z.object({
|
|
67
|
+
socketPath: z.string().min(1).optional(),
|
|
68
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
69
|
+
}),
|
|
70
|
+
output: z.object({
|
|
71
|
+
items: z.array(
|
|
72
|
+
z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
argv: z.array(z.string()),
|
|
75
|
+
cwd: z.string(),
|
|
76
|
+
decision: z.string(),
|
|
77
|
+
by: z.string(),
|
|
78
|
+
exit: z.number(),
|
|
79
|
+
output: z.string(),
|
|
80
|
+
step: z.enum(["initial", "confirm"]).optional(),
|
|
81
|
+
confirmOf: z.string().optional(),
|
|
82
|
+
kind: z.string().optional(),
|
|
83
|
+
link: z.string().optional(),
|
|
84
|
+
summary: z.string().optional(),
|
|
85
|
+
acked: z.boolean().optional(),
|
|
86
|
+
ackBy: z.string().optional(),
|
|
87
|
+
authUrl: z.string().optional(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
}),
|
|
91
|
+
description: "List recently decided 2fado requests with execution results",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const approvalStatus = defineContract({
|
|
95
|
+
name: "approval.status",
|
|
96
|
+
input: z.object({
|
|
97
|
+
id: z.string().min(1),
|
|
98
|
+
socketPath: z.string().min(1).optional(),
|
|
99
|
+
}),
|
|
100
|
+
output: z.object({
|
|
101
|
+
id: z.string(),
|
|
102
|
+
status: z.enum([
|
|
103
|
+
"not_found",
|
|
104
|
+
"pending",
|
|
105
|
+
"confirming",
|
|
106
|
+
"running",
|
|
107
|
+
"completed",
|
|
108
|
+
"denied",
|
|
109
|
+
"timeout",
|
|
110
|
+
"client_aborted",
|
|
111
|
+
"confirmation_timeout",
|
|
112
|
+
"acked",
|
|
113
|
+
]),
|
|
114
|
+
argv: z.array(z.string()).optional(),
|
|
115
|
+
cwd: z.string().optional(),
|
|
116
|
+
expiresIn: z.number().optional(),
|
|
117
|
+
decision: z.string().optional(),
|
|
118
|
+
by: z.string().optional(),
|
|
119
|
+
exit: z.number(),
|
|
120
|
+
output: z.string().optional(),
|
|
121
|
+
step: z.string().optional(),
|
|
122
|
+
confirmOf: z.string().optional(),
|
|
123
|
+
kind: z.string().optional(),
|
|
124
|
+
link: z.string().optional(),
|
|
125
|
+
summary: z.string().optional(),
|
|
126
|
+
acked: z.boolean().optional(),
|
|
127
|
+
ackBy: z.string().optional(),
|
|
128
|
+
ackAt: z.number().optional(),
|
|
129
|
+
authUrl: z.string().optional(),
|
|
130
|
+
}),
|
|
131
|
+
description: "Query status and execution outcome of a 2fado request by ID",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const approvalTelegramInfo = defineContract({
|
|
135
|
+
name: "approval.telegram_info",
|
|
136
|
+
input: z.object({
|
|
137
|
+
socketPath: z.string().min(1).optional(),
|
|
138
|
+
}),
|
|
139
|
+
output: z.object({
|
|
140
|
+
configured: z.boolean(),
|
|
141
|
+
botUsername: z.string().optional(),
|
|
142
|
+
chatId: z.string().optional(),
|
|
143
|
+
approvers: z.array(z.string()).default([]),
|
|
144
|
+
status: z.enum(["connected", "disconnected", "unconfigured", "error"]).default("unconfigured"),
|
|
145
|
+
error: z.string().optional(),
|
|
146
|
+
notificationTarget: z.enum(notificationTargets).default("both"),
|
|
147
|
+
}),
|
|
148
|
+
description: "Query Telegram bot connectivity status and recipient metadata",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const approvalTelegramSetConfig = defineContract({
|
|
152
|
+
name: "approval.telegram_set_config",
|
|
153
|
+
input: z.object({
|
|
154
|
+
botToken: z.string().optional(),
|
|
155
|
+
chatId: z.string().optional(),
|
|
156
|
+
approvers: z.array(z.string()).optional(),
|
|
157
|
+
notificationTarget: z.enum(notificationTargets).optional(),
|
|
158
|
+
socketPath: z.string().min(1).optional(),
|
|
159
|
+
}),
|
|
160
|
+
output: z.object({
|
|
161
|
+
success: z.boolean(),
|
|
162
|
+
botUsername: z.string().optional(),
|
|
163
|
+
error: z.string().optional(),
|
|
164
|
+
}),
|
|
165
|
+
description: "Send updated Telegram recipient credentials to 2fadod",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const daemonHealth = defineContract({
|
|
169
|
+
name: "approval.health",
|
|
170
|
+
input: z.object({ socketPath: z.string().min(1).optional() }),
|
|
171
|
+
output: z.object({
|
|
172
|
+
reachable: z.boolean(),
|
|
173
|
+
version: z.string().optional(),
|
|
174
|
+
pid: z.number().optional(),
|
|
175
|
+
}),
|
|
176
|
+
description: "Probe 2fadod reachability via version handshake",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export interface PolicyAddRuleParams {
|
|
180
|
+
target: "whitelist" | "blacklist";
|
|
181
|
+
match_type: "exact" | "base" | "custom";
|
|
182
|
+
pattern: string[];
|
|
183
|
+
socketPath?: string;
|
|
184
|
+
}
|
|
185
|
+
export interface PolicyAddRuleResult {
|
|
186
|
+
success: boolean;
|
|
187
|
+
error?: string;
|
|
188
|
+
rules_count?: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const policyAddRule = defineContract({
|
|
192
|
+
name: "approval.policy_add_rule",
|
|
193
|
+
input: z.object({
|
|
194
|
+
target: z.enum(["whitelist", "blacklist"]),
|
|
195
|
+
match_type: z.enum(["exact", "base", "custom"]),
|
|
196
|
+
pattern: z.array(z.string()),
|
|
197
|
+
socketPath: z.string().min(1).optional(),
|
|
198
|
+
}),
|
|
199
|
+
output: z.object({
|
|
200
|
+
success: z.boolean(),
|
|
201
|
+
error: z.string().optional(),
|
|
202
|
+
rules_count: z.number().optional(),
|
|
203
|
+
}),
|
|
204
|
+
description: "Persist a policy rule derived from an approval item",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export type ApprovalApi = {
|
|
208
|
+
policyAddRule(params: PolicyAddRuleParams): Promise<PolicyAddRuleResult>;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export function migrateLegacyNotificationTarget(input: unknown): {
|
|
212
|
+
data: Record<string, unknown>;
|
|
213
|
+
changed: boolean;
|
|
214
|
+
} {
|
|
215
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
216
|
+
return { data: {}, changed: false };
|
|
217
|
+
}
|
|
218
|
+
const record = input as Record<string, unknown>;
|
|
219
|
+
if (!("telegramFallback" in record)) {
|
|
220
|
+
return { data: record, changed: false };
|
|
221
|
+
}
|
|
222
|
+
const { telegramFallback, notificationTarget, ...rest } = record;
|
|
223
|
+
if (notificationTarget !== undefined) {
|
|
224
|
+
return { data: { ...rest, notificationTarget }, changed: true };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
data: { ...rest, notificationTarget: telegramFallback === false ? "paseo" : "both" },
|
|
228
|
+
changed: true,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const settingsSchema = z.object({
|
|
233
|
+
socketPath: z
|
|
234
|
+
.string()
|
|
235
|
+
.trim()
|
|
236
|
+
.min(1, "Enter the 2fadod socket path")
|
|
237
|
+
.default("/tmp/2fado.sock")
|
|
238
|
+
.describe("2fadod socket"),
|
|
239
|
+
notificationTarget: z
|
|
240
|
+
.enum(notificationTargets)
|
|
241
|
+
.default("both")
|
|
242
|
+
.describe("Notification target"),
|
|
243
|
+
telegramBotToken: z.string().default("").describe("Telegram bot token"),
|
|
244
|
+
telegramChatId: z.string().default("").describe("Telegram chat ID"),
|
|
245
|
+
telegramApprovers: z.string().default("").describe("Telegram approvers"),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export type ApprovalSettingsValues = z.output<typeof settingsSchema>;
|
|
249
|
+
|
|
250
|
+
export const approvalSettings = defineSettingsContract<ApprovalSettingsValues>({
|
|
251
|
+
name: "twofado.settings",
|
|
252
|
+
schema: settingsSchema,
|
|
253
|
+
description: "2fado approval settings",
|
|
254
|
+
});
|
|
@@ -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
|
+
}
|