@chorus-aidlc/chorus-openclaw-plugin 0.4.0 → 0.5.3

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 (58) hide show
  1. package/README.md +208 -278
  2. package/dist/commands.d.ts +5 -0
  3. package/dist/commands.d.ts.map +1 -0
  4. package/dist/commands.js +147 -0
  5. package/dist/commands.js.map +1 -0
  6. package/dist/config.d.ts +38 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +57 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/event-router.d.ts +55 -0
  11. package/dist/event-router.d.ts.map +1 -0
  12. package/dist/event-router.js +157 -0
  13. package/dist/event-router.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +108 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/mcp-client.d.ts +37 -0
  19. package/dist/mcp-client.d.ts.map +1 -0
  20. package/dist/mcp-client.js +137 -0
  21. package/dist/mcp-client.js.map +1 -0
  22. package/dist/mcp-registration.d.ts +25 -0
  23. package/dist/mcp-registration.d.ts.map +1 -0
  24. package/dist/mcp-registration.js +93 -0
  25. package/dist/mcp-registration.js.map +1 -0
  26. package/dist/sse-listener.d.ts +37 -0
  27. package/dist/sse-listener.d.ts.map +1 -0
  28. package/dist/sse-listener.js +152 -0
  29. package/dist/sse-listener.js.map +1 -0
  30. package/dist/wake.d.ts +67 -0
  31. package/dist/wake.d.ts.map +1 -0
  32. package/dist/wake.js +234 -0
  33. package/dist/wake.js.map +1 -0
  34. package/openclaw.plugin.json +13 -12
  35. package/package.json +23 -5
  36. package/skills/brainstorm/SKILL.md +163 -0
  37. package/skills/chorus/SKILL.md +114 -97
  38. package/skills/develop/SKILL.md +197 -52
  39. package/skills/idea/SKILL.md +136 -150
  40. package/skills/openspec-aware/SKILL.md +425 -0
  41. package/skills/proposal/SKILL.md +162 -153
  42. package/skills/proposal-reviewer/SKILL.md +118 -0
  43. package/skills/quick-dev/SKILL.md +34 -10
  44. package/skills/review/SKILL.md +109 -35
  45. package/skills/task-reviewer/SKILL.md +113 -0
  46. package/skills/yolo/SKILL.md +501 -0
  47. package/src/commands.ts +138 -71
  48. package/src/config.ts +23 -10
  49. package/src/event-router.ts +46 -54
  50. package/src/index.ts +56 -83
  51. package/src/mcp-client.ts +17 -0
  52. package/src/mcp-registration.ts +142 -0
  53. package/src/openclaw-sdk.d.ts +95 -0
  54. package/src/wake.ts +310 -0
  55. package/src/tools/admin-tools.ts +0 -126
  56. package/src/tools/common-tools.ts +0 -575
  57. package/src/tools/dev-tools.ts +0 -105
  58. package/src/tools/pm-tools.ts +0 -411
package/src/index.ts CHANGED
@@ -1,107 +1,89 @@
1
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
- type OpenClawPluginApi = any;
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
3
3
 
4
- import { chorusConfigSchema, type ChorusPluginConfig, validateConfigWithWarnings } from "./config.js";
4
+ import { resolveConfig, validateConfigWithWarnings } from "./config.js";
5
+ import { ensureChorusMcpServer } from "./mcp-registration.js";
5
6
  import { ChorusMcpClient } from "./mcp-client.js";
6
7
  import { ChorusSseListener } from "./sse-listener.js";
7
8
  import { ChorusEventRouter } from "./event-router.js";
8
- import { registerPmTools } from "./tools/pm-tools.js";
9
- import { registerDevTools } from "./tools/dev-tools.js";
10
- import { registerCommonTools } from "./tools/common-tools.js";
11
- import { registerAdminTools } from "./tools/admin-tools.js";
9
+ import { createWake } from "./wake.js";
12
10
  import { registerChorusCommands } from "./commands.js";
