@symerian/symi 3.0.18 → 3.0.19
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/extensions/copilot-proxy/README.md +0 -24
- package/extensions/copilot-proxy/index.ts +0 -154
- package/extensions/copilot-proxy/node_modules/.bin/symi +0 -21
- package/extensions/copilot-proxy/package.json +0 -15
- package/extensions/copilot-proxy/symi.plugin.json +0 -9
- package/extensions/device-pair/index.ts +0 -642
- package/extensions/device-pair/symi.plugin.json +0 -20
- package/extensions/diagnostics-otel/index.ts +0 -15
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
- package/extensions/diagnostics-otel/node_modules/.bin/symi +0 -21
- package/extensions/diagnostics-otel/package.json +0 -27
- package/extensions/diagnostics-otel/src/service.test.ts +0 -290
- package/extensions/diagnostics-otel/src/service.ts +0 -666
- package/extensions/diagnostics-otel/symi.plugin.json +0 -8
- package/extensions/google-antigravity-auth/README.md +0 -24
- package/extensions/google-antigravity-auth/index.ts +0 -424
- package/extensions/google-antigravity-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-antigravity-auth/package.json +0 -15
- package/extensions/google-antigravity-auth/symi.plugin.json +0 -9
- package/extensions/google-gemini-cli-auth/README.md +0 -35
- package/extensions/google-gemini-cli-auth/index.ts +0 -75
- package/extensions/google-gemini-cli-auth/node_modules/.bin/symi +0 -21
- package/extensions/google-gemini-cli-auth/oauth.test.ts +0 -162
- package/extensions/google-gemini-cli-auth/oauth.ts +0 -636
- package/extensions/google-gemini-cli-auth/package.json +0 -15
- package/extensions/google-gemini-cli-auth/symi.plugin.json +0 -9
- package/extensions/learning-loop/index.ts +0 -159
- package/extensions/learning-loop/node_modules/.bin/symi +0 -21
- package/extensions/learning-loop/package.json +0 -18
- package/extensions/learning-loop/src/analytics/gateway-methods.ts +0 -230
- package/extensions/learning-loop/src/analytics/metrics-aggregator.ts +0 -153
- package/extensions/learning-loop/src/capture/run-tracker.ts +0 -181
- package/extensions/learning-loop/src/capture/serializer.ts +0 -74
- package/extensions/learning-loop/src/db.ts +0 -583
- package/extensions/learning-loop/src/feedback/explicit-feedback.ts +0 -58
- package/extensions/learning-loop/src/feedback/implicit-signals.ts +0 -89
- package/extensions/learning-loop/src/graph/edge-inference.ts +0 -189
- package/extensions/learning-loop/src/graph/graph-retrieval.ts +0 -144
- package/extensions/learning-loop/src/graph/graph-store.ts +0 -183
- package/extensions/learning-loop/src/hooks.ts +0 -244
- package/extensions/learning-loop/src/injection/cache.ts +0 -73
- package/extensions/learning-loop/src/injection/context-injector.ts +0 -104
- package/extensions/learning-loop/src/injection/prompt-builder.ts +0 -43
- package/extensions/learning-loop/src/learning/embedding-bridge.ts +0 -54
- package/extensions/learning-loop/src/learning/learning-extractor.ts +0 -217
- package/extensions/learning-loop/src/learning/learning-store.ts +0 -158
- package/extensions/learning-loop/src/learning/retrieval.ts +0 -87
- package/extensions/learning-loop/src/math/confidence-intervals.ts +0 -62
- package/extensions/learning-loop/src/math/ewma.ts +0 -51
- package/extensions/learning-loop/src/math/weighted-scorer.ts +0 -42
- package/extensions/learning-loop/src/schema.ts +0 -176
- package/extensions/learning-loop/src/scoring/normalization.ts +0 -32
- package/extensions/learning-loop/src/scoring/quality-engine.ts +0 -78
- package/extensions/learning-loop/src/scoring/signal-extractors.ts +0 -155
- package/extensions/learning-loop/src/test/context-injector.test.ts +0 -142
- package/extensions/learning-loop/src/test/fixes.test.ts +0 -1286
- package/extensions/learning-loop/src/test/graph.test.ts +0 -711
- package/extensions/learning-loop/src/test/integration.test.ts +0 -312
- package/extensions/learning-loop/src/test/learning-store.test.ts +0 -191
- package/extensions/learning-loop/src/test/math.test.ts +0 -148
- package/extensions/learning-loop/src/test/quality-engine.test.ts +0 -231
- package/extensions/learning-loop/src/test/run-tracker.test.ts +0 -143
- package/extensions/learning-loop/src/types.ts +0 -281
- package/extensions/learning-loop/symi.plugin.json +0 -46
- package/extensions/llm-task/README.md +0 -97
- package/extensions/llm-task/index.ts +0 -6
- package/extensions/llm-task/package.json +0 -12
- package/extensions/llm-task/src/llm-task-tool.test.ts +0 -138
- package/extensions/llm-task/src/llm-task-tool.ts +0 -249
- package/extensions/llm-task/symi.plugin.json +0 -21
- package/extensions/memory-lancedb/config.ts +0 -161
- package/extensions/memory-lancedb/index.test.ts +0 -330
- package/extensions/memory-lancedb/index.ts +0 -670
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/symi +0 -21
- package/extensions/memory-lancedb/package.json +0 -20
- package/extensions/memory-lancedb/symi.plugin.json +0 -71
- package/extensions/minimax-portal-auth/README.md +0 -33
- package/extensions/minimax-portal-auth/index.ts +0 -161
- package/extensions/minimax-portal-auth/node_modules/.bin/symi +0 -21
- package/extensions/minimax-portal-auth/oauth.ts +0 -247
- package/extensions/minimax-portal-auth/package.json +0 -15
- package/extensions/minimax-portal-auth/symi.plugin.json +0 -9
- package/extensions/model-equalizer/index.ts +0 -80
- package/extensions/model-equalizer/skills/model-equalizer/SKILL.md +0 -58
- package/extensions/model-equalizer/src/detection.ts +0 -62
- package/extensions/model-equalizer/src/enhancer.ts +0 -63
- package/extensions/model-equalizer/src/test/detection.test.ts +0 -218
- package/extensions/model-equalizer/src/test/enhancer.test.ts +0 -137
- package/extensions/model-equalizer/src/test/integration.test.ts +0 -185
- package/extensions/model-equalizer/src/types.ts +0 -24
- package/extensions/model-equalizer/symi.plugin.json +0 -12
- package/extensions/phone-control/index.ts +0 -421
- package/extensions/phone-control/symi.plugin.json +0 -10
- package/extensions/pipeline/README.md +0 -75
- package/extensions/pipeline/SKILL.md +0 -97
- package/extensions/pipeline/index.ts +0 -18
- package/extensions/pipeline/package.json +0 -11
- package/extensions/pipeline/src/pipeline-tool.test.ts +0 -345
- package/extensions/pipeline/src/pipeline-tool.ts +0 -266
- package/extensions/pipeline/src/windows-spawn.test.ts +0 -148
- package/extensions/pipeline/src/windows-spawn.ts +0 -193
- package/extensions/pipeline/symi.plugin.json +0 -10
- package/extensions/qwen-portal-auth/README.md +0 -24
- package/extensions/qwen-portal-auth/index.ts +0 -134
- package/extensions/qwen-portal-auth/oauth.ts +0 -190
- package/extensions/qwen-portal-auth/symi.plugin.json +0 -9
- package/extensions/talk-voice/index.ts +0 -150
- package/extensions/talk-voice/symi.plugin.json +0 -10
- package/extensions/thread-ownership/index.test.ts +0 -180
- package/extensions/thread-ownership/index.ts +0 -133
- package/extensions/thread-ownership/symi.plugin.json +0 -28
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import qrcode from "qrcode-terminal";
|
|
4
|
-
import type { SymiPluginApi } from "symi/plugin-sdk";
|
|
5
|
-
import { approveDevicePairing, listDevicePairing } from "symi/plugin-sdk";
|
|
6
|
-
|
|
7
|
-
function renderQrAscii(data: string): Promise<string> {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
qrcode.generate(data, { small: true }, (output: string) => {
|
|
10
|
-
resolve(output);
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DEFAULT_GATEWAY_PORT = 18789;
|
|
16
|
-
|
|
17
|
-
type DevicePairPluginConfig = {
|
|
18
|
-
publicUrl?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type SetupPayload = {
|
|
22
|
-
url: string;
|
|
23
|
-
token?: string;
|
|
24
|
-
password?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type ResolveUrlResult = {
|
|
28
|
-
url?: string;
|
|
29
|
-
source?: string;
|
|
30
|
-
error?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type ResolveAuthResult = {
|
|
34
|
-
token?: string;
|
|
35
|
-
password?: string;
|
|
36
|
-
label?: string;
|
|
37
|
-
error?: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type CommandResult = {
|
|
41
|
-
code: number;
|
|
42
|
-
stdout: string;
|
|
43
|
-
stderr: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
async function runFixedCommandWithTimeout(
|
|
47
|
-
argv: string[],
|
|
48
|
-
timeoutMs: number,
|
|
49
|
-
): Promise<CommandResult> {
|
|
50
|
-
return await new Promise((resolve) => {
|
|
51
|
-
const [command, ...args] = argv;
|
|
52
|
-
if (!command) {
|
|
53
|
-
resolve({ code: 1, stdout: "", stderr: "command is required" });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const proc = spawn(command, args, {
|
|
57
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
-
env: { ...process.env },
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
let stdout = "";
|
|
62
|
-
let stderr = "";
|
|
63
|
-
let settled = false;
|
|
64
|
-
let timer: NodeJS.Timeout | null = null;
|
|
65
|
-
|
|
66
|
-
const finalize = (result: CommandResult) => {
|
|
67
|
-
if (settled) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
settled = true;
|
|
71
|
-
if (timer) {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
}
|
|
74
|
-
resolve(result);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
proc.stdout?.on("data", (chunk: Buffer | string) => {
|
|
78
|
-
stdout += chunk.toString();
|
|
79
|
-
});
|
|
80
|
-
proc.stderr?.on("data", (chunk: Buffer | string) => {
|
|
81
|
-
stderr += chunk.toString();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
timer = setTimeout(() => {
|
|
85
|
-
proc.kill("SIGKILL");
|
|
86
|
-
finalize({
|
|
87
|
-
code: 124,
|
|
88
|
-
stdout,
|
|
89
|
-
stderr: stderr || `command timed out after ${timeoutMs}ms`,
|
|
90
|
-
});
|
|
91
|
-
}, timeoutMs);
|
|
92
|
-
|
|
93
|
-
proc.on("error", (err) => {
|
|
94
|
-
finalize({
|
|
95
|
-
code: 1,
|
|
96
|
-
stdout,
|
|
97
|
-
stderr: err.message,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
proc.on("close", (code) => {
|
|
102
|
-
finalize({
|
|
103
|
-
code: code ?? 1,
|
|
104
|
-
stdout,
|
|
105
|
-
stderr,
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
|
|
112
|
-
const candidate = raw.trim();
|
|
113
|
-
if (!candidate) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
const parsedUrl = parseNormalizedGatewayUrl(candidate);
|
|
117
|
-
if (parsedUrl) {
|
|
118
|
-
return parsedUrl;
|
|
119
|
-
}
|
|
120
|
-
const hostPort = candidate.split("/", 1)[0]?.trim() ?? "";
|
|
121
|
-
return hostPort ? `${schemeFallback}://${hostPort}` : null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function parseNormalizedGatewayUrl(raw: string): string | null {
|
|
125
|
-
try {
|
|
126
|
-
const parsed = new URL(raw);
|
|
127
|
-
const scheme = parsed.protocol.slice(0, -1);
|
|
128
|
-
const normalizedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme;
|
|
129
|
-
if (!(normalizedScheme === "ws" || normalizedScheme === "wss")) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
if (!parsed.hostname) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
return `${normalizedScheme}://${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parsePositiveInteger(raw: string | undefined): number | null {
|
|
142
|
-
if (!raw) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const parsed = Number.parseInt(raw, 10);
|
|
146
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function resolveGatewayPort(cfg: SymiPluginApi["config"]): number {
|
|
150
|
-
const envPort =
|
|
151
|
-
parsePositiveInteger(process.env.SYMI_GATEWAY_PORT?.trim()) ??
|
|
152
|
-
parsePositiveInteger(process.env.SYMI_GATEWAY_PORT?.trim());
|
|
153
|
-
if (envPort) {
|
|
154
|
-
return envPort;
|
|
155
|
-
}
|
|
156
|
-
const configPort = cfg.gateway?.port;
|
|
157
|
-
if (typeof configPort === "number" && Number.isFinite(configPort) && configPort > 0) {
|
|
158
|
-
return configPort;
|
|
159
|
-
}
|
|
160
|
-
return DEFAULT_GATEWAY_PORT;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resolveScheme(
|
|
164
|
-
cfg: SymiPluginApi["config"],
|
|
165
|
-
opts?: { forceSecure?: boolean },
|
|
166
|
-
): "ws" | "wss" {
|
|
167
|
-
if (opts?.forceSecure) {
|
|
168
|
-
return "wss";
|
|
169
|
-
}
|
|
170
|
-
return cfg.gateway?.tls?.enabled === true ? "wss" : "ws";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function isPrivateIPv4(address: string): boolean {
|
|
174
|
-
const parts = address.split(".");
|
|
175
|
-
if (parts.length != 4) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
const octets = parts.map((part) => Number.parseInt(part, 10));
|
|
179
|
-
if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
const [a, b] = octets;
|
|
183
|
-
if (a === 10) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
if (a === 192 && b === 168) {
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function isTailnetIPv4(address: string): boolean {
|
|
196
|
-
const parts = address.split(".");
|
|
197
|
-
if (parts.length !== 4) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
const octets = parts.map((part) => Number.parseInt(part, 10));
|
|
201
|
-
if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
const [a, b] = octets;
|
|
205
|
-
return a === 100 && b >= 64 && b <= 127;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function pickMatchingIPv4(predicate: (address: string) => boolean): string | null {
|
|
209
|
-
const nets = os.networkInterfaces();
|
|
210
|
-
for (const entries of Object.values(nets)) {
|
|
211
|
-
if (!entries) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
for (const entry of entries) {
|
|
215
|
-
const family = entry?.family;
|
|
216
|
-
// Check for IPv4 (string "IPv4" on Node 18+, number 4 on older)
|
|
217
|
-
const isIpv4 = family === "IPv4" || String(family) === "4";
|
|
218
|
-
if (!entry || entry.internal || !isIpv4) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
const address = entry.address?.trim() ?? "";
|
|
222
|
-
if (!address) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (predicate(address)) {
|
|
226
|
-
return address;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function pickLanIPv4(): string | null {
|
|
234
|
-
return pickMatchingIPv4(isPrivateIPv4);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function pickTailnetIPv4(): string | null {
|
|
238
|
-
return pickMatchingIPv4(isTailnetIPv4);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function resolveTailnetHost(): Promise<string | null> {
|
|
242
|
-
const candidates = ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"];
|
|
243
|
-
for (const candidate of candidates) {
|
|
244
|
-
try {
|
|
245
|
-
const result = await runFixedCommandWithTimeout([candidate, "status", "--json"], 5000);
|
|
246
|
-
if (result.code !== 0) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
const raw = result.stdout.trim();
|
|
250
|
-
if (!raw) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const parsed = parsePossiblyNoisyJsonObject(raw);
|
|
254
|
-
const self =
|
|
255
|
-
typeof parsed.Self === "object" && parsed.Self !== null
|
|
256
|
-
? (parsed.Self as Record<string, unknown>)
|
|
257
|
-
: undefined;
|
|
258
|
-
const dns = typeof self?.DNSName === "string" ? self.DNSName : undefined;
|
|
259
|
-
if (dns && dns.length > 0) {
|
|
260
|
-
return dns.replace(/\.$/, "");
|
|
261
|
-
}
|
|
262
|
-
const ips = Array.isArray(self?.TailscaleIPs) ? (self?.TailscaleIPs as string[]) : [];
|
|
263
|
-
if (ips.length > 0) {
|
|
264
|
-
return ips[0] ?? null;
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function parsePossiblyNoisyJsonObject(raw: string): Record<string, unknown> {
|
|
274
|
-
const start = raw.indexOf("{");
|
|
275
|
-
const end = raw.lastIndexOf("}");
|
|
276
|
-
if (start === -1 || end <= start) {
|
|
277
|
-
return {};
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
return JSON.parse(raw.slice(start, end + 1)) as Record<string, unknown>;
|
|
281
|
-
} catch {
|
|
282
|
-
return {};
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function resolveAuth(cfg: SymiPluginApi["config"]): ResolveAuthResult {
|
|
287
|
-
const mode = cfg.gateway?.auth?.mode;
|
|
288
|
-
const token =
|
|
289
|
-
pickFirstDefined([
|
|
290
|
-
process.env.SYMI_GATEWAY_TOKEN,
|
|
291
|
-
process.env.SYMI_GATEWAY_TOKEN,
|
|
292
|
-
cfg.gateway?.auth?.token,
|
|
293
|
-
]) ?? undefined;
|
|
294
|
-
const password =
|
|
295
|
-
pickFirstDefined([
|
|
296
|
-
process.env.SYMI_GATEWAY_PASSWORD,
|
|
297
|
-
process.env.SYMI_GATEWAY_PASSWORD,
|
|
298
|
-
cfg.gateway?.auth?.password,
|
|
299
|
-
]) ?? undefined;
|
|
300
|
-
|
|
301
|
-
if (mode === "token" || mode === "password") {
|
|
302
|
-
return resolveRequiredAuth(mode, { token, password });
|
|
303
|
-
}
|
|
304
|
-
if (token) {
|
|
305
|
-
return { token, label: "token" };
|
|
306
|
-
}
|
|
307
|
-
if (password) {
|
|
308
|
-
return { password, label: "password" };
|
|
309
|
-
}
|
|
310
|
-
return { error: "Gateway auth is not configured (no token or password)." };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function pickFirstDefined(candidates: Array<string | undefined>): string | null {
|
|
314
|
-
for (const value of candidates) {
|
|
315
|
-
const trimmed = value?.trim();
|
|
316
|
-
if (trimmed) {
|
|
317
|
-
return trimmed;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function resolveRequiredAuth(
|
|
324
|
-
mode: "token" | "password",
|
|
325
|
-
values: { token?: string; password?: string },
|
|
326
|
-
): ResolveAuthResult {
|
|
327
|
-
if (mode === "token") {
|
|
328
|
-
return values.token
|
|
329
|
-
? { token: values.token, label: "token" }
|
|
330
|
-
: { error: "Gateway auth is set to token, but no token is configured." };
|
|
331
|
-
}
|
|
332
|
-
return values.password
|
|
333
|
-
? { password: values.password, label: "password" }
|
|
334
|
-
: { error: "Gateway auth is set to password, but no password is configured." };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function resolveGatewayUrl(api: SymiPluginApi): Promise<ResolveUrlResult> {
|
|
338
|
-
const cfg = api.config;
|
|
339
|
-
const pluginCfg = (api.pluginConfig ?? {}) as DevicePairPluginConfig;
|
|
340
|
-
const scheme = resolveScheme(cfg);
|
|
341
|
-
const port = resolveGatewayPort(cfg);
|
|
342
|
-
|
|
343
|
-
if (typeof pluginCfg.publicUrl === "string" && pluginCfg.publicUrl.trim()) {
|
|
344
|
-
const url = normalizeUrl(pluginCfg.publicUrl, scheme);
|
|
345
|
-
if (url) {
|
|
346
|
-
return { url, source: "plugins.entries.device-pair.config.publicUrl" };
|
|
347
|
-
}
|
|
348
|
-
return { error: "Configured publicUrl is invalid." };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
|
352
|
-
if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
|
|
353
|
-
const host = await resolveTailnetHost();
|
|
354
|
-
if (!host) {
|
|
355
|
-
return { error: "Tailscale Serve is enabled, but MagicDNS could not be resolved." };
|
|
356
|
-
}
|
|
357
|
-
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const remoteUrl = cfg.gateway?.remote?.url;
|
|
361
|
-
if (typeof remoteUrl === "string" && remoteUrl.trim()) {
|
|
362
|
-
const url = normalizeUrl(remoteUrl, scheme);
|
|
363
|
-
if (url) {
|
|
364
|
-
return { url, source: "gateway.remote.url" };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const bind = cfg.gateway?.bind ?? "loopback";
|
|
369
|
-
if (bind === "custom") {
|
|
370
|
-
const host = cfg.gateway?.customBindHost?.trim();
|
|
371
|
-
if (host) {
|
|
372
|
-
return { url: `${scheme}://${host}:${port}`, source: "gateway.bind=custom" };
|
|
373
|
-
}
|
|
374
|
-
return { error: "gateway.bind=custom requires gateway.customBindHost." };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (bind === "tailnet") {
|
|
378
|
-
const host = pickTailnetIPv4();
|
|
379
|
-
if (host) {
|
|
380
|
-
return { url: `${scheme}://${host}:${port}`, source: "gateway.bind=tailnet" };
|
|
381
|
-
}
|
|
382
|
-
return { error: "gateway.bind=tailnet set, but no tailnet IP was found." };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (bind === "lan") {
|
|
386
|
-
const host = pickLanIPv4();
|
|
387
|
-
if (host) {
|
|
388
|
-
return { url: `${scheme}://${host}:${port}`, source: "gateway.bind=lan" };
|
|
389
|
-
}
|
|
390
|
-
return { error: "gateway.bind=lan set, but no private LAN IP was found." };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
error:
|
|
395
|
-
"Gateway is only bound to loopback. Set gateway.bind=lan, enable tailscale serve, or configure plugins.entries.device-pair.config.publicUrl.",
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function encodeSetupCode(payload: SetupPayload): string {
|
|
400
|
-
const json = JSON.stringify(payload);
|
|
401
|
-
const base64 = Buffer.from(json, "utf8").toString("base64");
|
|
402
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function formatSetupReply(payload: SetupPayload, authLabel: string): string {
|
|
406
|
-
const setupCode = encodeSetupCode(payload);
|
|
407
|
-
return [
|
|
408
|
-
"Pairing setup code generated.",
|
|
409
|
-
"",
|
|
410
|
-
"1) Open the iOS app → Settings → Gateway",
|
|
411
|
-
"2) Paste the setup code below and tap Connect",
|
|
412
|
-
"3) Back here, run /pair approve",
|
|
413
|
-
"",
|
|
414
|
-
"Setup code:",
|
|
415
|
-
setupCode,
|
|
416
|
-
"",
|
|
417
|
-
`Gateway: ${payload.url}`,
|
|
418
|
-
`Auth: ${authLabel}`,
|
|
419
|
-
].join("\n");
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function formatSetupInstructions(): string {
|
|
423
|
-
return [
|
|
424
|
-
"Pairing setup code generated.",
|
|
425
|
-
"",
|
|
426
|
-
"1) Open the iOS app → Settings → Gateway",
|
|
427
|
-
"2) Paste the setup code from my next message and tap Connect",
|
|
428
|
-
"3) Back here, run /pair approve",
|
|
429
|
-
].join("\n");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
type PendingPairingRequest = {
|
|
433
|
-
requestId: string;
|
|
434
|
-
deviceId: string;
|
|
435
|
-
displayName?: string;
|
|
436
|
-
platform?: string;
|
|
437
|
-
remoteIp?: string;
|
|
438
|
-
ts?: number;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
function formatPendingRequests(pending: PendingPairingRequest[]): string {
|
|
442
|
-
if (pending.length === 0) {
|
|
443
|
-
return "No pending device pairing requests.";
|
|
444
|
-
}
|
|
445
|
-
const lines: string[] = ["Pending device pairing requests:"];
|
|
446
|
-
for (const req of pending) {
|
|
447
|
-
const label = req.displayName?.trim() || req.deviceId;
|
|
448
|
-
const platform = req.platform?.trim();
|
|
449
|
-
const ip = req.remoteIp?.trim();
|
|
450
|
-
const parts = [
|
|
451
|
-
`- ${req.requestId}`,
|
|
452
|
-
label ? `name=${label}` : null,
|
|
453
|
-
platform ? `platform=${platform}` : null,
|
|
454
|
-
ip ? `ip=${ip}` : null,
|
|
455
|
-
].filter(Boolean);
|
|
456
|
-
lines.push(parts.join(" · "));
|
|
457
|
-
}
|
|
458
|
-
return lines.join("\n");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export default function register(api: SymiPluginApi) {
|
|
462
|
-
api.registerCommand({
|
|
463
|
-
name: "pair",
|
|
464
|
-
description: "Generate setup codes and approve device pairing requests.",
|
|
465
|
-
acceptsArgs: true,
|
|
466
|
-
handler: async (ctx) => {
|
|
467
|
-
const args = ctx.args?.trim() ?? "";
|
|
468
|
-
const tokens = args.split(/\s+/).filter(Boolean);
|
|
469
|
-
const action = tokens[0]?.toLowerCase() ?? "";
|
|
470
|
-
api.logger.info?.(
|
|
471
|
-
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
|
|
472
|
-
action || "new"
|
|
473
|
-
}`,
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
if (action === "status" || action === "pending") {
|
|
477
|
-
const list = await listDevicePairing();
|
|
478
|
-
return { text: formatPendingRequests(list.pending) };
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (action === "approve") {
|
|
482
|
-
const requested = tokens[1]?.trim();
|
|
483
|
-
const list = await listDevicePairing();
|
|
484
|
-
if (list.pending.length === 0) {
|
|
485
|
-
return { text: "No pending device pairing requests." };
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
let pending: (typeof list.pending)[number] | undefined;
|
|
489
|
-
if (requested) {
|
|
490
|
-
if (requested.toLowerCase() === "latest") {
|
|
491
|
-
pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
|
|
492
|
-
} else {
|
|
493
|
-
pending = list.pending.find((entry) => entry.requestId === requested);
|
|
494
|
-
}
|
|
495
|
-
} else if (list.pending.length === 1) {
|
|
496
|
-
pending = list.pending[0];
|
|
497
|
-
} else {
|
|
498
|
-
return {
|
|
499
|
-
text:
|
|
500
|
-
`${formatPendingRequests(list.pending)}\n\n` +
|
|
501
|
-
"Multiple pending requests found. Approve one explicitly:\n" +
|
|
502
|
-
"/pair approve <requestId>\n" +
|
|
503
|
-
"Or approve the most recent:\n" +
|
|
504
|
-
"/pair approve latest",
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
if (!pending) {
|
|
508
|
-
return { text: "Pairing request not found." };
|
|
509
|
-
}
|
|
510
|
-
const approved = await approveDevicePairing(pending.requestId);
|
|
511
|
-
if (!approved) {
|
|
512
|
-
return { text: "Pairing request not found." };
|
|
513
|
-
}
|
|
514
|
-
const label = approved.device.displayName?.trim() || approved.device.deviceId;
|
|
515
|
-
const platform = approved.device.platform?.trim();
|
|
516
|
-
const platformLabel = platform ? ` (${platform})` : "";
|
|
517
|
-
return { text: `✅ Paired ${label}${platformLabel}.` };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const auth = resolveAuth(api.config);
|
|
521
|
-
if (auth.error) {
|
|
522
|
-
return { text: `Error: ${auth.error}` };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const urlResult = await resolveGatewayUrl(api);
|
|
526
|
-
if (!urlResult.url) {
|
|
527
|
-
return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const payload: SetupPayload = {
|
|
531
|
-
url: urlResult.url,
|
|
532
|
-
token: auth.token,
|
|
533
|
-
password: auth.password,
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
if (action === "qr") {
|
|
537
|
-
const setupCode = encodeSetupCode(payload);
|
|
538
|
-
const qrAscii = await renderQrAscii(setupCode);
|
|
539
|
-
const authLabel = auth.label ?? "auth";
|
|
540
|
-
|
|
541
|
-
const channel = ctx.channel;
|
|
542
|
-
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
|
543
|
-
|
|
544
|
-
if (channel === "telegram" && target) {
|
|
545
|
-
try {
|
|
546
|
-
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
|
|
547
|
-
if (send) {
|
|
548
|
-
await send(
|
|
549
|
-
target,
|
|
550
|
-
["Scan this QR code with the Symi iOS app:", "", "```", qrAscii, "```"].join("\n"),
|
|
551
|
-
{
|
|
552
|
-
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
|
|
553
|
-
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
|
|
554
|
-
},
|
|
555
|
-
);
|
|
556
|
-
return {
|
|
557
|
-
text: [
|
|
558
|
-
`Gateway: ${payload.url}`,
|
|
559
|
-
`Auth: ${authLabel}`,
|
|
560
|
-
"",
|
|
561
|
-
"After scanning, come back here and run `/pair approve` to complete pairing.",
|
|
562
|
-
].join("\n"),
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
} catch (err) {
|
|
566
|
-
api.logger.warn?.(
|
|
567
|
-
`device-pair: telegram QR send failed, falling back (${String(
|
|
568
|
-
(err as Error)?.message ?? err,
|
|
569
|
-
)})`,
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Render based on channel capability
|
|
575
|
-
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
|
|
576
|
-
const infoLines = [
|
|
577
|
-
`Gateway: ${payload.url}`,
|
|
578
|
-
`Auth: ${authLabel}`,
|
|
579
|
-
"",
|
|
580
|
-
"After scanning, run `/pair approve` to complete pairing.",
|
|
581
|
-
];
|
|
582
|
-
|
|
583
|
-
// WebUI + CLI/TUI: ASCII QR
|
|
584
|
-
return {
|
|
585
|
-
text: [
|
|
586
|
-
"Scan this QR code with the Symi iOS app:",
|
|
587
|
-
"",
|
|
588
|
-
"```",
|
|
589
|
-
qrAscii,
|
|
590
|
-
"```",
|
|
591
|
-
"",
|
|
592
|
-
...infoLines,
|
|
593
|
-
].join("\n"),
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const channel = ctx.channel;
|
|
598
|
-
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
|
|
599
|
-
const authLabel = auth.label ?? "auth";
|
|
600
|
-
|
|
601
|
-
if (channel === "telegram" && target) {
|
|
602
|
-
try {
|
|
603
|
-
const runtimeKeys = Object.keys(api.runtime ?? {});
|
|
604
|
-
const channelKeys = Object.keys(api.runtime?.channel ?? {});
|
|
605
|
-
api.logger.debug?.(
|
|
606
|
-
`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
|
|
607
|
-
channelKeys.join(",") || "none"
|
|
608
|
-
}`,
|
|
609
|
-
);
|
|
610
|
-
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
|
|
611
|
-
if (!send) {
|
|
612
|
-
throw new Error(
|
|
613
|
-
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
|
|
614
|
-
",",
|
|
615
|
-
)})`,
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
await send(target, formatSetupInstructions(), {
|
|
619
|
-
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
|
|
620
|
-
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
|
|
621
|
-
});
|
|
622
|
-
api.logger.info?.(
|
|
623
|
-
`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
|
|
624
|
-
ctx.messageThreadId ?? "none"
|
|
625
|
-
}`,
|
|
626
|
-
);
|
|
627
|
-
return { text: encodeSetupCode(payload) };
|
|
628
|
-
} catch (err) {
|
|
629
|
-
api.logger.warn?.(
|
|
630
|
-
`device-pair: telegram split send failed, falling back to single message (${String(
|
|
631
|
-
(err as Error)?.message ?? err,
|
|
632
|
-
)})`,
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
text: formatSetupReply(payload, authLabel),
|
|
639
|
-
};
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "device-pair",
|
|
3
|
-
"name": "Device Pairing",
|
|
4
|
-
"description": "Generate setup codes and approve device pairing requests.",
|
|
5
|
-
"configSchema": {
|
|
6
|
-
"type": "object",
|
|
7
|
-
"additionalProperties": false,
|
|
8
|
-
"properties": {
|
|
9
|
-
"publicUrl": {
|
|
10
|
-
"type": "string"
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"uiHints": {
|
|
15
|
-
"publicUrl": {
|
|
16
|
-
"label": "Gateway URL",
|
|
17
|
-
"help": "Public WebSocket URL used for /pair setup codes (ws/wss or http/https)."
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SymiPluginApi } from "symi/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "symi/plugin-sdk";
|
|
3
|
-
import { createDiagnosticsOtelService } from "./src/service.js";
|
|
4
|
-
|
|
5
|
-
const plugin = {
|
|
6
|
-
id: "diagnostics-otel",
|
|
7
|
-
name: "Diagnostics OpenTelemetry",
|
|
8
|
-
description: "Export diagnostics events to OpenTelemetry",
|
|
9
|
-
configSchema: emptyPluginConfigSchema(),
|
|
10
|
-
register(api: SymiPluginApi) {
|
|
11
|
-
api.registerService(createDiagnosticsOtelService());
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default plugin;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/bin/node_modules:/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/node_modules:/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/bin/node_modules:/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/node_modules:/home/symi/projects/symi/node_modules/.pnpm/acorn@8.16.0/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../../../../node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/bin/acorn" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../../../../node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/bin/acorn" "$@"
|
|
21
|
-
fi
|