@useorgx/openclaw-plugin 0.4.5 → 0.4.6

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 (54) hide show
  1. package/README.md +24 -3
  2. package/dashboard/dist/assets/0tOC3wSN.js +214 -0
  3. package/dashboard/dist/assets/B3ziCA02.js +8 -0
  4. package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
  5. package/dashboard/dist/assets/CpJsfbXo.js +9 -0
  6. package/dashboard/dist/assets/CyxZio4Y.js +1 -0
  7. package/dashboard/dist/assets/DaAIOik3.css +1 -0
  8. package/dashboard/dist/assets/sAhvFnpk.js +4 -0
  9. package/dashboard/dist/index.html +5 -5
  10. package/dist/activity-store.d.ts +28 -0
  11. package/dist/activity-store.js +250 -0
  12. package/dist/agent-context-store.d.ts +19 -0
  13. package/dist/agent-context-store.js +60 -3
  14. package/dist/agent-suite.d.ts +83 -0
  15. package/dist/agent-suite.js +615 -0
  16. package/dist/contracts/client.d.ts +22 -1
  17. package/dist/contracts/client.js +120 -3
  18. package/dist/contracts/types.d.ts +190 -1
  19. package/dist/entity-comment-store.d.ts +29 -0
  20. package/dist/entity-comment-store.js +190 -0
  21. package/dist/hooks/post-reporting-event.mjs +326 -0
  22. package/dist/http-handler.d.ts +7 -1
  23. package/dist/http-handler.js +3603 -578
  24. package/dist/index.js +936 -62
  25. package/dist/mcp-client-setup.js +156 -24
  26. package/dist/mcp-http-handler.d.ts +17 -0
  27. package/dist/mcp-http-handler.js +144 -3
  28. package/dist/next-up-queue-store.d.ts +31 -0
  29. package/dist/next-up-queue-store.js +169 -0
  30. package/dist/openclaw.plugin.json +1 -1
  31. package/dist/outbox.d.ts +1 -1
  32. package/dist/runtime-instance-store.d.ts +1 -1
  33. package/dist/runtime-instance-store.js +20 -3
  34. package/dist/skill-pack-state.d.ts +69 -0
  35. package/dist/skill-pack-state.js +232 -0
  36. package/dist/worker-supervisor.d.ts +25 -0
  37. package/dist/worker-supervisor.js +62 -0
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +10 -1
  40. package/skills/orgx-design-agent/SKILL.md +38 -0
  41. package/skills/orgx-engineering-agent/SKILL.md +55 -0
  42. package/skills/orgx-marketing-agent/SKILL.md +40 -0
  43. package/skills/orgx-operations-agent/SKILL.md +40 -0
  44. package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
  45. package/skills/orgx-product-agent/SKILL.md +39 -0
  46. package/skills/orgx-sales-agent/SKILL.md +40 -0
  47. package/skills/ship/SKILL.md +63 -0
  48. package/dashboard/dist/assets/B68j2crt.js +0 -1
  49. package/dashboard/dist/assets/BZZ-fiJx.js +0 -32
  50. package/dashboard/dist/assets/BoXlCHKa.js +0 -9
  51. package/dashboard/dist/assets/Bq9x_Xyh.css +0 -1
  52. package/dashboard/dist/assets/DBhrRVdp.js +0 -1
  53. package/dashboard/dist/assets/DD1jv1Hd.js +0 -8
  54. package/dashboard/dist/assets/DNjbmawF.js +0 -214
@@ -3,6 +3,27 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { writeFileAtomicSync, writeJsonFileAtomicSync } from "./fs-utils.js";
6
+ const ORGX_LOCAL_MCP_KEY = "orgx-openclaw";
7
+ const ORGX_HOSTED_MCP_URL = "https://mcp.useorgx.com/mcp";
8
+ const ORGX_LOCAL_MCP_SCOPES = [
9
+ "engineering",
10
+ "product",
11
+ "design",
12
+ "marketing",
13
+ "sales",
14
+ "operations",
15
+ "orchestration",
16
+ ];
17
+ function scopedMcpServerKey(scope) {
18
+ return `${ORGX_LOCAL_MCP_KEY}-${scope}`;
19
+ }
20
+ function scopedMcpUrl(localMcpUrl, scope) {
21
+ const base = localMcpUrl.replace(/\/+$/, "");
22
+ return `${base}/${scope}`;
23
+ }
24
+ function escapeRegExp(value) {
25
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ }
6
27
  function isRecord(value) {
7
28
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
8
29
  }
