@symerian/symi 3.0.17 → 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/{audio-preflight-CBDFctZN.js → audio-preflight-BfmZbg4Y.js} +4 -4
- package/dist/{audio-preflight-gsZSpG-6.js → audio-preflight-DcuC-liM.js} +4 -4
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +8 -8
- package/dist/bundled/session-memory/handler.js +7 -7
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-nPMY1XTJ.js → chrome-Bo7cbvFK.js} +5 -5
- package/dist/{chrome-BjVab8gM.js → chrome-DYp18Q0t.js} +5 -5
- package/dist/{deliver-D-QFqm31.js → deliver-ChSIbiMM.js} +1 -1
- package/dist/{deliver-B4-bcot9.js → deliver-DEgRQM4J.js} +1 -1
- package/dist/extensionAPI.js +7 -7
- package/dist/{image-CDwtQjmt.js → image-Bx-hvoNJ.js} +1 -1
- package/dist/{image-CcS-vzTA.js → image-CQl_mjWk.js} +1 -1
- package/dist/llm-slug-generator.js +7 -7
- package/dist/{manager-BnEdHzmO.js → manager-D_pn0urG.js} +1 -1
- package/dist/{manager-09r0qPze.js → manager-YQxK2t0C.js} +1 -1
- package/dist/{pi-embedded-CWsY69-4.js → pi-embedded-CLw_ZzEZ.js} +16 -16
- package/dist/{pi-embedded-helpers-BBMy-lqr.js → pi-embedded-helpers-B5I53aw6.js} +4 -4
- package/dist/{pi-embedded-helpers-ChEYbgVj.js → pi-embedded-helpers-sUAEIC9X.js} +4 -4
- package/dist/plugin-sdk/{accounts-BfyWsC_i.js → accounts-CWFytwbR.js} +3 -3
- package/dist/plugin-sdk/{active-listener-DcJW7xAT.js → active-listener-BkZ4jHrL.js} +2 -2
- package/dist/plugin-sdk/{agent-scope-ChbGV6of.js → agent-scope-C9gfY_Gk.js} +2 -2
- package/dist/plugin-sdk/{audio-preflight-D3GtNLqW.js → audio-preflight-HKbdzXLZ.js} +21 -21
- package/dist/plugin-sdk/{bindings-CN2Qmefj.js → bindings-BaKIqPPy.js} +2 -2
- package/dist/plugin-sdk/{channel-web-DTyqujjA.js → channel-web-D5nWiTH1.js} +18 -18
- package/dist/plugin-sdk/{chrome-BKzAKr3K.js → chrome-klTSnz-9.js} +3 -3
- package/dist/plugin-sdk/{chunk-DhDkBujV.js → chunk-BbrYSny_.js} +1 -1
- package/dist/plugin-sdk/{command-format-CVrYFyZS.js → command-format-BN6tyZt6.js} +1 -1
- package/dist/plugin-sdk/{commands-registry-17yfZkHZ.js → commands-registry-CTzKKtY6.js} +4 -4
- package/dist/plugin-sdk/{config-7wk65zKC.js → config-Crv2qEdJ.js} +9 -9
- package/dist/plugin-sdk/{consolidate-exbAW0ml.js → consolidate-DT1QH65Q.js} +2 -2
- package/dist/plugin-sdk/{deliver-TxAcw7J5.js → deliver-7rOvAlrc.js} +12 -12
- package/dist/plugin-sdk/{diagnostic-Debx4frd.js → diagnostic-0nsxhWp7.js} +1 -1
- package/dist/plugin-sdk/{fs-safe-wBYbAkJF.js → fs-safe-DfWYBeWF.js} +1 -1
- package/dist/plugin-sdk/{gemini-auth-7U2pm2Ky.js → gemini-auth-C0N0_u49.js} +1 -1
- package/dist/plugin-sdk/{image-BtDVmYA5.js → image-WOSl2apK.js} +4 -4
- package/dist/plugin-sdk/index.js +43 -43
- package/dist/plugin-sdk/{ir-CKMvRrGW.js → ir-9J84MTls.js} +4 -4
- package/dist/plugin-sdk/{local-roots-c_gaPs01.js → local-roots-OLRDbvyY.js} +3 -3
- package/dist/plugin-sdk/{login-DUym1Jy0.js → login-C7x4q0i2.js} +7 -7
- package/dist/plugin-sdk/{login-qr-B-WBdvrX.js → login-qr-Dv5_MoAW.js} +9 -9
- package/dist/plugin-sdk/{manager-B71SCzos.js → manager-C83tK17x.js} +8 -8
- package/dist/plugin-sdk/{manifest-registry-Dnic6Chh.js → manifest-registry-CJMV-PI7.js} +1 -1
- package/dist/plugin-sdk/{markdown-tables-Dur7OTlM.js → markdown-tables-DXNKz5y_.js} +1 -1
- package/dist/plugin-sdk/{message-channel-BrAhJJV_.js → message-channel-aGy1HbQQ.js} +1 -1
- package/dist/plugin-sdk/{model-selection-B9qaVQSJ.js → model-selection-C-3-tpe7.js} +4 -4
- package/dist/plugin-sdk/{outbound-DB1wDM8b.js → outbound-DquCeSy5.js} +6 -6
- package/dist/plugin-sdk/{pi-auth-json-ZO118hoy.js → pi-auth-json-D9PDCXGn.js} +1 -1
- package/dist/plugin-sdk/{pi-embedded-helpers-s_U0Un7j.js → pi-embedded-helpers-D3ygfH7l.js} +16 -16
- package/dist/plugin-sdk/{plugins-DF81oSaI.js → plugins-DOwnSg9D.js} +4 -4
- package/dist/plugin-sdk/{pw-ai-CTwP02uv.js → pw-ai-rlengLjb.js} +8 -8
- package/dist/plugin-sdk/{qmd-manager-CBaSGant.js → qmd-manager-BzxFjRFa.js} +4 -4
- package/dist/plugin-sdk/{registry-CZVURNhF.js → registry-5iFfixlB.js} +2 -2
- package/dist/plugin-sdk/{replies-hwRbkU3z.js → replies-BXOzO_H5.js} +7 -7
- package/dist/plugin-sdk/{reply-prefix-CaXmzZlx.js → reply-prefix-INAKTqCU.js} +1 -1
- package/dist/plugin-sdk/{resolve-outbound-target-fxVSOBmk.js → resolve-outbound-target-DvbxHtqp.js} +2 -2
- package/dist/plugin-sdk/{resolve-route-ClCyiOeu.js → resolve-route-URXlY3AK.js} +3 -3
- package/dist/plugin-sdk/{runner-Cq5jvwQ7.js → runner-Bv0_DWoH.js} +9 -9
- package/dist/plugin-sdk/{session-B_TkB65Y.js → session-C3r8l7ou.js} +4 -4
- package/dist/plugin-sdk/{skill-commands-0LF9HTGr.js → skill-commands-KjLUGIdZ.js} +5 -5
- package/dist/plugin-sdk/{skills-BIT_O7J0.js → skills-BrsD4L5c.js} +7 -7
- package/dist/plugin-sdk/{sqlite-Bx5Y5U5X.js → sqlite-CjW7ME1H.js} +1 -1
- package/dist/plugin-sdk/{subsystem-CXqYeDy-.js → subsystem-DcOg1xJr.js} +1 -1
- package/dist/plugin-sdk/{synthesis-DtsYAj1E.js → synthesis-CY7YAasV.js} +38 -38
- package/dist/plugin-sdk/{target-errors-B8mokOeH.js → target-errors-BVWJGWFq.js} +2 -2
- package/dist/plugin-sdk/{thinking-Ca0DhqzO.js → thinking-CtsTDPOi.js} +3 -3
- package/dist/plugin-sdk/{tokens-CvlONEqh.js → tokens-8lqOTZCB.js} +1 -1
- package/dist/plugin-sdk/{tool-images-DpBaWEHT.js → tool-images-Cl_rGIUZ.js} +2 -2
- package/dist/plugin-sdk/{tool-loop-detection-BOvUFa0f.js → tool-loop-detection-Da4WUT_P.js} +2 -2
- package/dist/plugin-sdk/{unified-runner-CnM7lyNd.js → unified-runner-nwMnsZyj.js} +60 -60
- package/dist/plugin-sdk/web-BlweOZDp.js +54 -0
- package/dist/plugin-sdk/{whatsapp-actions-CvnfsFJm.js → whatsapp-actions-DpfaGYs7.js} +21 -21
- package/dist/{pw-ai-BW8_KeDf.js → pw-ai-BqxJG-Wh.js} +1 -1
- package/dist/{pw-ai-j9IE1K0-.js → pw-ai-C-NSGye0.js} +1 -1
- package/dist/{runner-8ALr2UII.js → runner-COGFTeDw.js} +1 -1
- package/dist/{runner-C4-9kFdR.js → runner-DhCi2lT1.js} +1 -1
- package/dist/{synthesis-Cph3LhA1.js → synthesis-CXZu24Vx.js} +7 -7
- package/dist/{synthesis-Cus0A2dL.js → synthesis-DrPxcMlQ.js} +7 -7
- package/dist/{unified-runner-CX80YMTk.js → unified-runner-iByUazvW.js} +16 -16
- package/dist/{web-ChozvJ7I.js → web-EsMQBIYf.js} +7 -7
- package/dist/{web-DFlsbXmQ.js → web-PPg5y6xI.js} +7 -7
- package/package.json +1 -1
- package/dist/plugin-sdk/web-CIPJBHAU.js +0 -54
- 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
- package/skills/1password/SKILL.md +0 -71
- package/skills/1password/references/cli-examples.md +0 -29
- package/skills/1password/references/get-started.md +0 -17
- package/skills/apple-notes/SKILL.md +0 -78
- package/skills/apple-reminders/SKILL.md +0 -119
- package/skills/bear-notes/SKILL.md +0 -108
- package/skills/blogwatcher/SKILL.md +0 -70
- package/skills/blucli/SKILL.md +0 -48
- package/skills/bluebubbles/SKILL.md +0 -132
- package/skills/camsnap/SKILL.md +0 -46
- package/skills/canvas/SKILL.md +0 -204
- package/skills/connect-email/SKILL.md +0 -142
- package/skills/document-generation/SKILL.md +0 -83
- package/skills/eightctl/SKILL.md +0 -51
- package/skills/food-order/SKILL.md +0 -49
- package/skills/gemini/SKILL.md +0 -44
- package/skills/gh-issues/SKILL.md +0 -865
- package/skills/gifgrep/SKILL.md +0 -80
- package/skills/github/SKILL.md +0 -164
- package/skills/gog/SKILL.md +0 -117
- package/skills/goplaces/SKILL.md +0 -53
- package/skills/healthcheck/SKILL.md +0 -246
- package/skills/himalaya/SKILL.md +0 -258
- package/skills/himalaya/references/configuration.md +0 -184
- package/skills/himalaya/references/message-composition.md +0 -199
- package/skills/imsg/SKILL.md +0 -122
- package/skills/long-task/SKILL.md +0 -58
- package/skills/long-task/scripts/detach-task.sh +0 -187
- package/skills/nano-banana-pro/SKILL.md +0 -59
- package/skills/nano-banana-pro/scripts/generate_image.py +0 -184
- package/skills/nano-pdf/SKILL.md +0 -39
- package/skills/notion/SKILL.md +0 -173
- package/skills/obsidian/SKILL.md +0 -82
- package/skills/openai-image-gen/SKILL.md +0 -90
- package/skills/openai-image-gen/scripts/gen.py +0 -240
- package/skills/openai-whisper/SKILL.md +0 -39
- package/skills/openai-whisper-api/SKILL.md +0 -53
- package/skills/openai-whisper-api/scripts/transcribe.sh +0 -85
- package/skills/openhue/SKILL.md +0 -113
- package/skills/oracle/SKILL.md +0 -126
- package/skills/ordercli/SKILL.md +0 -79
- package/skills/peekaboo/SKILL.md +0 -191
- package/skills/reactions-extensive/SKILL.md +0 -30
- package/skills/reactions-minimal/SKILL.md +0 -31
- package/skills/safe-edit/SKILL.md +0 -51
- package/skills/sag/SKILL.md +0 -88
- package/skills/sherpa-onnx-tts/SKILL.md +0 -104
- package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +0 -178
- package/skills/songsee/SKILL.md +0 -50
- package/skills/sonoscli/SKILL.md +0 -66
- package/skills/spotify-player/SKILL.md +0 -65
- package/skills/symihub/SKILL.md +0 -78
- package/skills/things-mac/SKILL.md +0 -87
- package/skills/tmux/SKILL.md +0 -153
- package/skills/tmux/scripts/find-sessions.sh +0 -112
- package/skills/tmux/scripts/wait-for-text.sh +0 -83
- package/skills/trello/SKILL.md +0 -96
- package/skills/video-frames/SKILL.md +0 -47
- package/skills/video-frames/scripts/frame.sh +0 -81
- package/skills/voice-call/SKILL.md +0 -46
- package/skills/wacli/SKILL.md +0 -73
- package/skills/weather/SKILL.md +0 -113
- package/skills/xurl/SKILL.md +0 -462
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
emptyPluginConfigSchema,
|
|
3
|
-
type SymiPluginApi,
|
|
4
|
-
type ProviderAuthContext,
|
|
5
|
-
} from "symi/plugin-sdk";
|
|
6
|
-
import { loginQwenPortalOAuth } from "./oauth.js";
|
|
7
|
-
|
|
8
|
-
const PROVIDER_ID = "qwen-portal";
|
|
9
|
-
const PROVIDER_LABEL = "Qwen";
|
|
10
|
-
const DEFAULT_MODEL = "qwen-portal/coder-model";
|
|
11
|
-
const DEFAULT_BASE_URL = "https://portal.qwen.ai/v1";
|
|
12
|
-
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
13
|
-
const DEFAULT_MAX_TOKENS = 8192;
|
|
14
|
-
const OAUTH_PLACEHOLDER = "qwen-oauth";
|
|
15
|
-
|
|
16
|
-
function normalizeBaseUrl(value: string | undefined): string {
|
|
17
|
-
const raw = value?.trim() || DEFAULT_BASE_URL;
|
|
18
|
-
const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`;
|
|
19
|
-
return withProtocol.endsWith("/v1") ? withProtocol : `${withProtocol.replace(/\/+$/, "")}/v1`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildModelDefinition(params: {
|
|
23
|
-
id: string;
|
|
24
|
-
name: string;
|
|
25
|
-
input: Array<"text" | "image">;
|
|
26
|
-
}) {
|
|
27
|
-
return {
|
|
28
|
-
id: params.id,
|
|
29
|
-
name: params.name,
|
|
30
|
-
reasoning: false,
|
|
31
|
-
input: params.input,
|
|
32
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
33
|
-
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
34
|
-
maxTokens: DEFAULT_MAX_TOKENS,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const qwenPortalPlugin = {
|
|
39
|
-
id: "qwen-portal-auth",
|
|
40
|
-
name: "Qwen OAuth",
|
|
41
|
-
description: "OAuth flow for Qwen (free-tier) models",
|
|
42
|
-
configSchema: emptyPluginConfigSchema(),
|
|
43
|
-
register(api: SymiPluginApi) {
|
|
44
|
-
api.registerProvider({
|
|
45
|
-
id: PROVIDER_ID,
|
|
46
|
-
label: PROVIDER_LABEL,
|
|
47
|
-
docsPath: "/providers/qwen",
|
|
48
|
-
aliases: ["qwen"],
|
|
49
|
-
auth: [
|
|
50
|
-
{
|
|
51
|
-
id: "device",
|
|
52
|
-
label: "Qwen OAuth",
|
|
53
|
-
hint: "Device code login",
|
|
54
|
-
kind: "device_code",
|
|
55
|
-
run: async (ctx: ProviderAuthContext) => {
|
|
56
|
-
const progress = ctx.prompter.progress("Starting Qwen OAuth…");
|
|
57
|
-
try {
|
|
58
|
-
const result = await loginQwenPortalOAuth({
|
|
59
|
-
openUrl: ctx.openUrl,
|
|
60
|
-
note: ctx.prompter.note,
|
|
61
|
-
progress,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
progress.stop("Qwen OAuth complete");
|
|
65
|
-
|
|
66
|
-
const profileId = `${PROVIDER_ID}:default`;
|
|
67
|
-
const baseUrl = normalizeBaseUrl(result.resourceUrl);
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
profiles: [
|
|
71
|
-
{
|
|
72
|
-
profileId,
|
|
73
|
-
credential: {
|
|
74
|
-
type: "oauth",
|
|
75
|
-
provider: PROVIDER_ID,
|
|
76
|
-
access: result.access,
|
|
77
|
-
refresh: result.refresh,
|
|
78
|
-
expires: result.expires,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
configPatch: {
|
|
83
|
-
models: {
|
|
84
|
-
providers: {
|
|
85
|
-
[PROVIDER_ID]: {
|
|
86
|
-
baseUrl,
|
|
87
|
-
apiKey: OAUTH_PLACEHOLDER,
|
|
88
|
-
api: "openai-completions",
|
|
89
|
-
models: [
|
|
90
|
-
buildModelDefinition({
|
|
91
|
-
id: "coder-model",
|
|
92
|
-
name: "Qwen Coder",
|
|
93
|
-
input: ["text"],
|
|
94
|
-
}),
|
|
95
|
-
buildModelDefinition({
|
|
96
|
-
id: "vision-model",
|
|
97
|
-
name: "Qwen Vision",
|
|
98
|
-
input: ["text", "image"],
|
|
99
|
-
}),
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
agents: {
|
|
105
|
-
defaults: {
|
|
106
|
-
models: {
|
|
107
|
-
"qwen-portal/coder-model": { alias: "qwen" },
|
|
108
|
-
"qwen-portal/vision-model": {},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
defaultModel: DEFAULT_MODEL,
|
|
114
|
-
notes: [
|
|
115
|
-
"Qwen OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
|
|
116
|
-
`Base URL defaults to ${DEFAULT_BASE_URL}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`,
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
} catch (err) {
|
|
120
|
-
progress.stop("Qwen OAuth failed");
|
|
121
|
-
await ctx.prompter.note(
|
|
122
|
-
"If OAuth fails, verify your Qwen account has portal access and try again.",
|
|
123
|
-
"Qwen OAuth",
|
|
124
|
-
);
|
|
125
|
-
throw err;
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export default qwenPortalPlugin;
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
|
|
4
|
-
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
|
5
|
-
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
6
|
-
const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
7
|
-
const QWEN_OAUTH_SCOPE = "openid profile email model.completion";
|
|
8
|
-
const QWEN_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
9
|
-
|
|
10
|
-
export type QwenDeviceAuthorization = {
|
|
11
|
-
device_code: string;
|
|
12
|
-
user_code: string;
|
|
13
|
-
verification_uri: string;
|
|
14
|
-
verification_uri_complete?: string;
|
|
15
|
-
expires_in: number;
|
|
16
|
-
interval?: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type QwenOAuthToken = {
|
|
20
|
-
access: string;
|
|
21
|
-
refresh: string;
|
|
22
|
-
expires: number;
|
|
23
|
-
resourceUrl?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type TokenPending = { status: "pending"; slowDown?: boolean };
|
|
27
|
-
|
|
28
|
-
type DeviceTokenResult =
|
|
29
|
-
| { status: "success"; token: QwenOAuthToken }
|
|
30
|
-
| TokenPending
|
|
31
|
-
| { status: "error"; message: string };
|
|
32
|
-
|
|
33
|
-
function toFormUrlEncoded(data: Record<string, string>): string {
|
|
34
|
-
return Object.entries(data)
|
|
35
|
-
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
36
|
-
.join("&");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function generatePkce(): { verifier: string; challenge: string } {
|
|
40
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
41
|
-
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
42
|
-
return { verifier, challenge };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function requestDeviceCode(params: { challenge: string }): Promise<QwenDeviceAuthorization> {
|
|
46
|
-
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
50
|
-
Accept: "application/json",
|
|
51
|
-
"x-request-id": randomUUID(),
|
|
52
|
-
},
|
|
53
|
-
body: toFormUrlEncoded({
|
|
54
|
-
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
55
|
-
scope: QWEN_OAUTH_SCOPE,
|
|
56
|
-
code_challenge: params.challenge,
|
|
57
|
-
code_challenge_method: "S256",
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
const text = await response.text();
|
|
63
|
-
throw new Error(`Qwen device authorization failed: ${text || response.statusText}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const payload = (await response.json()) as QwenDeviceAuthorization & { error?: string };
|
|
67
|
-
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
payload.error ??
|
|
70
|
-
"Qwen device authorization returned an incomplete payload (missing user_code or verification_uri).",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
return payload;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function pollDeviceToken(params: {
|
|
77
|
-
deviceCode: string;
|
|
78
|
-
verifier: string;
|
|
79
|
-
}): Promise<DeviceTokenResult> {
|
|
80
|
-
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
81
|
-
method: "POST",
|
|
82
|
-
headers: {
|
|
83
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
84
|
-
Accept: "application/json",
|
|
85
|
-
},
|
|
86
|
-
body: toFormUrlEncoded({
|
|
87
|
-
grant_type: QWEN_OAUTH_GRANT_TYPE,
|
|
88
|
-
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
89
|
-
device_code: params.deviceCode,
|
|
90
|
-
code_verifier: params.verifier,
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
let payload: { error?: string; error_description?: string } | undefined;
|
|
96
|
-
try {
|
|
97
|
-
payload = (await response.json()) as { error?: string; error_description?: string };
|
|
98
|
-
} catch {
|
|
99
|
-
const text = await response.text();
|
|
100
|
-
return { status: "error", message: text || response.statusText };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (payload?.error === "authorization_pending") {
|
|
104
|
-
return { status: "pending" };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (payload?.error === "slow_down") {
|
|
108
|
-
return { status: "pending", slowDown: true };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
status: "error",
|
|
113
|
-
message: payload?.error_description || payload?.error || response.statusText,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const tokenPayload = (await response.json()) as {
|
|
118
|
-
access_token?: string | null;
|
|
119
|
-
refresh_token?: string | null;
|
|
120
|
-
expires_in?: number | null;
|
|
121
|
-
token_type?: string;
|
|
122
|
-
resource_url?: string;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
if (!tokenPayload.access_token || !tokenPayload.refresh_token || !tokenPayload.expires_in) {
|
|
126
|
-
return { status: "error", message: "Qwen OAuth returned incomplete token payload." };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
status: "success",
|
|
131
|
-
token: {
|
|
132
|
-
access: tokenPayload.access_token,
|
|
133
|
-
refresh: tokenPayload.refresh_token,
|
|
134
|
-
expires: Date.now() + tokenPayload.expires_in * 1000,
|
|
135
|
-
resourceUrl: tokenPayload.resource_url,
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export async function loginQwenPortalOAuth(params: {
|
|
141
|
-
openUrl: (url: string) => Promise<void>;
|
|
142
|
-
note: (message: string, title?: string) => Promise<void>;
|
|
143
|
-
progress: { update: (message: string) => void; stop: (message?: string) => void };
|
|
144
|
-
}): Promise<QwenOAuthToken> {
|
|
145
|
-
const { verifier, challenge } = generatePkce();
|
|
146
|
-
const device = await requestDeviceCode({ challenge });
|
|
147
|
-
const verificationUrl = device.verification_uri_complete || device.verification_uri;
|
|
148
|
-
|
|
149
|
-
await params.note(
|
|
150
|
-
[
|
|
151
|
-
`Open ${verificationUrl} to approve access.`,
|
|
152
|
-
`If prompted, enter the code ${device.user_code}.`,
|
|
153
|
-
].join("\n"),
|
|
154
|
-
"Qwen OAuth",
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
await params.openUrl(verificationUrl);
|
|
159
|
-
} catch {
|
|
160
|
-
// Fall back to manual copy/paste if browser open fails.
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const start = Date.now();
|
|
164
|
-
let pollIntervalMs = device.interval ? device.interval * 1000 : 2000;
|
|
165
|
-
const timeoutMs = device.expires_in * 1000;
|
|
166
|
-
|
|
167
|
-
while (Date.now() - start < timeoutMs) {
|
|
168
|
-
params.progress.update("Waiting for Qwen OAuth approval…");
|
|
169
|
-
const result = await pollDeviceToken({
|
|
170
|
-
deviceCode: device.device_code,
|
|
171
|
-
verifier,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (result.status === "success") {
|
|
175
|
-
return result.token;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (result.status === "error") {
|
|
179
|
-
throw new Error(`Qwen OAuth failed: ${result.message}`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (result.status === "pending" && result.slowDown) {
|
|
183
|
-
pollIntervalMs = Math.min(pollIntervalMs * 1.5, 10000);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
throw new Error("Qwen OAuth timed out waiting for authorization.");
|
|
190
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type { SymiPluginApi } from "symi/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
type ElevenLabsVoice = {
|
|
4
|
-
voice_id: string;
|
|
5
|
-
name?: string;
|
|
6
|
-
category?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
function mask(s: string, keep: number = 6): string {
|
|
11
|
-
const trimmed = s.trim();
|
|
12
|
-
if (trimmed.length <= keep) {
|
|
13
|
-
return "***";
|
|
14
|
-
}
|
|
15
|
-
return `${trimmed.slice(0, keep)}…`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isLikelyVoiceId(value: string): boolean {
|
|
19
|
-
const v = value.trim();
|
|
20
|
-
if (v.length < 10 || v.length > 64) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
return /^[a-zA-Z0-9_-]+$/.test(v);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function listVoices(apiKey: string): Promise<ElevenLabsVoice[]> {
|
|
27
|
-
const res = await fetch("https://api.elevenlabs.io/v1/voices", {
|
|
28
|
-
headers: {
|
|
29
|
-
"xi-api-key": apiKey,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
throw new Error(`ElevenLabs voices API error (${res.status})`);
|
|
34
|
-
}
|
|
35
|
-
const json = (await res.json()) as { voices?: ElevenLabsVoice[] };
|
|
36
|
-
return Array.isArray(json.voices) ? json.voices : [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatVoiceList(voices: ElevenLabsVoice[], limit: number): string {
|
|
40
|
-
const sliced = voices.slice(0, Math.max(1, Math.min(limit, 50)));
|
|
41
|
-
const lines: string[] = [];
|
|
42
|
-
lines.push(`Voices: ${voices.length}`);
|
|
43
|
-
lines.push("");
|
|
44
|
-
for (const v of sliced) {
|
|
45
|
-
const name = (v.name ?? "").trim() || "(unnamed)";
|
|
46
|
-
const category = (v.category ?? "").trim();
|
|
47
|
-
const meta = category ? ` · ${category}` : "";
|
|
48
|
-
lines.push(`- ${name}${meta}`);
|
|
49
|
-
lines.push(` id: ${v.voice_id}`);
|
|
50
|
-
}
|
|
51
|
-
if (voices.length > sliced.length) {
|
|
52
|
-
lines.push("");
|
|
53
|
-
lines.push(`(showing first ${sliced.length})`);
|
|
54
|
-
}
|
|
55
|
-
return lines.join("\n");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function findVoice(voices: ElevenLabsVoice[], query: string): ElevenLabsVoice | null {
|
|
59
|
-
const q = query.trim();
|
|
60
|
-
if (!q) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const lower = q.toLowerCase();
|
|
64
|
-
const byId = voices.find((v) => v.voice_id === q);
|
|
65
|
-
if (byId) {
|
|
66
|
-
return byId;
|
|
67
|
-
}
|
|
68
|
-
const exactName = voices.find((v) => (v.name ?? "").trim().toLowerCase() === lower);
|
|
69
|
-
if (exactName) {
|
|
70
|
-
return exactName;
|
|
71
|
-
}
|
|
72
|
-
const partial = voices.find((v) => (v.name ?? "").trim().toLowerCase().includes(lower));
|
|
73
|
-
return partial ?? null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export default function register(api: SymiPluginApi) {
|
|
77
|
-
api.registerCommand({
|
|
78
|
-
name: "voice",
|
|
79
|
-
description: "List/set ElevenLabs Talk voice (affects iOS Talk playback).",
|
|
80
|
-
acceptsArgs: true,
|
|
81
|
-
handler: async (ctx) => {
|
|
82
|
-
const args = ctx.args?.trim() ?? "";
|
|
83
|
-
const tokens = args.split(/\s+/).filter(Boolean);
|
|
84
|
-
const action = (tokens[0] ?? "status").toLowerCase();
|
|
85
|
-
|
|
86
|
-
const cfg = api.runtime.config.loadConfig();
|
|
87
|
-
const apiKey = (cfg.talk?.apiKey ?? "").trim();
|
|
88
|
-
if (!apiKey) {
|
|
89
|
-
return {
|
|
90
|
-
text:
|
|
91
|
-
"Talk voice is not configured.\n\n" +
|
|
92
|
-
"Missing: talk.apiKey (ElevenLabs API key).\n" +
|
|
93
|
-
"Set it on the gateway, then retry.",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const currentVoiceId = (cfg.talk?.voiceId ?? "").trim();
|
|
98
|
-
|
|
99
|
-
if (action === "status") {
|
|
100
|
-
return {
|
|
101
|
-
text:
|
|
102
|
-
"Talk voice status:\n" +
|
|
103
|
-
`- talk.voiceId: ${currentVoiceId ? currentVoiceId : "(unset)"}\n` +
|
|
104
|
-
`- talk.apiKey: ${mask(apiKey)}`,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (action === "list") {
|
|
109
|
-
const limit = Number.parseInt(tokens[1] ?? "12", 10);
|
|
110
|
-
const voices = await listVoices(apiKey);
|
|
111
|
-
return { text: formatVoiceList(voices, Number.isFinite(limit) ? limit : 12) };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (action === "set") {
|
|
115
|
-
const query = tokens.slice(1).join(" ").trim();
|
|
116
|
-
if (!query) {
|
|
117
|
-
return { text: "Usage: /voice set <voiceId|name>" };
|
|
118
|
-
}
|
|
119
|
-
const voices = await listVoices(apiKey);
|
|
120
|
-
const chosen = findVoice(voices, query);
|
|
121
|
-
if (!chosen) {
|
|
122
|
-
const hint = isLikelyVoiceId(query) ? query : `"${query}"`;
|
|
123
|
-
return { text: `No voice found for ${hint}. Try: /voice list` };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const nextConfig = {
|
|
127
|
-
...cfg,
|
|
128
|
-
talk: {
|
|
129
|
-
...cfg.talk,
|
|
130
|
-
voiceId: chosen.voice_id,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
await api.runtime.config.writeConfigFile(nextConfig);
|
|
134
|
-
|
|
135
|
-
const name = (chosen.name ?? "").trim() || "(unnamed)";
|
|
136
|
-
return { text: `✅ Talk voice set to ${name}\n${chosen.voice_id}` };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
text: [
|
|
141
|
-
"Voice commands:",
|
|
142
|
-
"",
|
|
143
|
-
"/voice status",
|
|
144
|
-
"/voice list [limit]",
|
|
145
|
-
"/voice set <voiceId|name>",
|
|
146
|
-
].join("\n"),
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import register from "./index.js";
|
|
3
|
-
|
|
4
|
-
describe("thread-ownership plugin", () => {
|
|
5
|
-
const hooks: Record<string, Function> = {};
|
|
6
|
-
const api = {
|
|
7
|
-
pluginConfig: {},
|
|
8
|
-
config: {
|
|
9
|
-
agents: {
|
|
10
|
-
list: [{ id: "test-agent", default: true, identity: { name: "TestBot" } }],
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
id: "thread-ownership",
|
|
14
|
-
name: "Thread Ownership",
|
|
15
|
-
logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
|
16
|
-
on: vi.fn((hookName: string, handler: Function) => {
|
|
17
|
-
hooks[hookName] = handler;
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
let originalFetch: typeof globalThis.fetch;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.clearAllMocks();
|
|
25
|
-
for (const key of Object.keys(hooks)) delete hooks[key];
|
|
26
|
-
|
|
27
|
-
process.env.SLACK_FORWARDER_URL = "http://localhost:8750";
|
|
28
|
-
process.env.SLACK_BOT_USER_ID = "U999";
|
|
29
|
-
|
|
30
|
-
originalFetch = globalThis.fetch;
|
|
31
|
-
globalThis.fetch = vi.fn() as unknown as typeof globalThis.fetch;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
globalThis.fetch = originalFetch;
|
|
36
|
-
delete process.env.SLACK_FORWARDER_URL;
|
|
37
|
-
delete process.env.SLACK_BOT_USER_ID;
|
|
38
|
-
vi.restoreAllMocks();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("registers message_received and message_sending hooks", () => {
|
|
42
|
-
register(api as any);
|
|
43
|
-
|
|
44
|
-
expect(api.on).toHaveBeenCalledTimes(2);
|
|
45
|
-
expect(api.on).toHaveBeenCalledWith("message_received", expect.any(Function));
|
|
46
|
-
expect(api.on).toHaveBeenCalledWith("message_sending", expect.any(Function));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("message_sending", () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
register(api as any);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("allows non-slack channels", async () => {
|
|
55
|
-
const result = await hooks.message_sending(
|
|
56
|
-
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
|
|
57
|
-
{ channelId: "telegram", conversationId: "C123" },
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
expect(result).toBeUndefined();
|
|
61
|
-
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("allows top-level messages (no threadTs)", async () => {
|
|
65
|
-
const result = await hooks.message_sending(
|
|
66
|
-
{ content: "hello", metadata: {}, to: "C123" },
|
|
67
|
-
{ channelId: "slack", conversationId: "C123" },
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
expect(result).toBeUndefined();
|
|
71
|
-
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("claims ownership successfully", async () => {
|
|
75
|
-
vi.mocked(globalThis.fetch).mockResolvedValue(
|
|
76
|
-
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const result = await hooks.message_sending(
|
|
80
|
-
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
|
|
81
|
-
{ channelId: "slack", conversationId: "C123" },
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
expect(result).toBeUndefined();
|
|
85
|
-
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
86
|
-
"http://localhost:8750/api/v1/ownership/C123/1234.5678",
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
method: "POST",
|
|
89
|
-
body: JSON.stringify({ agent_id: "test-agent" }),
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("cancels when thread owned by another agent", async () => {
|
|
95
|
-
vi.mocked(globalThis.fetch).mockResolvedValue(
|
|
96
|
-
new Response(JSON.stringify({ owner: "other-agent" }), { status: 409 }),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const result = await hooks.message_sending(
|
|
100
|
-
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
|
|
101
|
-
{ channelId: "slack", conversationId: "C123" },
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
expect(result).toEqual({ cancel: true });
|
|
105
|
-
expect(api.logger.info).toHaveBeenCalledWith(expect.stringContaining("cancelled send"));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("fails open on network error", async () => {
|
|
109
|
-
vi.mocked(globalThis.fetch).mockRejectedValue(new Error("ECONNREFUSED"));
|
|
110
|
-
|
|
111
|
-
const result = await hooks.message_sending(
|
|
112
|
-
{ content: "hello", metadata: { threadTs: "1234.5678", channelId: "C123" }, to: "C123" },
|
|
113
|
-
{ channelId: "slack", conversationId: "C123" },
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
expect(result).toBeUndefined();
|
|
117
|
-
expect(api.logger.warn).toHaveBeenCalledWith(
|
|
118
|
-
expect.stringContaining("ownership check failed"),
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("message_received @-mention tracking", () => {
|
|
124
|
-
beforeEach(() => {
|
|
125
|
-
register(api as any);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("tracks @-mentions and skips ownership check for mentioned threads", async () => {
|
|
129
|
-
// Simulate receiving a message that @-mentions the agent.
|
|
130
|
-
await hooks.message_received(
|
|
131
|
-
{ content: "Hey @TestBot help me", metadata: { threadTs: "9999.0001", channelId: "C456" } },
|
|
132
|
-
{ channelId: "slack", conversationId: "C456" },
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Now send in the same thread -- should skip the ownership HTTP call.
|
|
136
|
-
const result = await hooks.message_sending(
|
|
137
|
-
{ content: "Sure!", metadata: { threadTs: "9999.0001", channelId: "C456" }, to: "C456" },
|
|
138
|
-
{ channelId: "slack", conversationId: "C456" },
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
expect(result).toBeUndefined();
|
|
142
|
-
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("ignores @-mentions on non-slack channels", async () => {
|
|
146
|
-
// Use a unique thread key so module-level state from other tests doesn't interfere.
|
|
147
|
-
await hooks.message_received(
|
|
148
|
-
{ content: "Hey @TestBot", metadata: { threadTs: "7777.0001", channelId: "C999" } },
|
|
149
|
-
{ channelId: "telegram", conversationId: "C999" },
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// The mention should not have been tracked, so sending should still call fetch.
|
|
153
|
-
vi.mocked(globalThis.fetch).mockResolvedValue(
|
|
154
|
-
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
await hooks.message_sending(
|
|
158
|
-
{ content: "Sure!", metadata: { threadTs: "7777.0001", channelId: "C999" }, to: "C999" },
|
|
159
|
-
{ channelId: "slack", conversationId: "C999" },
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
expect(globalThis.fetch).toHaveBeenCalled();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("tracks bot user ID mentions via <@U999> syntax", async () => {
|
|
166
|
-
await hooks.message_received(
|
|
167
|
-
{ content: "Hey <@U999> help", metadata: { threadTs: "8888.0001", channelId: "C789" } },
|
|
168
|
-
{ channelId: "slack", conversationId: "C789" },
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const result = await hooks.message_sending(
|
|
172
|
-
{ content: "On it!", metadata: { threadTs: "8888.0001", channelId: "C789" }, to: "C789" },
|
|
173
|
-
{ channelId: "slack", conversationId: "C789" },
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(result).toBeUndefined();
|
|
177
|
-
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|