@soundchain/openclaw-plugin 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/messaging.ts DELETED
@@ -1,309 +0,0 @@
1
- /**
2
- * SoundChain Messaging Client
3
- *
4
- * GraphQL client for SoundChain DM operations.
5
- * Used by the channel plugin for outbound messaging
6
- * and inbound message polling.
7
- *
8
- * CRITICAL: `toId` must be a PROFILE ID, not a chat ID.
9
- * Using chat.id instead of chat.profile.id causes blank page crash
10
- * (documented Bug #17 in CLAUDE.md).
11
- */
12
-
13
- // ---------------------------------------------------------------------------
14
- // Types
15
- // ---------------------------------------------------------------------------
16
-
17
- export interface ChatMessage {
18
- id: string;
19
- message: string;
20
- fromId?: string;
21
- toId?: string;
22
- createdAt: string;
23
- fromProfile?: { id: string; displayName?: string; handle?: string };
24
- }
25
-
26
- export interface Chat {
27
- id: string;
28
- message: string;
29
- fromId?: string;
30
- unread?: boolean;
31
- createdAt: string;
32
- profile?: {
33
- id: string;
34
- displayName?: string;
35
- };
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // GraphQL transport
40
- // ---------------------------------------------------------------------------
41
-
42
- interface GraphQLResponse {
43
- data?: Record<string, unknown>;
44
- errors?: Array<{ message: string }>;
45
- }
46
-
47
- async function graphql(
48
- apiUrl: string,
49
- token: string,
50
- query: string,
51
- variables?: Record<string, unknown>,
52
- ): Promise<Record<string, unknown>> {
53
- const res = await fetch(apiUrl, {
54
- method: "POST",
55
- headers: {
56
- "Content-Type": "application/json",
57
- Authorization: `Bearer ${token}`,
58
- },
59
- body: JSON.stringify({ query, variables }),
60
- signal: AbortSignal.timeout(10_000),
61
- });
62
-
63
- if (!res.ok) {
64
- throw new Error(`SoundChain GraphQL HTTP error ${res.status}: ${res.statusText}`);
65
- }
66
- const json = (await res.json()) as GraphQLResponse;
67
-
68
- if (json.errors && json.errors.length > 0) {
69
- throw new Error(`SoundChain GraphQL: ${json.errors[0].message}`);
70
- }
71
-
72
- return json.data ?? {};
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // Queries & Mutations
77
- // ---------------------------------------------------------------------------
78
-
79
- const CHATS_QUERY = `
80
- query Chats {
81
- chats(page: { first: 50 }) {
82
- nodes {
83
- id
84
- message
85
- fromId
86
- unread
87
- createdAt
88
- profile {
89
- id
90
- displayName
91
- }
92
- }
93
- }
94
- }
95
- `;
96
-
97
- const SEND_MESSAGE_MUTATION = `
98
- mutation SendMessage($toId: String!, $message: String!) {
99
- sendMessage(input: { toId: $toId, message: $message }) {
100
- message {
101
- id
102
- fromId
103
- toId
104
- message
105
- createdAt
106
- }
107
- }
108
- }
109
- `;
110
-
111
- const UNREAD_COUNT_QUERY = `
112
- query UnreadMessageCount {
113
- unreadMessageCount
114
- }
115
- `;
116
-
117
- const EXPLORE_USERS_QUERY = `
118
- query ExploreUsers($page: PageInput) {
119
- exploreUsers(page: $page) {
120
- nodes {
121
- id
122
- userHandle
123
- displayName
124
- isFollowed
125
- }
126
- pageInfo {
127
- hasNextPage
128
- endCursor
129
- }
130
- }
131
- }
132
- `;
133
-
134
- const FOLLOW_PROFILE_MUTATION = `
135
- mutation FollowProfile($input: FollowProfileInput!) {
136
- followProfile(input: $input) {
137
- followedProfile {
138
- id
139
- userHandle
140
- isFollowed
141
- }
142
- }
143
- }
144
- `;
145
-
146
- // ---------------------------------------------------------------------------
147
- // Client
148
- // ---------------------------------------------------------------------------
149
-
150
- export interface UserProfile {
151
- id: string;
152
- userHandle?: string;
153
- displayName?: string;
154
- isFollowed?: boolean;
155
- }
156
-
157
- export interface MessagingClient {
158
- getChats(): Promise<Chat[]>;
159
- sendMessage(toProfileId: string, message: string): Promise<ChatMessage>;
160
- getUnreadCount(): Promise<number>;
161
- getAllUsers(): Promise<UserProfile[]>;
162
- followUser(profileId: string): Promise<void>;
163
- }
164
-
165
- /**
166
- * Dual-mode client: tries Vercel REST API first (always available),
167
- * falls back to Lambda GraphQL if REST fails.
168
- */
169
- export function createMessagingClient(apiUrl: string, token: string): MessagingClient {
170
- // Derive REST base URL from the configured apiUrl
171
- // e.g. "https://api.soundchain.io/graphql" → "https://soundchain.io"
172
- // e.g. "https://staging.soundchain.io/graphql" → "https://staging.soundchain.io"
173
- let baseUrl: string;
174
- try {
175
- const parsed = new URL(apiUrl);
176
- // Strip "api." prefix — REST routes live on the main domain (soundchain.io)
177
- const host = parsed.hostname.replace(/^api\./, "");
178
- const port = parsed.port ? `:${parsed.port}` : "";
179
- baseUrl = `${parsed.protocol}//${host}${port}`;
180
- } catch {
181
- throw new Error(
182
- `Invalid SoundChain apiUrl: "${apiUrl}". Expected a valid URL like "https://soundchain.io" or "https://api.soundchain.io/graphql".`,
183
- );
184
- }
185
-
186
- const REST_TIMEOUT_MS = 10_000;
187
-
188
- async function restGet(path: string): Promise<Record<string, unknown>> {
189
- const res = await fetch(`${baseUrl}${path}`, {
190
- headers: { Authorization: `Bearer ${token}` },
191
- signal: AbortSignal.timeout(REST_TIMEOUT_MS),
192
- });
193
- if (!res.ok) throw new Error(`REST ${path}: ${res.status}`);
194
- return (await res.json()) as Record<string, unknown>;
195
- }
196
-
197
- async function restPost(
198
- path: string,
199
- body: Record<string, unknown>,
200
- ): Promise<Record<string, unknown>> {
201
- const res = await fetch(`${baseUrl}${path}`, {
202
- method: "POST",
203
- headers: {
204
- "Content-Type": "application/json",
205
- Authorization: `Bearer ${token}`,
206
- },
207
- body: JSON.stringify(body),
208
- signal: AbortSignal.timeout(REST_TIMEOUT_MS),
209
- });
210
- if (!res.ok) throw new Error(`REST ${path}: ${res.status}`);
211
- return (await res.json()) as Record<string, unknown>;
212
- }
213
-
214
- return {
215
- async getChats(): Promise<Chat[]> {
216
- try {
217
- const data = await restGet("/api/pulse/chats");
218
- return (data.chats as Chat[]) ?? [];
219
- } catch {
220
- // Fallback to GraphQL
221
- const data = await graphql(apiUrl, token, CHATS_QUERY);
222
- const connection = data.chats as { nodes?: Chat[] } | undefined;
223
- return connection?.nodes ?? [];
224
- }
225
- },
226
-
227
- async sendMessage(toProfileId: string, message: string): Promise<ChatMessage> {
228
- // Try REST first; only fall back to GraphQL on connection/network errors
229
- // (NOT on HTTP errors where the server may have already processed the message)
230
- try {
231
- const data = await restPost("/api/pulse/send", { toId: toProfileId, message });
232
- const result = data.message as ChatMessage | undefined;
233
- if (!result) throw new Error("sendMessage returned null");
234
- return result;
235
- } catch (err: unknown) {
236
- const isNetworkError =
237
- err instanceof TypeError || // fetch network failure
238
- (err instanceof DOMException &&
239
- (err.name === "AbortError" || err.name === "TimeoutError")); // timeout (Node uses TimeoutError)
240
- if (!isNetworkError) throw err; // Server responded — do NOT retry (avoids duplicate DMs)
241
-
242
- // Network failure — safe to retry via GraphQL since REST never reached the server
243
- const data = await graphql(apiUrl, token, SEND_MESSAGE_MUTATION, {
244
- toId: toProfileId,
245
- message,
246
- });
247
- const payload = data.sendMessage as { message?: ChatMessage } | undefined;
248
- const result = payload?.message;
249
- if (!result) throw new Error("sendMessage returned null");
250
- return result;
251
- }
252
- },
253
-
254
- async getUnreadCount(): Promise<number> {
255
- try {
256
- const data = await restGet("/api/pulse/chats");
257
- const chats = (data.chats as Chat[]) ?? [];
258
- return chats.filter((c) => c.unread).length;
259
- } catch {
260
- const data = await graphql(apiUrl, token, UNREAD_COUNT_QUERY);
261
- return (data.unreadMessageCount as number | undefined) ?? 0;
262
- }
263
- },
264
-
265
- async getAllUsers(): Promise<UserProfile[]> {
266
- try {
267
- // Paginate REST users (100 per page, max 1000)
268
- const allUsers: UserProfile[] = [];
269
- for (let skip = 0; skip < 1000; skip += 100) {
270
- const data = await restGet(`/api/pulse/users?limit=100&skip=${skip}`);
271
- const users = (data.users as UserProfile[]) ?? [];
272
- allUsers.push(...users);
273
- if (users.length < 100) break; // Last page
274
- }
275
- return allUsers;
276
- } catch {
277
- // Fallback to GraphQL pagination
278
- const allUsers: UserProfile[] = [];
279
- let cursor: string | null = null;
280
- let hasNext = true;
281
- const MAX_PAGES = 20; // Cap at 1,000 users (20 × 50)
282
- let pageCount = 0;
283
- while (hasNext && pageCount < MAX_PAGES) {
284
- pageCount++;
285
- const page: Record<string, unknown> = { first: 50 };
286
- if (cursor) page.after = cursor;
287
- const data = await graphql(apiUrl, token, EXPLORE_USERS_QUERY, { page });
288
- const connection = data.exploreUsers as
289
- | { nodes?: UserProfile[]; pageInfo?: { hasNextPage?: boolean; endCursor?: string } }
290
- | undefined;
291
- allUsers.push(...(connection?.nodes ?? []));
292
- hasNext = connection?.pageInfo?.hasNextPage ?? false;
293
- cursor = connection?.pageInfo?.endCursor ?? null;
294
- }
295
- return allUsers;
296
- }
297
- },
298
-
299
- async followUser(profileId: string): Promise<void> {
300
- try {
301
- await restPost("/api/pulse/follow", { profileId });
302
- } catch {
303
- await graphql(apiUrl, token, FOLLOW_PROFILE_MUTATION, {
304
- input: { followedId: profileId },
305
- });
306
- }
307
- },
308
- };
309
- }
@@ -1,343 +0,0 @@
1
- /**
2
- * Phil Jackson Triangle Pipeline — 7 Ollama Models, Zero Tokens
3
- *
4
- * The legendary triangle offense, adapted for code diagnostics.
5
- * Each model has a role. Each role has a trigger. The pipeline
6
- * chains them in sequence — syntax → deep analysis → fast fix →
7
- * deps → architect → captain → backup — until the problem is solved.
8
- *
9
- * "The strength of the team is each individual member.
10
- * The strength of each member is the team." — Phil Jackson
11
- *
12
- * Team Chemistry (from agentide_optimizer.py):
13
- * falcon:7b — Syntax Specialist (Steph Curry's shooting)
14
- * jmorgan/grok — Deep Analyst (LeBron: sees the whole court)
15
- * mistral:latest — First Responder (Curry: fast handles, instant patch)
16
- * gemma:7b — Dependency Coordinator (Draymond: organizes the team)
17
- * mixtral:8x22b — Architect (Durant: builds with long-range vision)
18
- * llama3.1 — Team Captain (Jordan: closes with dominance)
19
- * qwen:7b — Backup (Iguodala: steps up when needed)
20
- */
21
-
22
- import type { WarRoomClient } from "./warroom.js";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Pipeline stage definitions (mirrors ORCHESTRATION_CONFIG from Python)
26
- // ---------------------------------------------------------------------------
27
-
28
- export interface PipelineStage {
29
- name: string;
30
- model: string;
31
- role: string;
32
- trigger: string;
33
- tasks: string[];
34
- output: string;
35
- /** NBA analogy from team_chemistry */
36
- chemistry: string;
37
- }
38
-
39
- export const PIPELINE_STAGES: PipelineStage[] = [
40
- {
41
- name: "syntax_correction",
42
- model: "falcon:7b",
43
- role: "Syntax Specialist",
44
- trigger: "any_syntax_error",
45
- tasks: [
46
- "Detect and fix syntax errors (missing >, ;, unterminated regex)",
47
- "Ensure JSX/TSX validity",
48
- ],
49
- output: "syntax_patch",
50
- chemistry: "Steph Curry's shooting — precision from deep",
51
- },
52
- {
53
- name: "logic_validation",
54
- model: "jmorgan/grok:latest",
55
- role: "Deep Analyst",
56
- trigger: "any_type_error",
57
- tasks: [
58
- "Detect logic conflicts across tsconfig, next.config.js, dockerfiles",
59
- "Explain error causes in human-friendly terms",
60
- "Validate and update type definitions",
61
- ],
62
- output: "validated_fix",
63
- chemistry: "LeBron — sees the whole court, validates plays",
64
- },
65
- {
66
- name: "fast_reaction",
67
- model: "mistral:latest",
68
- role: "First Responder",
69
- trigger: "any_build_error",
70
- tasks: [
71
- "Scan logs instantly",
72
- "Apply quick fixes like missing imports and typos",
73
- "Suggest yarn install if lockfile integrity fails",
74
- ],
75
- output: "quick_fix_patch",
76
- chemistry: "Curry — fast handles, instant patch",
77
- },
78
- {
79
- name: "dependency_management",
80
- model: "gemma:7b",
81
- role: "Dependency Coordinator",
82
- trigger: "after_quick_fix_patch",
83
- tasks: [
84
- "Validate dependency versions in package.json and lock files",
85
- "Suggest yarn/pnpm add/remove for missing or conflicting deps",
86
- ],
87
- output: "dependency_patch",
88
- chemistry: "Draymond — organizes the team, sets the screen",
89
- },
90
- {
91
- name: "strategic_reasoning",
92
- model: "mixtral:8x22b",
93
- role: "Architect",
94
- trigger: "after_validated_fix",
95
- tasks: [
96
- "Analyze project-wide implications of fixes",
97
- "Suggest architectural or config-level improvements",
98
- "Future-proof corrections for CI/CD and workspace",
99
- ],
100
- output: "strategic_fix_plan",
101
- chemistry: "Durant — builds with long-range vision",
102
- },
103
- {
104
- name: "final_review",
105
- model: "llama3.1:latest",
106
- role: "Team Captain",
107
- trigger: "after_strategic_fix_plan",
108
- tasks: ["Review all fixes for consistency", "Ensure build success", "Make the final call"],
109
- output: "final_patch",
110
- chemistry: "Jordan — closes with dominance",
111
- },
112
- {
113
- name: "fallback",
114
- model: "qwen:7b",
115
- role: "Backup",
116
- trigger: "after_final_review_fail",
117
- tasks: [
118
- "Provide alternative fixes if primary pipeline fails",
119
- "Fresh perspective on the problem",
120
- ],
121
- output: "fallback_fix",
122
- chemistry: "Iguodala — steps up when it matters most",
123
- },
124
- ];
125
-
126
- // ---------------------------------------------------------------------------
127
- // Decision flow — routes errors to the right starting stage
128
- // ---------------------------------------------------------------------------
129
-
130
- export type ErrorCategory = "syntax" | "type" | "build" | "dependency" | "complex" | "unknown";
131
-
132
- export function classifyError(error: string): ErrorCategory {
133
- const lower = error.toLowerCase();
134
- if (
135
- lower.includes("unexpected token") ||
136
- lower.includes("unterminated") ||
137
- lower.includes("missing >") ||
138
- lower.includes("jsx") ||
139
- lower.includes("parsing error")
140
- ) {
141
- return "syntax";
142
- }
143
- if (
144
- lower.includes("type error") ||
145
- lower.includes("does not exist on type") ||
146
- lower.includes("is not assignable") ||
147
- lower.includes("ts(") ||
148
- lower.includes("cannot find name")
149
- ) {
150
- return "type";
151
- }
152
- if (
153
- lower.includes("module not found") ||
154
- lower.includes("cannot find module") ||
155
- lower.includes("yarn add") ||
156
- lower.includes("pnpm add") ||
157
- lower.includes("peer dep") ||
158
- lower.includes("version mismatch")
159
- ) {
160
- return "dependency";
161
- }
162
- if (
163
- lower.includes("build") ||
164
- lower.includes("compile") ||
165
- lower.includes("webpack") ||
166
- lower.includes("next build") ||
167
- lower.includes("tsc")
168
- ) {
169
- return "build";
170
- }
171
- if (
172
- lower.includes("architecture") ||
173
- lower.includes("refactor") ||
174
- lower.includes("circular") ||
175
- lower.includes("design")
176
- ) {
177
- return "complex";
178
- }
179
- return "unknown";
180
- }
181
-
182
- /** Get the starting stage index for an error category */
183
- function getStartStage(category: ErrorCategory): number {
184
- switch (category) {
185
- case "syntax":
186
- return 0; // Start with falcon (syntax specialist)
187
- case "type":
188
- return 1; // Start with grok (deep analyst)
189
- case "build":
190
- return 2; // Start with mistral (fast reaction)
191
- case "dependency":
192
- return 3; // Start with gemma (dep coordinator)
193
- case "complex":
194
- return 4; // Start with mixtral (architect)
195
- case "unknown":
196
- return 2; // Default to mistral (first responder)
197
- }
198
- }
199
-
200
- // ---------------------------------------------------------------------------
201
- // Pipeline execution
202
- // ---------------------------------------------------------------------------
203
-
204
- export interface StageResult {
205
- stage: string;
206
- model: string;
207
- role: string;
208
- chemistry: string;
209
- response: unknown;
210
- durationMs: number;
211
- }
212
-
213
- export interface DiagnoseResult {
214
- problem: string;
215
- category: ErrorCategory;
216
- stagesRun: StageResult[];
217
- totalDurationMs: number;
218
- }
219
-
220
- /**
221
- * Run the Phil Jackson Triangle Pipeline.
222
- *
223
- * Routes the problem through models in sequence based on error category.
224
- * Each stage builds on the previous — like passing the ball around the
225
- * triangle until someone has the open shot.
226
- *
227
- * @param maxStages - Max models to consult (default: all 7). Use fewer for speed.
228
- */
229
- export async function runPipeline(
230
- client: WarRoomClient,
231
- problem: string,
232
- maxStages = 7,
233
- ): Promise<DiagnoseResult> {
234
- const category = classifyError(problem);
235
- const startIdx = getStartStage(category);
236
- const totalStart = Date.now();
237
-
238
- const stagesRun: StageResult[] = [];
239
-
240
- // Build context that accumulates across stages
241
- let context = problem;
242
-
243
- for (let i = 0; i < maxStages && i < PIPELINE_STAGES.length; i++) {
244
- const stageIdx = (startIdx + i) % PIPELINE_STAGES.length;
245
- const stage = PIPELINE_STAGES[stageIdx];
246
-
247
- const prompt = buildStagePrompt(stage, context, stagesRun);
248
-
249
- const stageStart = Date.now();
250
- let response: unknown;
251
- try {
252
- response = await client.ollamaGenerate(stage.model, prompt);
253
- } catch (err) {
254
- response = { error: `Model ${stage.model} unavailable: ${err}` };
255
- }
256
- const durationMs = Date.now() - stageStart;
257
-
258
- const result: StageResult = {
259
- stage: stage.name,
260
- model: stage.model,
261
- role: stage.role,
262
- chemistry: stage.chemistry,
263
- response,
264
- durationMs,
265
- };
266
- stagesRun.push(result);
267
-
268
- // Add this stage's output to the rolling context
269
- const responseText =
270
- typeof response === "object" && response !== null && "response" in response
271
- ? String((response as Record<string, unknown>).response)
272
- : JSON.stringify(response);
273
- context += `\n\n--- ${stage.role} (${stage.model}) ---\n${responseText}`;
274
- }
275
-
276
- return {
277
- problem,
278
- category,
279
- stagesRun,
280
- totalDurationMs: Date.now() - totalStart,
281
- };
282
- }
283
-
284
- /**
285
- * Quick diagnose — only runs the most relevant model for the error type.
286
- * Fast. One model. One answer. Like a fast break.
287
- */
288
- export async function quickDiagnose(
289
- client: WarRoomClient,
290
- problem: string,
291
- ): Promise<DiagnoseResult> {
292
- return runPipeline(client, problem, 1);
293
- }
294
-
295
- /**
296
- * Deep diagnose — runs all 7 models. Full triangle rotation.
297
- * Takes longer but gives a 360-degree view of the problem.
298
- */
299
- export async function deepDiagnose(
300
- client: WarRoomClient,
301
- problem: string,
302
- ): Promise<DiagnoseResult> {
303
- return runPipeline(client, problem, 7);
304
- }
305
-
306
- // ---------------------------------------------------------------------------
307
- // Prompt construction
308
- // ---------------------------------------------------------------------------
309
-
310
- function buildStagePrompt(
311
- stage: PipelineStage,
312
- context: string,
313
- previousResults: StageResult[],
314
- ): string {
315
- const taskList = stage.tasks.map((t, i) => ` ${i + 1}. ${t}`).join("\n");
316
-
317
- let prompt = `You are the ${stage.role} in the Phil Jackson Triangle Pipeline.
318
- Your job: ${stage.tasks[0]}
319
-
320
- PROBLEM:
321
- ${context}
322
-
323
- YOUR TASKS:
324
- ${taskList}
325
-
326
- `;
327
-
328
- if (previousResults.length > 0) {
329
- prompt += `PREVIOUS ANALYSIS FROM THE TEAM:\n`;
330
- for (const prev of previousResults) {
331
- const text =
332
- typeof prev.response === "object" && prev.response !== null && "response" in prev.response
333
- ? String((prev.response as Record<string, unknown>).response)
334
- : JSON.stringify(prev.response);
335
- prompt += `- ${prev.role} (${prev.model}): ${text.slice(0, 500)}\n`;
336
- }
337
- prompt += `\nBuild on their analysis. Don't repeat what they already found.\n`;
338
- }
339
-
340
- prompt += `\nBe concise and actionable. Give specific file paths, line numbers, and code fixes where possible. No fluff.`;
341
-
342
- return prompt;
343
- }