@@ -40,51 +61,109 @@ function backupFileSync(path, mode) {
40
61
  }
41
62
  export function patchClaudeMcpConfig(input) {
42
63
  const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
43
- const currentOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
44
- const priorUrl = typeof currentOrgx.url === "string" ? currentOrgx.url : "";
45
- const priorType = typeof currentOrgx.type === "string" ? currentOrgx.type : "";
46
- const nextOrgx = {
47
- ...currentOrgx,
64
+ const existingOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
65
+ const existingOrgxUrl = typeof existingOrgx.url === "string" ? existingOrgx.url : "";
66
+ const existingOrgxType = typeof existingOrgx.type === "string" ? existingOrgx.type : "";
67
+ const existing = isRecord(currentServers[ORGX_LOCAL_MCP_KEY]) ? currentServers[ORGX_LOCAL_MCP_KEY] : {};
68
+ const priorUrl = typeof existing.url === "string" ? existing.url : "";
69
+ const priorType = typeof existing.type === "string" ? existing.type : "";
70
+ // Ensure hosted OrgX is available alongside the local proxy. Avoid overwriting
71
+ // custom `orgx` entries unless it's clearly redundant (pointing at the same
72
+ // local proxy URL we install under `orgx-openclaw`).
73
+ const shouldSetHostedOrgx = !isRecord(currentServers.orgx) ||
74
+ (existingOrgxUrl === input.localMcpUrl && existingOrgxType === "http");
75
+ const nextOrgxEntry = {
76
+ ...existingOrgx,
77
+ type: "http",
78
+ url: ORGX_HOSTED_MCP_URL,
79
+ description: typeof existingOrgx.description === "string" && existingOrgx.description.trim().length > 0
80
+ ? existingOrgx.description
81
+ : "OrgX cloud MCP (OAuth)",
82
+ };
83
+ const nextEntry = {
84
+ ...existing,
48
85
  type: "http",
49
86
  url: input.localMcpUrl,
50
- description: typeof currentOrgx.description === "string" && currentOrgx.description.trim().length > 0
51
- ? currentOrgx.description
87
+ description: typeof existing.description === "string" && existing.description.trim().length > 0
88
+ ? existing.description
52
89
  : "OrgX platform via local OpenClaw plugin (no OAuth)",
53
90
  };
91
+ const updatedScopes = [];
92
+ const scopedEntries = {};
93
+ for (const scope of ORGX_LOCAL_MCP_SCOPES) {
94
+ const key = scopedMcpServerKey(scope);
95
+ const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
96
+ const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
97
+ const priorUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
98
+ const priorType = typeof existingScoped.type === "string" ? existingScoped.type : "";
99
+ const nextScoped = {
100
+ ...existingScoped,
101
+ type: "http",
102
+ url: expectedUrl,
103
+ description: typeof existingScoped.description === "string" && existingScoped.description.trim().length > 0
104
+ ? existingScoped.description
105
+ : `OrgX platform via local OpenClaw plugin (${scope} scope)`,
106
+ };
107
+ scopedEntries[key] = nextScoped;
108
+ if (priorUrl !== expectedUrl || priorType !== "http") {
109
+ updatedScopes.push(scope);
110
+ }
111
+ }
54
112
  const nextServers = {
55
113
  ...currentServers,
56
- orgx: nextOrgx,
114
+ ...(shouldSetHostedOrgx ? { orgx: nextOrgxEntry } : {}),
115
+ [ORGX_LOCAL_MCP_KEY]: nextEntry,
116
+ ...scopedEntries,
57
117
  };
58
118
  const next = {
59
119
  ...input.current,
60
120
  mcpServers: nextServers,
61
121
  };
62
- const updated = priorUrl !== input.localMcpUrl || priorType !== "http";
122
+ const updatedLocal = priorUrl !== input.localMcpUrl || priorType !== "http";
123
+ const updatedHosted = shouldSetHostedOrgx &&
124
+ (existingOrgxUrl !== ORGX_HOSTED_MCP_URL || existingOrgxType !== "http");
125
+ const updated = updatedLocal || updatedHosted || updatedScopes.length > 0;
63
126
  return { updated, next };
64
127
  }
65
128
  export function patchCursorMcpConfig(input) {
66
129
  const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
67
- const key = "orgx-openclaw";
68
- const existing = isRecord(currentServers[key]) ? currentServers[key] : {};
130
+ const existing = isRecord(currentServers[ORGX_LOCAL_MCP_KEY]) ? currentServers[ORGX_LOCAL_MCP_KEY] : {};
69
131
  const priorUrl = typeof existing.url === "string" ? existing.url : "";
70
132
  const nextEntry = {
71
133
  ...existing,
72
134
  url: input.localMcpUrl,
73
135
  };
136
+ const scopedEntries = {};
137
+ let updatedScopes = false;
138
+ for (const scope of ORGX_LOCAL_MCP_SCOPES) {
139
+ const key = scopedMcpServerKey(scope);
140
+ const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
141
+ const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
142
+ const priorScopedUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
143
+ scopedEntries[key] = {
144
+ ...existingScoped,
145
+ url: expectedUrl,
146
+ };
147
+ if (priorScopedUrl !== expectedUrl)
148
+ updatedScopes = true;
149
+ }
74
150
  const nextServers = {
75
151
  ...currentServers,
76
- [key]: nextEntry,
152
+ [ORGX_LOCAL_MCP_KEY]: nextEntry,
153
+ ...scopedEntries,
77
154
  };
78
155
  const next = {
79
156
  ...input.current,
80
157
  mcpServers: nextServers,
81
158
  };
82
- const updated = priorUrl !== input.localMcpUrl;
159
+ const updated = priorUrl !== input.localMcpUrl || updatedScopes;
83
160
  return { updated, next };
84
161
  }
85
- export function patchCodexConfigToml(input) {
86
- const lines = input.current.split(/\r?\n/);
87
- const headerRegex = /^\[mcp_servers\.(?:orgx|"orgx")\]\s*$/;
162
+ function upsertCodexMcpServerSection(input) {
163
+ const currentText = input.current;
164
+ const lines = currentText.split(/\r?\n/);
165
+ const escapedKey = escapeRegExp(input.key);
166
+ const headerRegex = new RegExp(`^\\[mcp_servers\\.(?:"${escapedKey}"|${escapedKey})\\]\\s*$`);
88
167
  let headerIndex = -1;
89
168
  for (let i = 0; i < lines.length; i += 1) {
90
169
  if (headerRegex.test(lines[i].trim())) {
@@ -92,15 +171,10 @@ export function patchCodexConfigToml(input) {
92
171
  break;
93
172
  }
94
173
  }
95
- const urlLine = `url = "${input.localMcpUrl}"`;
174
+ const urlLine = `url = "${input.url}"`;
96
175
  if (headerIndex === -1) {
97
- const suffix = [
98
- "",
99
- "[mcp_servers.orgx]",
100
- urlLine,
101
- "",
102
- ].join("\n");
103
- const normalized = input.current.endsWith("\n") ? input.current : `${input.current}\n`;
176
+ const suffix = ["", `[mcp_servers.\"${input.key}\"]`, urlLine, ""].join("\n");
177
+ const normalized = currentText.endsWith("\n") ? currentText : `${currentText}\n`;
104
178
  return { updated: true, next: `${normalized}${suffix}` };
105
179
  }
106
180
  let sectionEnd = lines.length;
@@ -127,9 +201,67 @@ export function patchCodexConfigToml(input) {
127
201
  else {
128
202
  lines.splice(headerIndex + 1, 0, urlLine);
129
203
  updated = true;
204
+ // Recalculate sectionEnd after splice
205
+ sectionEnd = lines.length;
206
+ for (let i = headerIndex + 1; i < lines.length; i += 1) {
207
+ if (lines[i].trim().startsWith("[")) {
208
+ sectionEnd = i;
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ // Strip stale stdio-transport fields that conflict with url-only entries.
214
+ // Codex rejects `url` when `command`/`args` are present (stdio transport).
215
+ const staleFieldRegex = /^\s*(command|args|startup_timeout_sec)\s*=/;
216
+ for (let i = sectionEnd - 1; i > headerIndex; i -= 1) {
217
+ if (staleFieldRegex.test(lines[i])) {
218
+ lines.splice(i, 1);
219
+ updated = true;
220
+ }
130
221
  }
131
222
  return { updated, next: `${lines.join("\n")}\n` };
132
223
  }
224
+ export function patchCodexConfigToml(input) {
225
+ let current = input.current;
226
+ let updatedHosted = false;
227
+ // If the hosted OrgX entry is missing entirely, add a sensible default. This is
228
+ // a no-op if the user already has `orgx` pointed at staging or another URL.
229
+ const hostedHeaderRegex = /^\[mcp_servers\.(?:"orgx"|orgx)\]\s*$/;
230
+ {
231
+ const lines = current.split(/\r?\n/);
232
+ const hasHosted = lines.some((line) => hostedHeaderRegex.test(line.trim()));
233
+ if (!hasHosted) {
234
+ const hostedUrlLine = `url = "${ORGX_HOSTED_MCP_URL}"`;
235
+ const suffix = [
236
+ ...(current.trim().length === 0 ? [] : [""]),
237
+ "[mcp_servers.orgx]",
238
+ hostedUrlLine,
239
+ "",
240
+ ].join("\n");
241
+ const normalized = current.endsWith("\n") ? current : `${current}\n`;
242
+ current = `${normalized}${suffix}`;
243
+ updatedHosted = true;
244
+ }
245
+ }
246
+ let updated = false;
247
+ const base = upsertCodexMcpServerSection({
248
+ current,
249
+ key: ORGX_LOCAL_MCP_KEY,
250
+ url: input.localMcpUrl,
251
+ });
252
+ updated = updated || base.updated;
253
+ current = base.next;
254
+ for (const scope of ORGX_LOCAL_MCP_SCOPES) {
255
+ const next = upsertCodexMcpServerSection({
256
+ current,
257
+ key: scopedMcpServerKey(scope),
258
+ url: scopedMcpUrl(input.localMcpUrl, scope),
259
+ });
260
+ updated = updated || next.updated;
261
+ current = next.next;
262
+ }
263
+ return { updated: updatedHosted || updated, next: current };
264
+ }
133
265
  export async function autoConfigureDetectedMcpClients(input) {
134
266
  const logger = input.logger ?? {};
135
267
  const home = input.homeDir ?? homedir();
@@ -30,9 +30,26 @@ export type RegisteredTool = {
30
30
  parameters: Record<string, unknown>;
31
31
  execute: (callId: string, params?: unknown) => Promise<ToolResult>;
32
32
  };
33
+ type PromptRole = "system" | "user" | "assistant";
34
+ type PromptMessage = {
35
+ role: PromptRole;
36
+ content: string;
37
+ };
38
+ export type RegisteredPrompt = {
39
+ name: string;
40
+ description?: string;
41
+ arguments?: Array<{
42
+ name: string;
43
+ description?: string;
44
+ required?: boolean;
45
+ }>;
46
+ messages: PromptMessage[];
47
+ };
33
48
  export declare function createMcpHttpHandler(input: {
34
49
  tools: Map<string, RegisteredTool>;
50
+ prompts?: Map<string, RegisteredPrompt>;
35
51
  logger?: Logger;
36
52
  serverName: string;
37
53
  serverVersion: string;
38
54
  }): (req: PluginRequest, res: PluginResponse) => Promise<boolean>;
55
+ export {};
@@ -1,5 +1,79 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
3
+ // Domain-scoped MCP servers are meant to be "default safe". The unscoped
4
+ // `/orgx/mcp` endpoint remains available for power users / debugging.
5
+ //
6
+ // NOTE: This scopes only the tools exposed by this plugin (OrgX reporting + mutation).
7
+ // It cannot restrict OpenClaw-native tools (filesystem, shell, etc).
8
+ const ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE = {
9
+ engineering: [
10
+ "orgx_status",
11
+ "orgx_sync",
12
+ "orgx_emit_activity",
13
+ "orgx_report_progress",
14
+ "orgx_register_artifact",
15
+ "orgx_request_decision",
16
+ "orgx_spawn_check",
17
+ ],
18
+ product: [
19
+ "orgx_status",
20
+ "orgx_sync",
21
+ "orgx_emit_activity",
22
+ "orgx_report_progress",
23
+ "orgx_register_artifact",
24
+ "orgx_request_decision",
25
+ "orgx_spawn_check",
26
+ ],
27
+ design: [
28
+ "orgx_status",
29
+ "orgx_sync",
30
+ "orgx_emit_activity",
31
+ "orgx_report_progress",
32
+ "orgx_register_artifact",
33
+ "orgx_request_decision",
34
+ "orgx_spawn_check",
35
+ ],
36
+ marketing: [
37
+ "orgx_status",
38
+ "orgx_sync",
39
+ "orgx_emit_activity",
40
+ "orgx_report_progress",
41
+ "orgx_register_artifact",
42
+ "orgx_request_decision",
43
+ "orgx_spawn_check",
44
+ ],
45
+ sales: [
46
+ "orgx_status",
47
+ "orgx_sync",
48
+ "orgx_emit_activity",
49
+ "orgx_report_progress",
50
+ "orgx_register_artifact",
51
+ "orgx_request_decision",
52
+ "orgx_spawn_check",
53
+ ],
54
+ operations: [
55
+ "orgx_status",
56
+ "orgx_sync",
57
+ "orgx_emit_activity",
58
+ "orgx_report_progress",
59
+ "orgx_register_artifact",
60
+ "orgx_request_decision",
61
+ "orgx_spawn_check",
62
+ // Operations is allowed to do explicit changesets for remediation/runbooks.
63
+ "orgx_apply_changeset",
64
+ ],
65
+ orchestration: [
66
+ "orgx_status",
67
+ "orgx_sync",
68
+ "orgx_emit_activity",
69
+ "orgx_report_progress",
70
+ "orgx_register_artifact",
71
+ "orgx_request_decision",
72
+ "orgx_spawn_check",
73
+ // Orchestrator is the primary mutation surface by design.
74
+ "orgx_apply_changeset",
75
+ ],
76
+ };
3
77
  function isRecord(value) {
4
78
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
5
79
  }
@@ -38,6 +112,28 @@ function normalizePath(rawUrl) {
38
112
  const [path] = rawUrl.split("?", 2);
39
113
  return path || "/";
40
114
  }
115
+ function parseScopeKey(url) {
116
+ // Supported paths:
117
+ // - /orgx/mcp (unscoped)
118
+ // - /orgx/mcp/<scope> (domain-scoped)
119
+ if (!url.startsWith("/orgx/mcp/"))
120
+ return null;
121
+ const rest = url.slice("/orgx/mcp/".length);
122
+ const key = rest.split("/", 1)[0]?.trim() ?? "";
123
+ if (!key)
124
+ return null;
125
+ if (!Object.prototype.hasOwnProperty.call(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE, key))
126
+ return null;
127
+ return key;
128
+ }
129
+ function resolveToolScope(scopeKey) {
130
+ if (!scopeKey)
131
+ return null;
132
+ return {
133
+ key: scopeKey,
134
+ allowedTools: new Set(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE[scopeKey]),
135
+ };
136
+ }
41
137
  async function readRequestBodyBuffer(req) {
42
138
  const body = req.body;
43
139
  if (typeof body === "string")
@@ -131,13 +227,15 @@ async function handleRpcMessage(input) {
131
227
  if (method === "initialize") {
132
228
  const requestedProtocol = typeof params.protocolVersion === "string" ? params.protocolVersion : null;
133
229
  const protocolVersion = requestedProtocol?.trim() || DEFAULT_PROTOCOL_VERSION;
230
+ const scopedServerName = input.toolScope ? `${input.serverName}/${input.toolScope.key}` : input.serverName;
134
231
  return jsonRpcResult(id, {
135
232
  protocolVersion,
136
233
  capabilities: {
137
234
  tools: {},
235
+ prompts: {},
138
236
  },
139
237
  serverInfo: {
140
- name: input.serverName,
238
+ name: scopedServerName,
141
239
  version: input.serverVersion,
142
240
  },
143
241
  });
@@ -146,6 +244,15 @@ async function handleRpcMessage(input) {
146
244
  return jsonRpcResult(id, { ok: true });
147
245
  }
148
246
  if (method === "tools/list") {
247
+ if (input.toolScope) {
248
+ const scopedTools = new Map();
249
+ for (const name of input.toolScope.allowedTools) {
250
+ const tool = input.tools.get(name);
251
+ if (tool)
252
+ scopedTools.set(name, tool);
253
+ }
254
+ return jsonRpcResult(id, { tools: buildToolsList(scopedTools) });
255
+ }
149
256
  return jsonRpcResult(id, {
150
257
  tools: buildToolsList(input.tools),
151
258
  });
@@ -155,6 +262,9 @@ async function handleRpcMessage(input) {
155
262
  if (!toolName) {
156
263
  return jsonRpcError(id, -32602, "Missing tool name");
157
264
  }
265
+ if (input.toolScope && !input.toolScope.allowedTools.has(toolName)) {
266
+ return jsonRpcError(id, -32601, `Tool not available in scope '${input.toolScope.key}': ${toolName}`);
267
+ }
158
268
  const tool = input.tools.get(toolName) ?? null;
159
269
  if (!tool) {
160
270
  return jsonRpcError(id, -32601, `Tool not found: ${toolName}`);
@@ -188,7 +298,28 @@ async function handleRpcMessage(input) {
188
298
  return jsonRpcResult(id, { resources: [] });
189
299
  }
190
300
  if (method === "prompts/list") {
191
- return jsonRpcResult(id, { prompts: [] });
301
+ const prompts = Array.from(input.prompts.values())
302
+ .sort((a, b) => a.name.localeCompare(b.name))
303
+ .map((prompt) => ({
304
+ name: prompt.name,
305
+ description: prompt.description ?? "",
306
+ arguments: Array.isArray(prompt.arguments) ? prompt.arguments : [],
307
+ }));
308
+ return jsonRpcResult(id, { prompts });
309
+ }
310
+ if (method === "prompts/get") {
311
+ const promptName = typeof params.name === "string" ? params.name.trim() : "";
312
+ if (!promptName) {
313
+ return jsonRpcError(id, -32602, "Missing prompt name");
314
+ }
315
+ const prompt = input.prompts.get(promptName) ?? null;
316
+ if (!prompt) {
317
+ return jsonRpcError(id, -32601, `Prompt not found: ${promptName}`);
318
+ }
319
+ return jsonRpcResult(id, {
320
+ description: prompt.description ?? "",
321
+ messages: prompt.messages,
322
+ });
192
323
  }
193
324
  if (method.startsWith("notifications/")) {
194
325
  return null;
@@ -197,6 +328,7 @@ async function handleRpcMessage(input) {
197
328
  }
198
329
  export function createMcpHttpHandler(input) {
199
330
  const logger = input.logger ?? {};
331
+ const prompts = input.prompts ?? new Map();
200
332
  return async function handler(req, res) {
201
333
  const method = (req.method ?? "GET").toUpperCase();
202
334
  const rawUrl = req.url ?? "/";
@@ -204,6 +336,12 @@ export function createMcpHttpHandler(input) {
204
336
  if (!(url === "/orgx/mcp" || url.startsWith("/orgx/mcp/"))) {
205
337
  return false;
206
338
  }
339
+ const scopeKey = parseScopeKey(url);
340
+ const toolScope = resolveToolScope(scopeKey);
341
+ if (url.startsWith("/orgx/mcp/") && !scopeKey) {
342
+ sendText(res, 404, `Unknown OrgX MCP scope. Supported: ${Object.keys(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE).join(", ")}\n`);
343
+ return true;
344
+ }
207
345
  if (method === "OPTIONS") {
208
346
  res.writeHead(204, {
209
347
  "cache-control": "no-store",
@@ -212,7 +350,8 @@ export function createMcpHttpHandler(input) {
212
350
  return true;
213
351
  }
214
352
  if (method === "GET") {
215
- sendText(res, 200, "OrgX Local MCP bridge is running.\n");
353
+ const suffix = toolScope ? ` (scope: ${toolScope.key})` : "";
354
+ sendText(res, 200, `OrgX Local MCP bridge is running${suffix}.\n`);
216
355
  return true;
217
356
  }
218
357
  if (method !== "POST") {
@@ -234,9 +373,11 @@ export function createMcpHttpHandler(input) {
234
373
  const response = await handleRpcMessage({
235
374
  message,
236
375
  tools: input.tools,
376
+ prompts,
237
377
  logger,
238
378
  serverName: input.serverName,
239
379
  serverVersion: input.serverVersion,
380
+ toolScope,
240
381
  });
241
382
  if (response)
242
383
  responses.push(response);
@@ -0,0 +1,31 @@
1
+ export type NextUpPinnedEntry = {
2
+ initiativeId: string;
3
+ workstreamId: string;
4
+ preferredTaskId: string | null;
5
+ preferredMilestoneId: string | null;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ };
9
+ type PersistedNextUpQueue = {
10
+ version: 1;
11
+ updatedAt: string;
12
+ pins: NextUpPinnedEntry[];
13
+ };
14
+ export declare function readNextUpQueuePins(): PersistedNextUpQueue;
15
+ export declare function upsertNextUpQueuePin(input: {
16
+ initiativeId: string;
17
+ workstreamId: string;
18
+ preferredTaskId?: string | null;
19
+ preferredMilestoneId?: string | null;
20
+ }): PersistedNextUpQueue;
21
+ export declare function removeNextUpQueuePin(input: {
22
+ initiativeId: string;
23
+ workstreamId: string;
24
+ }): PersistedNextUpQueue;
25
+ export declare function setNextUpQueuePinOrder(input: {
26
+ order: Array<{
27
+ initiativeId: string;
28
+ workstreamId: string;
29
+ }>;
30
+ }): PersistedNextUpQueue;
31
+ export {};
@@ -0,0 +1,169 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, } from "node:fs";
2
+ import { getOrgxPluginConfigDir, getOrgxPluginConfigPath } from "./paths.js";
3
+ import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
4
+ const MAX_PINS = 240;
5
+ function storeDir() {
6
+ return getOrgxPluginConfigDir();
7
+ }
8
+ function storeFile() {
9
+ return getOrgxPluginConfigPath("next-up-queue.json");
10
+ }
11
+ function ensureStoreDir() {
12
+ const dir = storeDir();
13
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
14
+ try {
15
+ chmodSync(dir, 0o700);
16
+ }
17
+ catch {
18
+ // best effort
19
+ }
20
+ }
21
+ function parseJson(value) {
22
+ try {
23
+ return JSON.parse(value);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function normalizeNullableString(value) {
30
+ if (typeof value !== "string")
31
+ return null;
32
+ const trimmed = value.trim();
33
+ return trimmed.length > 0 ? trimmed : null;
34
+ }
35
+ function normalizeEntry(input) {
36
+ return {
37
+ initiativeId: input.initiativeId.trim(),
38
+ workstreamId: input.workstreamId.trim(),
39
+ preferredTaskId: normalizeNullableString(input.preferredTaskId),
40
+ preferredMilestoneId: normalizeNullableString(input.preferredMilestoneId),
41
+ createdAt: input.createdAt,
42
+ updatedAt: input.updatedAt,
43
+ };
44
+ }
45
+ export function readNextUpQueuePins() {
46
+ const file = storeFile();
47
+ try {
48
+ if (!existsSync(file)) {
49
+ return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
50
+ }
51
+ const raw = readFileSync(file, "utf8");
52
+ const parsed = parseJson(raw);
53
+ if (!parsed || typeof parsed !== "object") {
54
+ backupCorruptFileSync(file);
55
+ return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
56
+ }
57
+ const pins = Array.isArray(parsed.pins) ? parsed.pins : [];
58
+ return {
59
+ version: 1,
60
+ updatedAt: typeof parsed.updatedAt === "string"
61
+ ? parsed.updatedAt
62
+ : new Date().toISOString(),
63
+ pins: pins
64
+ .filter((entry) => Boolean(entry && typeof entry === "object"))
65
+ .map((entry) => normalizeEntry(entry)),
66
+ };
67
+ }
68
+ catch {
69
+ return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
70
+ }
71
+ }
72
+ export function upsertNextUpQueuePin(input) {
73
+ const initiativeId = input.initiativeId.trim();
74
+ const workstreamId = input.workstreamId.trim();
75
+ if (!initiativeId || !workstreamId) {
76
+ return readNextUpQueuePins();
77
+ }
78
+ ensureStoreDir();
79
+ const now = new Date().toISOString();
80
+ const next = readNextUpQueuePins();
81
+ const key = `${initiativeId}:${workstreamId}`;
82
+ const existing = next.pins.find((pin) => `${pin.initiativeId}:${pin.workstreamId}` === key);
83
+ const updated = normalizeEntry({
84
+ initiativeId,
85
+ workstreamId,
86
+ preferredTaskId: input.preferredTaskId ?? existing?.preferredTaskId ?? null,
87
+ preferredMilestoneId: input.preferredMilestoneId ?? existing?.preferredMilestoneId ?? null,
88
+ createdAt: existing?.createdAt ?? now,
89
+ updatedAt: now,
90
+ });
91
+ next.pins = [updated, ...next.pins.filter((pin) => `${pin.initiativeId}:${pin.workstreamId}` !== key)].slice(0, MAX_PINS);
92
+ next.updatedAt = now;
93
+ try {
94
+ writeJsonFileAtomicSync(storeFile(), next, 0o600);
95
+ }
96
+ catch {
97
+ // best effort
98
+ }
99
+ return next;
100
+ }
101
+ export function removeNextUpQueuePin(input) {
102
+ const initiativeId = input.initiativeId.trim();
103
+ const workstreamId = input.workstreamId.trim();
104
+ if (!initiativeId || !workstreamId) {
105
+ return readNextUpQueuePins();
106
+ }
107
+ ensureStoreDir();
108
+ const next = readNextUpQueuePins();
109
+ const key = `${initiativeId}:${workstreamId}`;
110
+ const filtered = next.pins.filter((pin) => `${pin.initiativeId}:${pin.workstreamId}` !== key);
111
+ if (filtered.length === next.pins.length)
112
+ return next;
113
+ next.pins = filtered;
114
+ next.updatedAt = new Date().toISOString();
115
+ try {
116
+ writeJsonFileAtomicSync(storeFile(), next, 0o600);
117
+ }
118
+ catch {
119
+ // best effort
120
+ }
121
+ return next;
122
+ }
123
+ export function setNextUpQueuePinOrder(input) {
124
+ ensureStoreDir();
125
+ const next = readNextUpQueuePins();
126
+ const now = new Date().toISOString();
127
+ const byKey = new Map(next.pins.map((pin) => [`${pin.initiativeId}:${pin.workstreamId}`, pin]));
128
+ const ordered = [];
129
+ const seen = new Set();
130
+ for (const entry of input.order) {
131
+ const initiativeId = (entry.initiativeId ?? "").trim();
132
+ const workstreamId = (entry.workstreamId ?? "").trim();
133
+ if (!initiativeId || !workstreamId)
134
+ continue;
135
+ const key = `${initiativeId}:${workstreamId}`;
136
+ if (seen.has(key))
137
+ continue;
138
+ seen.add(key);
139
+ const pin = byKey.get(key);
140
+ if (pin) {
141
+ ordered.push(pin);
142
+ }
143
+ else {
144
+ ordered.push({
145
+ initiativeId,
146
+ workstreamId,
147
+ preferredTaskId: null,
148
+ preferredMilestoneId: null,
149
+ createdAt: now,
150
+ updatedAt: now,
151
+ });
152
+ }
153
+ }
154
+ for (const pin of next.pins) {
155
+ const key = `${pin.initiativeId}:${pin.workstreamId}`;
156
+ if (seen.has(key))
157
+ continue;
158
+ ordered.push(pin);
159
+ }
160
+ next.pins = ordered.slice(0, MAX_PINS);
161
+ next.updatedAt = now;
162
+ try {
163
+ writeJsonFileAtomicSync(storeFile(), next, 0o600);
164
+ }
165
+ catch {
166
+ // best effort
167
+ }
168
+ return next;
169
+ }