@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,266 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import type { SymiPluginApi } from "../../../src/plugins/types.js";
|
|
5
|
-
import { resolveWindowsPipelineSpawn } from "./windows-spawn.js";
|
|
6
|
-
|
|
7
|
-
type PipelineEnvelope =
|
|
8
|
-
| {
|
|
9
|
-
ok: true;
|
|
10
|
-
status: "ok" | "needs_approval" | "cancelled";
|
|
11
|
-
output: unknown[];
|
|
12
|
-
requiresApproval: null | {
|
|
13
|
-
type: "approval_request";
|
|
14
|
-
prompt: string;
|
|
15
|
-
items: unknown[];
|
|
16
|
-
resumeToken?: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
| {
|
|
20
|
-
ok: false;
|
|
21
|
-
error: { type?: string; message: string };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function normalizeForCwdSandbox(p: string): string {
|
|
25
|
-
const normalized = path.normalize(p);
|
|
26
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function resolveCwd(cwdRaw: unknown): string {
|
|
30
|
-
if (typeof cwdRaw !== "string" || !cwdRaw.trim()) {
|
|
31
|
-
return process.cwd();
|
|
32
|
-
}
|
|
33
|
-
const cwd = cwdRaw.trim();
|
|
34
|
-
if (path.isAbsolute(cwd)) {
|
|
35
|
-
throw new Error("cwd must be a relative path");
|
|
36
|
-
}
|
|
37
|
-
const base = process.cwd();
|
|
38
|
-
const resolved = path.resolve(base, cwd);
|
|
39
|
-
|
|
40
|
-
const rel = path.relative(normalizeForCwdSandbox(base), normalizeForCwdSandbox(resolved));
|
|
41
|
-
if (rel === "" || rel === ".") {
|
|
42
|
-
return resolved;
|
|
43
|
-
}
|
|
44
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
45
|
-
throw new Error("cwd must stay within the gateway working directory");
|
|
46
|
-
}
|
|
47
|
-
return resolved;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function runPipelineSubprocessOnce(params: {
|
|
51
|
-
execPath: string;
|
|
52
|
-
argv: string[];
|
|
53
|
-
cwd: string;
|
|
54
|
-
timeoutMs: number;
|
|
55
|
-
maxStdoutBytes: number;
|
|
56
|
-
}) {
|
|
57
|
-
const { execPath, argv, cwd } = params;
|
|
58
|
-
const timeoutMs = Math.max(200, params.timeoutMs);
|
|
59
|
-
const maxStdoutBytes = Math.max(1024, params.maxStdoutBytes);
|
|
60
|
-
|
|
61
|
-
const env = { ...process.env, LOBSTER_MODE: "tool" } as Record<string, string | undefined>;
|
|
62
|
-
const nodeOptions = env.NODE_OPTIONS ?? "";
|
|
63
|
-
if (nodeOptions.includes("--inspect")) {
|
|
64
|
-
delete env.NODE_OPTIONS;
|
|
65
|
-
}
|
|
66
|
-
const spawnTarget =
|
|
67
|
-
process.platform === "win32"
|
|
68
|
-
? resolveWindowsPipelineSpawn(execPath, argv, env)
|
|
69
|
-
: { command: execPath, argv };
|
|
70
|
-
|
|
71
|
-
return await new Promise<{ stdout: string }>((resolve, reject) => {
|
|
72
|
-
const child = spawn(spawnTarget.command, spawnTarget.argv, {
|
|
73
|
-
cwd,
|
|
74
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
-
env,
|
|
76
|
-
windowsHide: spawnTarget.windowsHide,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
let stdout = "";
|
|
80
|
-
let stdoutBytes = 0;
|
|
81
|
-
let stderr = "";
|
|
82
|
-
let settled = false;
|
|
83
|
-
|
|
84
|
-
const settle = (
|
|
85
|
-
result: { ok: true; value: { stdout: string } } | { ok: false; error: Error },
|
|
86
|
-
) => {
|
|
87
|
-
if (settled) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
settled = true;
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
if (result.ok) {
|
|
93
|
-
resolve(result.value);
|
|
94
|
-
} else {
|
|
95
|
-
reject(result.error);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const failAndTerminate = (message: string) => {
|
|
100
|
-
try {
|
|
101
|
-
child.kill("SIGKILL");
|
|
102
|
-
} finally {
|
|
103
|
-
settle({ ok: false, error: new Error(message) });
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
child.stdout?.setEncoding("utf8");
|
|
108
|
-
child.stderr?.setEncoding("utf8");
|
|
109
|
-
|
|
110
|
-
child.stdout?.on("data", (chunk) => {
|
|
111
|
-
const str = String(chunk);
|
|
112
|
-
stdoutBytes += Buffer.byteLength(str, "utf8");
|
|
113
|
-
if (stdoutBytes > maxStdoutBytes) {
|
|
114
|
-
failAndTerminate("pipeline output exceeded maxStdoutBytes");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
stdout += str;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
child.stderr?.on("data", (chunk) => {
|
|
121
|
-
stderr += String(chunk);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const timer = setTimeout(() => {
|
|
125
|
-
failAndTerminate("pipeline subprocess timed out");
|
|
126
|
-
}, timeoutMs);
|
|
127
|
-
|
|
128
|
-
child.once("error", (err) => {
|
|
129
|
-
settle({ ok: false, error: err });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
child.once("exit", (code) => {
|
|
133
|
-
if (code !== 0) {
|
|
134
|
-
settle({
|
|
135
|
-
ok: false,
|
|
136
|
-
error: new Error(`pipeline failed (${code ?? "?"}): ${stderr.trim() || stdout.trim()}`),
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
settle({ ok: true, value: { stdout } });
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function parseEnvelope(stdout: string): PipelineEnvelope {
|
|
146
|
-
const trimmed = stdout.trim();
|
|
147
|
-
|
|
148
|
-
const tryParse = (input: string) => {
|
|
149
|
-
try {
|
|
150
|
-
return JSON.parse(input) as unknown;
|
|
151
|
-
} catch {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
let parsed: unknown = tryParse(trimmed);
|
|
157
|
-
|
|
158
|
-
// Some environments can leak extra stdout (e.g. warnings/logs) before the
|
|
159
|
-
// final JSON envelope. Be tolerant and parse the last JSON-looking suffix.
|
|
160
|
-
if (parsed === undefined) {
|
|
161
|
-
const suffixMatch = trimmed.match(/({[\s\S]*}|\[[\s\S]*])\s*$/);
|
|
162
|
-
if (suffixMatch?.[1]) {
|
|
163
|
-
parsed = tryParse(suffixMatch[1]);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (parsed === undefined) {
|
|
168
|
-
throw new Error("pipeline returned invalid JSON");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!parsed || typeof parsed !== "object") {
|
|
172
|
-
throw new Error("pipeline returned invalid JSON envelope");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const ok = (parsed as { ok?: unknown }).ok;
|
|
176
|
-
if (ok === true || ok === false) {
|
|
177
|
-
return parsed as PipelineEnvelope;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
throw new Error("pipeline returned invalid JSON envelope");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function buildPipelineArgv(action: string, params: Record<string, unknown>): string[] {
|
|
184
|
-
if (action === "run") {
|
|
185
|
-
const pipeline = typeof params.pipeline === "string" ? params.pipeline : "";
|
|
186
|
-
if (!pipeline.trim()) {
|
|
187
|
-
throw new Error("pipeline required");
|
|
188
|
-
}
|
|
189
|
-
const argv = ["run", "--mode", "tool", pipeline];
|
|
190
|
-
const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
|
|
191
|
-
if (argsJson.trim()) {
|
|
192
|
-
argv.push("--args-json", argsJson);
|
|
193
|
-
}
|
|
194
|
-
return argv;
|
|
195
|
-
}
|
|
196
|
-
if (action === "resume") {
|
|
197
|
-
const token = typeof params.token === "string" ? params.token : "";
|
|
198
|
-
if (!token.trim()) {
|
|
199
|
-
throw new Error("token required");
|
|
200
|
-
}
|
|
201
|
-
const approve = params.approve;
|
|
202
|
-
if (typeof approve !== "boolean") {
|
|
203
|
-
throw new Error("approve required");
|
|
204
|
-
}
|
|
205
|
-
return ["resume", "--token", token, "--approve", approve ? "yes" : "no"];
|
|
206
|
-
}
|
|
207
|
-
throw new Error(`Unknown action: ${action}`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function createPipelineTool(api: SymiPluginApi) {
|
|
211
|
-
return {
|
|
212
|
-
name: "pipeline",
|
|
213
|
-
label: "Pipeline Workflow",
|
|
214
|
-
description:
|
|
215
|
-
"Run pipelines as a local-first workflow runtime (typed JSON envelope + resumable approvals).",
|
|
216
|
-
parameters: Type.Object({
|
|
217
|
-
// NOTE: Prefer string enums in tool schemas; some providers reject unions/anyOf.
|
|
218
|
-
action: Type.Unsafe<"run" | "resume">({ type: "string", enum: ["run", "resume"] }),
|
|
219
|
-
pipeline: Type.Optional(Type.String()),
|
|
220
|
-
argsJson: Type.Optional(Type.String()),
|
|
221
|
-
token: Type.Optional(Type.String()),
|
|
222
|
-
approve: Type.Optional(Type.Boolean()),
|
|
223
|
-
cwd: Type.Optional(
|
|
224
|
-
Type.String({
|
|
225
|
-
description:
|
|
226
|
-
"Relative working directory (optional). Must stay within the gateway working directory.",
|
|
227
|
-
}),
|
|
228
|
-
),
|
|
229
|
-
timeoutMs: Type.Optional(Type.Number()),
|
|
230
|
-
maxStdoutBytes: Type.Optional(Type.Number()),
|
|
231
|
-
}),
|
|
232
|
-
async execute(_id: string, params: Record<string, unknown>) {
|
|
233
|
-
const action = typeof params.action === "string" ? params.action.trim() : "";
|
|
234
|
-
if (!action) {
|
|
235
|
-
throw new Error("action required");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const execPath = "lobster";
|
|
239
|
-
const cwd = resolveCwd(params.cwd);
|
|
240
|
-
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
|
|
241
|
-
const maxStdoutBytes =
|
|
242
|
-
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
|
|
243
|
-
|
|
244
|
-
const argv = buildPipelineArgv(action, params);
|
|
245
|
-
|
|
246
|
-
if (api.runtime?.version && api.logger?.debug) {
|
|
247
|
-
api.logger.debug(`pipeline plugin runtime=${api.runtime.version}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const { stdout } = await runPipelineSubprocessOnce({
|
|
251
|
-
execPath,
|
|
252
|
-
argv,
|
|
253
|
-
cwd,
|
|
254
|
-
timeoutMs,
|
|
255
|
-
maxStdoutBytes,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const envelope = parseEnvelope(stdout);
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
262
|
-
details: envelope,
|
|
263
|
-
};
|
|
264
|
-
},
|
|
265
|
-
};
|
|
266
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { resolveWindowsPipelineSpawn } from "./windows-spawn.js";
|
|
6
|
-
|
|
7
|
-
function setProcessPlatform(platform: NodeJS.Platform) {
|
|
8
|
-
Object.defineProperty(process, "platform", {
|
|
9
|
-
value: platform,
|
|
10
|
-
configurable: true,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("resolveWindowsPipelineSpawn", () => {
|
|
15
|
-
let tempDir = "";
|
|
16
|
-
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
17
|
-
const originalPath = process.env.PATH;
|
|
18
|
-
const originalPathAlt = process.env.Path;
|
|
19
|
-
const originalPathExt = process.env.PATHEXT;
|
|
20
|
-
const originalPathExtAlt = process.env.Pathext;
|
|
21
|
-
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "symi-pipeline-win-spawn-"));
|
|
24
|
-
setProcessPlatform("win32");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
if (originalPlatform) {
|
|
29
|
-
Object.defineProperty(process, "platform", originalPlatform);
|
|
30
|
-
}
|
|
31
|
-
if (originalPath === undefined) {
|
|
32
|
-
delete process.env.PATH;
|
|
33
|
-
} else {
|
|
34
|
-
process.env.PATH = originalPath;
|
|
35
|
-
}
|
|
36
|
-
if (originalPathAlt === undefined) {
|
|
37
|
-
delete process.env.Path;
|
|
38
|
-
} else {
|
|
39
|
-
process.env.Path = originalPathAlt;
|
|
40
|
-
}
|
|
41
|
-
if (originalPathExt === undefined) {
|
|
42
|
-
delete process.env.PATHEXT;
|
|
43
|
-
} else {
|
|
44
|
-
process.env.PATHEXT = originalPathExt;
|
|
45
|
-
}
|
|
46
|
-
if (originalPathExtAlt === undefined) {
|
|
47
|
-
delete process.env.Pathext;
|
|
48
|
-
} else {
|
|
49
|
-
process.env.Pathext = originalPathExtAlt;
|
|
50
|
-
}
|
|
51
|
-
if (tempDir) {
|
|
52
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
53
|
-
tempDir = "";
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("unwraps cmd shim with %dp0% token", async () => {
|
|
58
|
-
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
|
|
59
|
-
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
|
|
60
|
-
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
61
|
-
await fs.mkdir(path.dirname(shimPath), { recursive: true });
|
|
62
|
-
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
|
|
63
|
-
await fs.writeFile(
|
|
64
|
-
shimPath,
|
|
65
|
-
`@echo off\r\n"%dp0%\\..\\shim-dist\\lobster-cli.cjs" %*\r\n`,
|
|
66
|
-
"utf8",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const target = resolveWindowsPipelineSpawn(shimPath, ["run", "noop"], process.env);
|
|
70
|
-
expect(target.command).toBe(process.execPath);
|
|
71
|
-
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
|
|
72
|
-
expect(target.windowsHide).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("unwraps cmd shim with %~dp0% token", async () => {
|
|
76
|
-
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
|
|
77
|
-
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
|
|
78
|
-
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
79
|
-
await fs.mkdir(path.dirname(shimPath), { recursive: true });
|
|
80
|
-
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
|
|
81
|
-
await fs.writeFile(
|
|
82
|
-
shimPath,
|
|
83
|
-
`@echo off\r\n"%~dp0%\\..\\shim-dist\\lobster-cli.cjs" %*\r\n`,
|
|
84
|
-
"utf8",
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const target = resolveWindowsPipelineSpawn(shimPath, ["run", "noop"], process.env);
|
|
88
|
-
expect(target.command).toBe(process.execPath);
|
|
89
|
-
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
|
|
90
|
-
expect(target.windowsHide).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("ignores node.exe shim entries and picks pipeline script", async () => {
|
|
94
|
-
const shimDir = path.join(tempDir, "shim-with-node");
|
|
95
|
-
const scriptPath = path.join(tempDir, "shim-dist-node", "lobster-cli.cjs");
|
|
96
|
-
const shimPath = path.join(shimDir, "lobster.cmd");
|
|
97
|
-
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
98
|
-
await fs.mkdir(shimDir, { recursive: true });
|
|
99
|
-
await fs.writeFile(path.join(shimDir, "node.exe"), "", "utf8");
|
|
100
|
-
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
|
|
101
|
-
await fs.writeFile(
|
|
102
|
-
shimPath,
|
|
103
|
-
`@echo off\r\n"%~dp0%\\node.exe" "%~dp0%\\..\\shim-dist-node\\lobster-cli.cjs" %*\r\n`,
|
|
104
|
-
"utf8",
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const target = resolveWindowsPipelineSpawn(shimPath, ["run", "noop"], process.env);
|
|
108
|
-
expect(target.command).toBe(process.execPath);
|
|
109
|
-
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
|
|
110
|
-
expect(target.windowsHide).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("resolves pipeline .cmd from PATH and unwraps npm layout shim", async () => {
|
|
114
|
-
const binDir = path.join(tempDir, "node_modules", ".bin");
|
|
115
|
-
const packageDir = path.join(tempDir, "node_modules", "lobster");
|
|
116
|
-
const scriptPath = path.join(packageDir, "dist", "cli.js");
|
|
117
|
-
const shimPath = path.join(binDir, "lobster.cmd");
|
|
118
|
-
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
119
|
-
await fs.mkdir(binDir, { recursive: true });
|
|
120
|
-
await fs.writeFile(shimPath, "@echo off\r\n", "utf8");
|
|
121
|
-
await fs.writeFile(
|
|
122
|
-
path.join(packageDir, "package.json"),
|
|
123
|
-
JSON.stringify({ name: "lobster", version: "0.0.0", bin: { lobster: "dist/cli.js" } }),
|
|
124
|
-
"utf8",
|
|
125
|
-
);
|
|
126
|
-
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
|
|
127
|
-
|
|
128
|
-
const env = {
|
|
129
|
-
...process.env,
|
|
130
|
-
PATH: `${binDir};${process.env.PATH ?? ""}`,
|
|
131
|
-
PATHEXT: ".CMD;.EXE",
|
|
132
|
-
};
|
|
133
|
-
const target = resolveWindowsPipelineSpawn("lobster", ["run", "noop"], env);
|
|
134
|
-
expect(target.command).toBe(process.execPath);
|
|
135
|
-
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
|
|
136
|
-
expect(target.windowsHide).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("fails fast when wrapper cannot be resolved without shell execution", async () => {
|
|
140
|
-
const badShimPath = path.join(tempDir, "bad-shim", "lobster.cmd");
|
|
141
|
-
await fs.mkdir(path.dirname(badShimPath), { recursive: true });
|
|
142
|
-
await fs.writeFile(badShimPath, "@echo off\r\nREM no entrypoint\r\n", "utf8");
|
|
143
|
-
|
|
144
|
-
expect(() => resolveWindowsPipelineSpawn(badShimPath, ["run", "noop"], process.env)).toThrow(
|
|
145
|
-
/without shell execution/,
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
type SpawnTarget = {
|
|
5
|
-
command: string;
|
|
6
|
-
argv: string[];
|
|
7
|
-
windowsHide?: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
function isFilePath(value: string): boolean {
|
|
11
|
-
try {
|
|
12
|
-
const stat = fs.statSync(value);
|
|
13
|
-
return stat.isFile();
|
|
14
|
-
} catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function resolveWindowsExecutablePath(execPath: string, env: NodeJS.ProcessEnv): string {
|
|
20
|
-
if (execPath.includes("/") || execPath.includes("\\") || path.isAbsolute(execPath)) {
|
|
21
|
-
return execPath;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const pathValue = env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? "";
|
|
25
|
-
const pathEntries = pathValue
|
|
26
|
-
.split(";")
|
|
27
|
-
.map((entry) => entry.trim())
|
|
28
|
-
.filter(Boolean);
|
|
29
|
-
|
|
30
|
-
const hasExtension = path.extname(execPath).length > 0;
|
|
31
|
-
const pathExtRaw =
|
|
32
|
-
env.PATHEXT ??
|
|
33
|
-
env.Pathext ??
|
|
34
|
-
process.env.PATHEXT ??
|
|
35
|
-
process.env.Pathext ??
|
|
36
|
-
".EXE;.CMD;.BAT;.COM";
|
|
37
|
-
const pathExt = hasExtension
|
|
38
|
-
? [""]
|
|
39
|
-
: pathExtRaw
|
|
40
|
-
.split(";")
|
|
41
|
-
.map((ext) => ext.trim())
|
|
42
|
-
.filter(Boolean)
|
|
43
|
-
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`));
|
|
44
|
-
|
|
45
|
-
for (const dir of pathEntries) {
|
|
46
|
-
for (const ext of pathExt) {
|
|
47
|
-
for (const candidateExt of [ext, ext.toLowerCase(), ext.toUpperCase()]) {
|
|
48
|
-
const candidate = path.join(dir, `${execPath}${candidateExt}`);
|
|
49
|
-
if (isFilePath(candidate)) {
|
|
50
|
-
return candidate;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return execPath;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resolveBinEntry(binField: string | Record<string, string> | undefined): string | null {
|
|
60
|
-
if (typeof binField === "string") {
|
|
61
|
-
const trimmed = binField.trim();
|
|
62
|
-
return trimmed || null;
|
|
63
|
-
}
|
|
64
|
-
if (!binField || typeof binField !== "object") {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const preferred = binField.lobster;
|
|
69
|
-
if (typeof preferred === "string" && preferred.trim()) {
|
|
70
|
-
return preferred.trim();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const value of Object.values(binField)) {
|
|
74
|
-
if (typeof value === "string" && value.trim()) {
|
|
75
|
-
return value.trim();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function resolvePipelineScriptFromPackageJson(wrapperPath: string): string | null {
|
|
82
|
-
const wrapperDir = path.dirname(wrapperPath);
|
|
83
|
-
const packageDirs = [
|
|
84
|
-
// Local install: <repo>/node_modules/.bin/lobster.cmd -> ../lobster
|
|
85
|
-
path.resolve(wrapperDir, "..", "lobster"),
|
|
86
|
-
// Global npm install: <npm-prefix>/lobster.cmd -> ./node_modules/lobster
|
|
87
|
-
path.resolve(wrapperDir, "node_modules", "lobster"),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
for (const packageDir of packageDirs) {
|
|
91
|
-
const packageJsonPath = path.join(packageDir, "package.json");
|
|
92
|
-
if (!isFilePath(packageJsonPath)) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
98
|
-
bin?: string | Record<string, string>;
|
|
99
|
-
};
|
|
100
|
-
const scriptRel = resolveBinEntry(packageJson.bin);
|
|
101
|
-
if (!scriptRel) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const scriptPath = path.resolve(packageDir, scriptRel);
|
|
105
|
-
if (isFilePath(scriptPath)) {
|
|
106
|
-
return scriptPath;
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
// Ignore malformed package metadata; caller will throw a guided error.
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolvePipelineScriptFromCmdShim(wrapperPath: string): string | null {
|
|
117
|
-
if (!isFilePath(wrapperPath)) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const content = fs.readFileSync(wrapperPath, "utf8");
|
|
123
|
-
const candidates: string[] = [];
|
|
124
|
-
const extractRelativeFromToken = (token: string): string | null => {
|
|
125
|
-
const match = token.match(/%~?dp0%\s*[\\/]*(.*)$/i);
|
|
126
|
-
if (!match) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
const relative = match[1];
|
|
130
|
-
if (!relative) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
return relative;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const matches = content.matchAll(/"([^"\r\n]*)"/g);
|
|
137
|
-
for (const match of matches) {
|
|
138
|
-
const token = match[1] ?? "";
|
|
139
|
-
const relative = extractRelativeFromToken(token);
|
|
140
|
-
if (!relative) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const normalizedRelative = relative
|
|
145
|
-
.trim()
|
|
146
|
-
.replace(/[\\/]+/g, path.sep)
|
|
147
|
-
.replace(/^[\\/]+/, "");
|
|
148
|
-
const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative);
|
|
149
|
-
if (isFilePath(candidate)) {
|
|
150
|
-
candidates.push(candidate);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const nonNode = candidates.find((candidate) => {
|
|
155
|
-
const base = path.basename(candidate).toLowerCase();
|
|
156
|
-
return base !== "node.exe" && base !== "node";
|
|
157
|
-
});
|
|
158
|
-
if (nonNode) {
|
|
159
|
-
return nonNode;
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// Ignore unreadable shims; caller will throw a guided error.
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function resolveWindowsPipelineSpawn(
|
|
169
|
-
execPath: string,
|
|
170
|
-
argv: string[],
|
|
171
|
-
env: NodeJS.ProcessEnv,
|
|
172
|
-
): SpawnTarget {
|
|
173
|
-
const resolvedExecPath = resolveWindowsExecutablePath(execPath, env);
|
|
174
|
-
const ext = path.extname(resolvedExecPath).toLowerCase();
|
|
175
|
-
if (ext !== ".cmd" && ext !== ".bat") {
|
|
176
|
-
return { command: resolvedExecPath, argv };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const scriptPath =
|
|
180
|
-
resolvePipelineScriptFromCmdShim(resolvedExecPath) ??
|
|
181
|
-
resolvePipelineScriptFromPackageJson(resolvedExecPath);
|
|
182
|
-
if (!scriptPath) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`${path.basename(resolvedExecPath)} wrapper resolved, but no Node entrypoint could be resolved without shell execution. Ensure the pipeline runtime is installed and runnable on PATH (prefer lobster.exe).`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const entryExt = path.extname(scriptPath).toLowerCase();
|
|
189
|
-
if (entryExt === ".exe") {
|
|
190
|
-
return { command: scriptPath, argv, windowsHide: true };
|
|
191
|
-
}
|
|
192
|
-
return { command: process.execPath, argv: [scriptPath, ...argv], windowsHide: true };
|
|
193
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Qwen OAuth (Symi plugin)
|
|
2
|
-
|
|
3
|
-
OAuth provider plugin for **Qwen** (free-tier OAuth).
|
|
4
|
-
|
|
5
|
-
## Enable
|
|
6
|
-
|
|
7
|
-
Bundled plugins are disabled by default. Enable this one:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
symi plugins enable qwen-portal-auth
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Restart the Gateway after enabling.
|
|
14
|
-
|
|
15
|
-
## Authenticate
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
symi models auth login --provider qwen-portal --set-default
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Notes
|
|
22
|
-
|
|
23
|
-
- Qwen OAuth uses a device-code login flow.
|
|
24
|
-
- Tokens auto-refresh; re-run login if refresh fails or access is revoked.
|