13
11
 
14
12
  /**
15
- * Trigger the OpenClaw agent by posting a system event to the gateway's
16
- * /hooks/wake endpoint. This enqueues the text into the agent's prompt
17
- * and triggers an immediate heartbeat so the agent processes it right away.
13
+ * JSON-Schema config contract for the Chorus plugin.
14
+ *
15
+ * This mirrors the canonical `configSchema` in `openclaw.plugin.json`
16
+ * (the manifest is validated by the host BEFORE this code loads). Keep the two
17
+ * in sync: same property set, same `additionalProperties: false`.
18
18
  */
19
- async function wakeAgent(
20
- gatewayUrl: string,
21
- hooksToken: string,
22
- text: string,
23
- logger: { info: (msg: string) => void; warn: (msg: string) => void },
24
- ) {
25
- try {
26
- const res = await fetch(`${gatewayUrl}/hooks/wake`, {
27
- method: "POST",
28
- headers: {
29
- "Content-Type": "application/json",
30
- Authorization: `Bearer ${hooksToken}`,
31
- },
32
- body: JSON.stringify({ text, mode: "now" }),
33
- });
34
- if (!res.ok) {
35
- logger.warn(`Wake agent failed: HTTP ${res.status}`);
36
- } else {
37
- logger.info(`Agent woken: ${text.slice(0, 80)}...`);
38
- }
39
- } catch (err) {
40
- logger.warn(`Wake agent error: ${err}`);
41
- }
42
- }
19
+ const CHORUS_JSON_SCHEMA = {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ properties: {
23
+ chorusUrl: {
24
+ type: "string",
25
+ description: "Chorus server URL (e.g. https://chorus.example.com)",
26
+ },
27
+ apiKey: {
28
+ type: "string",
29
+ description: "Chorus API Key (cho_ prefix)",
30
+ },
31
+ },
32
+ } as const;
43
33
 
