@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.
Files changed (116) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  3. package/package.json +1 -1
  4. package/extensions/copilot-proxy/README.md +0 -24
  5. package/extensions/copilot-proxy/index.ts +0 -154
  6. package/extensions/copilot-proxy/node_modules/.bin/symi +0 -21
  7. package/extensions/copilot-proxy/package.json +0 -15
  8. package/extensions/copilot-proxy/symi.plugin.json +0 -9
  9. package/extensions/device-pair/index.ts +0 -642
  10. package/extensions/device-pair/symi.plugin.json +0 -20
  11. package/extensions/diagnostics-otel/index.ts +0 -15
  12. package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
  13. package/extensions/diagnostics-otel/node_modules/.bin/symi +0 -21
  14. package/extensions/diagnostics-otel/package.json +0 -27
  15. package/extensions/diagnostics-otel/src/service.test.ts +0 -290
  16. package/extensions/diagnostics-otel/src/service.ts +0 -666
  17. package/extensions/diagnostics-otel/symi.plugin.json +0 -8
  18. package/extensions/google-antigravity-auth/README.md +0 -24
  19. package/extensions/google-antigravity-auth/index.ts +0 -424
  20. package/extensions/google-antigravity-auth/node_modules/.bin/symi +0 -21
  21. package/extensions/google-antigravity-auth/package.json +0 -15
  22. package/extensions/google-antigravity-auth/symi.plugin.json +0 -9
  23. package/extensions/google-gemini-cli-auth/README.md +0 -35
  24. package/extensions/google-gemini-cli-auth/index.ts +0 -75
  25. package/extensions/google-gemini-cli-auth/node_modules/.bin/symi +0 -21
  26. package/extensions/google-gemini-cli-auth/oauth.test.ts +0 -162
  27. package/extensions/google-gemini-cli-auth/oauth.ts +0 -636
  28. package/extensions/google-gemini-cli-auth/package.json +0 -15
  29. package/extensions/google-gemini-cli-auth/symi.plugin.json +0 -9
  30. package/extensions/learning-loop/index.ts +0 -159
  31. package/extensions/learning-loop/node_modules/.bin/symi +0 -21
  32. package/extensions/learning-loop/package.json +0 -18
  33. package/extensions/learning-loop/src/analytics/gateway-methods.ts +0 -230
  34. package/extensions/learning-loop/src/analytics/metrics-aggregator.ts +0 -153
  35. package/extensions/learning-loop/src/capture/run-tracker.ts +0 -181
  36. package/extensions/learning-loop/src/capture/serializer.ts +0 -74
  37. package/extensions/learning-loop/src/db.ts +0 -583
  38. package/extensions/learning-loop/src/feedback/explicit-feedback.ts +0 -58
  39. package/extensions/learning-loop/src/feedback/implicit-signals.ts +0 -89
  40. package/extensions/learning-loop/src/graph/edge-inference.ts +0 -189
  41. package/extensions/learning-loop/src/graph/graph-retrieval.ts +0 -144
  42. package/extensions/learning-loop/src/graph/graph-store.ts +0 -183
  43. package/extensions/learning-loop/src/hooks.ts +0 -244
  44. package/extensions/learning-loop/src/injection/cache.ts +0 -73
  45. package/extensions/learning-loop/src/injection/context-injector.ts +0 -104
  46. package/extensions/learning-loop/src/injection/prompt-builder.ts +0 -43
  47. package/extensions/learning-loop/src/learning/embedding-bridge.ts +0 -54
  48. package/extensions/learning-loop/src/learning/learning-extractor.ts +0 -217
  49. package/extensions/learning-loop/src/learning/learning-store.ts +0 -158
  50. package/extensions/learning-loop/src/learning/retrieval.ts +0 -87
  51. package/extensions/learning-loop/src/math/confidence-intervals.ts +0 -62
  52. package/extensions/learning-loop/src/math/ewma.ts +0 -51
  53. package/extensions/learning-loop/src/math/weighted-scorer.ts +0 -42
  54. package/extensions/learning-loop/src/schema.ts +0 -176
  55. package/extensions/learning-loop/src/scoring/normalization.ts +0 -32
  56. package/extensions/learning-loop/src/scoring/quality-engine.ts +0 -78
  57. package/extensions/learning-loop/src/scoring/signal-extractors.ts +0 -155
  58. package/extensions/learning-loop/src/test/context-injector.test.ts +0 -142
  59. package/extensions/learning-loop/src/test/fixes.test.ts +0 -1286
  60. package/extensions/learning-loop/src/test/graph.test.ts +0 -711
  61. package/extensions/learning-loop/src/test/integration.test.ts +0 -312
  62. package/extensions/learning-loop/src/test/learning-store.test.ts +0 -191
  63. package/extensions/learning-loop/src/test/math.test.ts +0 -148
  64. package/extensions/learning-loop/src/test/quality-engine.test.ts +0 -231
  65. package/extensions/learning-loop/src/test/run-tracker.test.ts +0 -143
  66. package/extensions/learning-loop/src/types.ts +0 -281
  67. package/extensions/learning-loop/symi.plugin.json +0 -46
  68. package/extensions/llm-task/README.md +0 -97
  69. package/extensions/llm-task/index.ts +0 -6
  70. package/extensions/llm-task/package.json +0 -12
  71. package/extensions/llm-task/src/llm-task-tool.test.ts +0 -138
  72. package/extensions/llm-task/src/llm-task-tool.ts +0 -249
  73. package/extensions/llm-task/symi.plugin.json +0 -21
  74. package/extensions/memory-lancedb/config.ts +0 -161
  75. package/extensions/memory-lancedb/index.test.ts +0 -330
  76. package/extensions/memory-lancedb/index.ts +0 -670
  77. package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
  78. package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
  79. package/extensions/memory-lancedb/node_modules/.bin/symi +0 -21
  80. package/extensions/memory-lancedb/package.json +0 -20
  81. package/extensions/memory-lancedb/symi.plugin.json +0 -71
  82. package/extensions/minimax-portal-auth/README.md +0 -33
  83. package/extensions/minimax-portal-auth/index.ts +0 -161
  84. package/extensions/minimax-portal-auth/node_modules/.bin/symi +0 -21
  85. package/extensions/minimax-portal-auth/oauth.ts +0 -247
  86. package/extensions/minimax-portal-auth/package.json +0 -15
  87. package/extensions/minimax-portal-auth/symi.plugin.json +0 -9
  88. package/extensions/model-equalizer/index.ts +0 -80
  89. package/extensions/model-equalizer/skills/model-equalizer/SKILL.md +0 -58
  90. package/extensions/model-equalizer/src/detection.ts +0 -62
  91. package/extensions/model-equalizer/src/enhancer.ts +0 -63
  92. package/extensions/model-equalizer/src/test/detection.test.ts +0 -218
  93. package/extensions/model-equalizer/src/test/enhancer.test.ts +0 -137
  94. package/extensions/model-equalizer/src/test/integration.test.ts +0 -185
  95. package/extensions/model-equalizer/src/types.ts +0 -24
  96. package/extensions/model-equalizer/symi.plugin.json +0 -12
  97. package/extensions/phone-control/index.ts +0 -421
  98. package/extensions/phone-control/symi.plugin.json +0 -10
  99. package/extensions/pipeline/README.md +0 -75
  100. package/extensions/pipeline/SKILL.md +0 -97
  101. package/extensions/pipeline/index.ts +0 -18
  102. package/extensions/pipeline/package.json +0 -11
  103. package/extensions/pipeline/src/pipeline-tool.test.ts +0 -345
  104. package/extensions/pipeline/src/pipeline-tool.ts +0 -266
  105. package/extensions/pipeline/src/windows-spawn.test.ts +0 -148
  106. package/extensions/pipeline/src/windows-spawn.ts +0 -193
  107. package/extensions/pipeline/symi.plugin.json +0 -10
  108. package/extensions/qwen-portal-auth/README.md +0 -24
  109. package/extensions/qwen-portal-auth/index.ts +0 -134
  110. package/extensions/qwen-portal-auth/oauth.ts +0 -190
  111. package/extensions/qwen-portal-auth/symi.plugin.json +0 -9
  112. package/extensions/talk-voice/index.ts +0 -150
  113. package/extensions/talk-voice/symi.plugin.json +0 -10
  114. package/extensions/thread-ownership/index.test.ts +0 -180
  115. package/extensions/thread-ownership/index.ts +0 -133
  116. package/extensions/thread-ownership/symi.plugin.json +0 -28
@@ -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,9 +0,0 @@
1
- {
2
- "id": "qwen-portal-auth",
3
- "providers": ["qwen-portal"],
4
- "configSchema": {
5
- "type": "object",
6
- "additionalProperties": false,
7
- "properties": {}
8
- }
9
- }
@@ -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,10 +0,0 @@
1
- {
2
- "id": "talk-voice",
3
- "name": "Talk Voice",
4
- "description": "Manage Talk voice selection (list/set).",
5
- "configSchema": {
6
- "type": "object",
7
- "additionalProperties": false,
8
- "properties": {}
9
- }
10
- }
@@ -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
- });