44
- const plugin = {
34
+ export default definePluginEntry({
45
35
  id: "chorus-openclaw-plugin",
46
36
  name: "Chorus",
47
37
  description:
48
- "Chorus AI-DLC collaboration platform — SSE real-time events + MCP tool integration",
49
- configSchema: chorusConfigSchema,
38
+ "Chorus AI-DLC collaboration platform — native MCP + SSE real-time events",
39
+ configSchema: {
40
+ jsonSchema: CHORUS_JSON_SCHEMA,
41
+ uiHints: { apiKey: { sensitive: true } },
42
+ },
50
43
 
51
44
  register(api: OpenClawPluginApi) {
52
- const rawConfig = api.pluginConfig ?? {};
53
- const config: ChorusPluginConfig = {
54
- chorusUrl: rawConfig.chorusUrl || undefined,
55
- apiKey: rawConfig.apiKey || undefined,
56
- projectUuids: rawConfig.projectUuids ?? [],
57
- autoStart: rawConfig.autoStart ?? true,
58
- };
59
- const logger = api.logger;
45
+ // 1. Discovery-mode guard: heavy runtime wiring (MCP connect, SSE socket,
46
+ // config mutation) must run ONLY in "full" registration mode. In
47
+ // discovery / cli-metadata / setup-* modes we declare nothing heavy.
48
+ if (api.registrationMode !== "full") return;
60
49
 
50
+ // 2. Resolve + validate config from the host-validated pluginConfig bag.
51
+ const config = resolveConfig(api.pluginConfig);
52
+ const logger = api.logger;
61
53
  if (!validateConfigWithWarnings(config, logger)) {
62
54
  return;
63
55
  }
64
56
 
65
- // After validateConfigWithWarnings, chorusUrl and apiKey are guaranteed present
57
+ // After validateConfigWithWarnings, chorusUrl and apiKey are present.
66
58
  const chorusUrl = config.chorusUrl!;
67
59
  const apiKey = config.apiKey!;
68
60
 
69
- // Resolve gateway URL and hooks token from OpenClaw config
70
- const gatewayPort = api.config?.gateway?.port ?? 18789;
71
- const gatewayUrl = `http://127.0.0.1:${gatewayPort}`;
72
- const hooksToken = api.config?.hooks?.token ?? "";
61
+ logger.info(`Chorus plugin initializing ${chorusUrl}`);
73
62
 
74
- logger.info(
75
- `Chorus plugin initializing ${chorusUrl} (${config.projectUuids?.length || "all"} projects)`
63
+ // 3. Ensure the Chorus MCP server is registered with OpenClaw so the agent
64
+ // gains native chorus__* tools (fire-and-log; real impl in sibling task).
65
+ void ensureChorusMcpServer(api, config).catch((err) =>
66
+ logger.error(`MCP registration failed: ${err}`),
76
67
  );
77
68
 
78
- // --- MCP Client ---
79
- const mcpClient = new ChorusMcpClient({
80
- chorusUrl,
81
- apiKey,
82
- logger,
83
- });
69
+ // 4. Slim MCP client for the plugin's own synchronous calls (checkin,
70
+ // assignments, notifications back-fill).
71
+ const mcpClient = new ChorusMcpClient({ chorusUrl, apiKey, logger });
84
72
 
85
- // --- Event Router ---
73
+ // 5. Event router. Wakes the agent in-process by running an embedded agent
74
+ // turn via `api.runtime.agent.runEmbeddedAgent` (see wake.ts). `createWake`
75
+ // resolves the main agent session + configured model on each wake and
76
+ // gracefully DROPS (logs + returns) when it cannot run — it never throws,
77
+ // so the SSE service stays alive even on a host that exposes no session.
86
78
  const eventRouter = new ChorusEventRouter({
87
79
  mcpClient,
88
- config,
89
80
  logger,
90
- triggerAgent: (message: string, _metadata?: Record<string, unknown>) => {
91
- // Use /hooks/wake to enqueue a system event + trigger immediate heartbeat
92
- if (hooksToken) {
93
- wakeAgent(gatewayUrl, hooksToken, message, logger);
94
- } else {
95
- logger.warn(
96
- `[Chorus] Cannot wake agent — gateway.auth.token not configured. Event: ${message.slice(0, 100)}`
97
- );
98
- }
99
- },
81
+ wake: createWake(api, logger),
100
82
  });
101
83
 
102
- // --- SSE Listener (background service) ---
84
+ // 6. Background SSE service. The SSE socket opens only inside start(), which
85
+ // the host calls in full mode — keeping the heavy socket gated.
103
86
  let sseListener: ChorusSseListener | null = null;
104
-
105
87
  api.registerService({
106
88
  id: "chorus-sse",
107
89
  async start() {
@@ -111,7 +93,6 @@ const plugin = {
111
93
  logger,
112
94
  onEvent: (event) => eventRouter.dispatch(event),
113
95
  onReconnect: async () => {
114
- // Back-fill missed notifications after reconnect
115
96
  try {
116
97
  const result = (await mcpClient.callTool("chorus_get_notifications", {
117
98
  status: "unread",
@@ -134,15 +115,7 @@ const plugin = {
134
115
  },
135
116
  });
136
117
 
137
- // --- Tools ---
138
- registerPmTools(api, mcpClient);
139
- registerDevTools(api, mcpClient);
140
- registerCommonTools(api, mcpClient);
141
- registerAdminTools(api, mcpClient);
142
-
143
- // --- Commands ---
118
+ // 7. /chorus command (status | tasks | ideas | skills).
144
119
  registerChorusCommands(api, mcpClient, () => sseListener?.status ?? "disconnected");
145
120
  },
146
- };
147
-
148
- export default plugin;
121
+ });
package/src/mcp-client.ts CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Plugin-INTERNAL MCP client.
3
+ *
4
+ * This is NOT how the agent gets Chorus tools — those are exposed natively by
5
+ * OpenClaw connecting to the `mcp.servers.chorus` entry written by
6
+ * `ensureChorusMcpServer` (see mcp-registration.ts). The plugin no longer
7
+ * hand-wraps any Chorus tool with `api.registerTool`.
8
+ *
9
+ * This client exists only for the plugin's OWN synchronous calls into Chorus:
10
+ * - `/chorus` command: chorus_checkin, chorus_get_my_assignments,
11
+ * chorus_get_available_ideas (see commands.ts)
12
+ * - SSE reconnect back-fill: chorus_get_notifications (see index.ts)
13
+ *
14
+ * No agent-facing tool depends on it. Keep it a generic StreamableHTTP
15
+ * `callTool` client (lazy connect + 404 reconnect); do not grow it into a tool
16
+ * surface.
17
+ */
1
18
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
19
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
20
 
@@ -0,0 +1,142 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
+ import type { ChorusPluginConfig } from "./config.js";
3
+
4
+ /**
5
+ * Shape of the `mcp.servers.chorus` entry we write into OpenClaw config.
6
+ *
7
+ * Mirrors the relevant subset of OpenClaw's `McpServerConfig`
8
+ * (`../openclaw/src/config/types.mcp.ts:12`): a remote streamable-http MCP
9
+ * server reachable at `<chorusUrl>/api/mcp` with a Bearer auth header.
10
+ */
11
+ interface ChorusMcpServerEntry {
12
+ url: string;
13
+ transport: "streamable-http";
14
+ headers: { Authorization: string };
15
+ }
16
+
17
+ /**
18
+ * Build the desired `mcp.servers.chorus` entry for the current config.
19
+ */
20
+ function buildDesiredEntry(chorusUrl: string, apiKey: string): ChorusMcpServerEntry {
21
+ return {
22
+ url: new URL("/api/mcp", chorusUrl).toString(),
23
+ transport: "streamable-http",
24
+ headers: { Authorization: `Bearer ${apiKey}` },
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Shallow-equal the fields that define the desired entry. We only compare the
30
+ * three load-bearing fields (url, transport, Authorization header) so an
31
+ * operator-added extra field — e.g. `connectionTimeoutMs` — does not force a
32
+ * rewrite-and-reload on every activation.
33
+ */
34
+ function entryMatches(existing: unknown, desired: ChorusMcpServerEntry): boolean {
35
+ if (!existing || typeof existing !== "object") return false;
36
+ const e = existing as {
37
+ url?: unknown;
38
+ transport?: unknown;
39
+ headers?: { Authorization?: unknown } | undefined;
40
+ };
41
+ return (
42
+ e.url === desired.url &&
43
+ e.transport === desired.transport &&
44
+ e.headers?.Authorization === desired.headers.Authorization
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Ensure the Chorus MCP server is registered with OpenClaw so the agent gains
50
+ * the native `chorus__*` tools.
51
+ *
52
+ * Writes (idempotently) an `mcp.servers.chorus` entry into the OpenClaw config
53
+ * via `api.runtime.config.mutateConfigFile` — a remote streamable-http MCP
54
+ * server with Bearer auth. OpenClaw then connects to the remote Chorus MCP
55
+ * server on (re)load and auto-exposes its tools under the `chorus__` prefix; we
56
+ * never re-declare those tools with `api.registerTool`.
57
+ *
58
+ * Behavior:
59
+ * - Missing `chorusUrl`/`apiKey`: do NOT write; warn naming the missing field(s).
60
+ * - Existing entry already equals the desired entry: return without writing
61
+ * (idempotent — no config reload triggered).
62
+ * - Otherwise: `mutateConfigFile({ afterWrite: { mode: "auto" }, mutate })`,
63
+ * mutating the draft in place (OpenClaw's `mutate` callback receives a
64
+ * structuredClone draft and persists the in-place mutation).
65
+ * - Any rejection from `mutateConfigFile` (or the runtime API being absent) is
66
+ * logged at error level and swallowed — registration failure must never crash
67
+ * the gateway, so the SSE service and `/chorus` command still register.
68
+ */
69
+ export async function ensureChorusMcpServer(
70
+ api: OpenClawPluginApi,
71
+ cfg: ChorusPluginConfig,
72
+ ): Promise<void> {
73
+ const logger = api.logger;
74
+
75
+ // 1. Required-config guard: do not write a half-formed entry.
76
+ const missing: string[] = [];
77
+ if (!cfg.chorusUrl) missing.push("chorusUrl");
78
+ if (!cfg.apiKey) missing.push("apiKey");
79
+ if (missing.length > 0) {
80
+ logger.warn(
81
+ `[Chorus] Skipping MCP server registration — missing required config: ${missing.join(", ")}`,
82
+ );
83
+ return;
84
+ }
85
+
86
+ const desired = buildDesiredEntry(cfg.chorusUrl!, cfg.apiKey!);
87
+
88
+ try {
89
+ // `api.runtime` is permissively typed via the SDK shim; narrow to the
90
+ // config surface we use (current() + mutateConfigFile). At runtime the host
91
+ // provides the real `PluginRuntimeCore.config` API
92
+ // (../openclaw/src/plugins/runtime/types-core.ts:145).
93
+ const runtimeConfig = (
94
+ api.runtime as
95
+ | {
96
+ config?: {
97
+ current?: () => { mcp?: { servers?: Record<string, unknown> } } | undefined;
98
+ mutateConfigFile?: (params: {
99
+ afterWrite: { mode: "auto" };
100
+ mutate: (draft: {
101
+ mcp?: { servers?: Record<string, unknown> };
102
+ }) => void;
103
+ }) => Promise<unknown>;
104
+ };
105
+ }
106
+ | undefined
107
+ )?.config;
108
+
109
+ if (!runtimeConfig?.mutateConfigFile) {
110
+ logger.error(
111
+ "[Chorus] MCP server registration failed — runtime.config.mutateConfigFile is unavailable on this host",
112
+ );
113
+ return;
114
+ }
115
+
116
+ // 2. Idempotency: skip the write (and the reload it triggers) when the
117
+ // existing entry already matches.
118
+ const existing = runtimeConfig.current?.()?.mcp?.servers?.chorus;
119
+ if (entryMatches(existing, desired)) {
120
+ logger.info("[Chorus] MCP server entry already up to date — no config change");
121
+ return;
122
+ }
123
+
124
+ // 3. Write the entry, mutating the draft in place.
125
+ await runtimeConfig.mutateConfigFile({
126
+ afterWrite: { mode: "auto" },
127
+ mutate: (draft) => {
128
+ const mcp = (draft.mcp ??= {});
129
+ const servers = (mcp.servers ??= {});
130
+ servers.chorus = desired;
131
+ },
132
+ });
133
+
134
+ logger.info(`[Chorus] Registered MCP server entry — ${desired.url}`);
135
+ } catch (err) {
136
+ // 4. Never crash the gateway: log at error level and continue so the SSE
137
+ // service and /chorus command still register.
138
+ logger.error(
139
+ `[Chorus] MCP server registration failed: ${err instanceof Error ? err.message : String(err)}`,
140
+ );
141
+ }
142
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Ambient module shim for the OpenClaw 2026.5.x Plugin SDK.
3
+ *
4
+ * WHY THIS EXISTS:
5
+ * The plugin's runtime entry imports `definePluginEntry` from the subpath
6
+ * `openclaw/plugin-sdk/plugin-entry`. That subpath export only exists in the
7
+ * OpenClaw 2026.5.30 Plugin SDK. The `openclaw` package currently resolvable in
8
+ * this workspace's node_modules is an older build (2026.3.x) that does NOT
9
+ * export `./plugin-sdk/plugin-entry`, so `tsc` cannot resolve the import from
10
+ * the real package and would fail type-checking.
11
+ *
12
+ * Rather than bundle/install the full 2026.5.30 OpenClaw package just to satisfy
13
+ * `tsc --noEmit`, we declare a minimal ambient module with a permissive
14
+ * signature. At install/runtime the real host provides the actual SDK
15
+ * (the package.json `peerDependencies.openclaw` floor + the
16
+ * `openclaw.compat.pluginApi >=2026.5.30` gate enforce the version contract);
17
+ * this declaration is purely a compile-time bridge.
18
+ *
19
+ * The signature mirrors `definePluginEntry` from
20
+ * `../openclaw/src/plugin-sdk/plugin-entry.ts` (verified against OpenClaw
21
+ * 2026.5.30). `api` is typed `unknown`-ish via a permissive shape so the entry
22
+ * file stays readable without pulling in the full `OpenClawPluginApi` type
23
+ * graph. When the workspace upgrades to an `openclaw` build that exports
24
+ * `plugin-sdk/plugin-entry`, delete this shim and rely on the real types.
25
+ */
26
+ declare module "openclaw/plugin-sdk/plugin-entry" {
27
+ /** Public registration modes surfaced to plugin `register(api)` calls. */
28
+ export type PluginRegistrationMode =
29
+ | "full"
30
+ | "discovery"
31
+ | "tool-discovery"
32
+ | "setup-only"
33
+ | "setup-runtime"
34
+ | "cli-metadata";
35
+
36
+ /** JSON-Schema / parser config contract accepted by plugin entries. */
37
+ export type OpenClawPluginConfigSchema = {
38
+ safeParse?: (value: unknown) => unknown;
39
+ parse?: (value: unknown) => unknown;
40
+ validate?: (value: unknown) => unknown;
41
+ uiHints?: Record<string, unknown>;
42
+ jsonSchema?: Record<string, unknown>;
43
+ };
44
+
45
+ /**
46
+ * Permissive subset of `OpenClawPluginApi` used by this plugin's entry.
47
+ *
48
+ * Only the members this plugin touches are typed; the index signature keeps
49
+ * the rest of the (large) host API accessible without importing it.
50
+ */
51
+ export type OpenClawPluginApi = {
52
+ registrationMode: PluginRegistrationMode;
53
+ pluginConfig?: Record<string, unknown>;
54
+ config?: Record<string, unknown> & {
55
+ gateway?: { port?: number };
56
+ hooks?: { token?: string };
57
+ };
58
+ logger: {
59
+ debug?: (message: string) => void;
60
+ info: (message: string) => void;
61
+ warn: (message: string) => void;
62
+ error: (message: string) => void;
63
+ };
64
+ registerService: (service: {
65
+ id: string;
66
+ start: (...args: unknown[]) => void | Promise<void>;
67
+ stop?: (...args: unknown[]) => void | Promise<void>;
68
+ }) => void;
69
+ registerCommand: (command: unknown) => void;
70
+ registerTool: (tool: unknown, opts?: unknown) => void;
71
+ runtime?: unknown;
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ [key: string]: any;
74
+ };
75
+
76
+ export type DefinePluginEntryOptions = {
77
+ id: string;
78
+ name: string;
79
+ description: string;
80
+ configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
81
+ register: (api: OpenClawPluginApi) => void;
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ [key: string]: any;
84
+ };
85
+
86
+ export type DefinedPluginEntry = {
87
+ id: string;
88
+ name: string;
89
+ description: string;
90
+ configSchema: OpenClawPluginConfigSchema;
91
+ register: (api: OpenClawPluginApi) => void;
92
+ };
93
+
94
+ export function definePluginEntry(options: DefinePluginEntryOptions): DefinedPluginEntry;
95
+ }