@dev-mainsequence/codex-plugin 0.3.0 → 0.7.9

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 (39) hide show
  1. package/.codex-plugin/plugin.json +6 -5
  2. package/.mcp.json +17 -0
  3. package/README.md +49 -11
  4. package/assets/mainsequence-mark.svg +453 -0
  5. package/build-info.json +3 -3
  6. package/dist/hosts/codexPresentation.js +1 -0
  7. package/dist/hosts/discovery.js +252 -0
  8. package/dist/hosts/messaging.js +63 -0
  9. package/dist/hosts/panel/panel.html +265 -0
  10. package/dist/hosts/workspace.js +107 -0
  11. package/dist/panel-server.js +223 -0
  12. package/dist/vendor/devtools/index.js +45 -1
  13. package/dist/vendor/devtools/package.json +1 -1
  14. package/dist/vendor/devtools/shared/accountScope.js +19 -0
  15. package/dist/vendor/devtools/shared/actions.js +44 -7
  16. package/dist/vendor/devtools/shared/agentAssets.js +38 -0
  17. package/dist/vendor/devtools/shared/agentDelivery.js +477 -0
  18. package/dist/vendor/devtools/shared/agentIcon.js +43 -0
  19. package/dist/vendor/devtools/shared/agents.js +18 -7
  20. package/dist/vendor/devtools/shared/arguments.js +16 -9
  21. package/dist/vendor/devtools/shared/deployments.js +214 -0
  22. package/dist/vendor/devtools/shared/environments.js +38 -0
  23. package/dist/vendor/devtools/shared/mcp.js +237 -46
  24. package/dist/vendor/devtools/shared/mcpTransport.js +79 -23
  25. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +8 -3
  26. package/dist/vendor/devtools/shared/organizationProjects.js +2 -13
  27. package/dist/vendor/devtools/shared/projectTools.js +437 -0
  28. package/dist/vendor/devtools/shared/repositories.js +2 -2
  29. package/dist/vendor/devtools/shared/session.js +133 -14
  30. package/dist/vendor/devtools/shared/sharedTransport.js +506 -0
  31. package/dist/vendor/devtools/shared/stateDirectory.js +44 -0
  32. package/dist/vendor/devtools/shared/workingDirectory.js +13 -0
  33. package/dist/vendor/devtools/shared/workspaceSettings.js +96 -0
  34. package/docs/agents-tools-panel.md +522 -0
  35. package/docs/codex-verification.md +37 -0
  36. package/docs/conversational-mcp.md +16 -3
  37. package/package.json +9 -5
  38. package/skills/mainsequence-mcp/SKILL.md +19 -4
  39. package/skills/ms/assets/mainsequence-mark.svg +453 -0
@@ -6,6 +6,7 @@ const models_1 = require("./models.js");
6
6
  const session_1 = require("./session.js");
7
7
  const repositoryModels_1 = require("./repositoryModels.js");
8
8
  const repositoryClient_1 = require("./repositoryClient.js");
9
+ const environments_1 = require("./environments.js");
9
10
  function projectUid(value, label = 'UID') {
10
11
  if (typeof value !== 'string' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value))
11
12
  throw new models_1.ToolingError('INVALID_REQUEST', `${label} must be a UUID.`);
@@ -58,19 +59,7 @@ class OrganizationProjectClient {
58
59
  }
59
60
  async get(apiPath) { this.signal?.throwIfAborted(); return this.session.request(this.backend, 'GET', apiPath); }
60
61
  async environments() {
61
- const items = [];
62
- for (let offset = 0;;) {
63
- const payload = pageValues(await this.get(`/api/v1/organization-environments/?limit=100&offset=${offset}`));
64
- const rows = payload.results.map(row => ({ uid: projectUid(object(row).uid), name: line(row.name, 'environment name') }));
65
- if (rows.some(row => items.some(item => item.uid === row.uid)))
66
- throw new models_1.ToolingError('API_ERROR', 'Environment pagination returned duplicate rows.');
67
- items.push(...rows);
68
- offset += rows.length;
69
- if (offset >= payload.count)
70
- return items;
71
- if (!rows.length)
72
- throw new models_1.ToolingError('API_ERROR', 'Environment pagination stopped before completion.');
73
- }
62
+ return (await (0, environments_1.listEnvironments)(this.session, this.backend, this.signal)).map(({ uid, name }) => ({ uid, name }));
74
63
  }
75
64
  async list(environment, options = {}) {
76
65
  const environmentUid = projectUid(environment, 'Environment UID'), limit = options.limit ?? 25, offset = options.offset ?? 0;
@@ -0,0 +1,437 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.projectToolCatalogPath = exports.projectToolConnectionLabel = exports.isProjectToolName = exports.MAX_PROJECT_TOOL_TIMEOUT_MS = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = exports.PROJECT_TOOL_TIMEOUT_HINT = exports.MAX_PROJECT_TOOLS_PER_CONNECTION = exports.PROJECT_TOOL_PREFIX = void 0;
37
+ exports.projectToolName = projectToolName;
38
+ exports.projectToolConnection = projectToolConnection;
39
+ exports.projectToolDefinition = projectToolDefinition;
40
+ exports.projectToolDefinitions = projectToolDefinitions;
41
+ exports.projectToolBinding = projectToolBinding;
42
+ exports.projectToolRegistration = projectToolRegistration;
43
+ exports.projectToolResult = projectToolResult;
44
+ exports.projectToolTimeoutMs = projectToolTimeoutMs;
45
+ exports.readProjectToolCatalog = readProjectToolCatalog;
46
+ exports.readProjectToolCatalogFile = readProjectToolCatalogFile;
47
+ exports.createProjectToolCatalog = createProjectToolCatalog;
48
+ const fs = __importStar(require("node:fs"));
49
+ const fsp = __importStar(require("node:fs/promises"));
50
+ const path = __importStar(require("node:path"));
51
+ const node_crypto_1 = require("node:crypto");
52
+ const models_1 = require("./models.js");
53
+ const stateDirectory_1 = require("./stateDirectory.js");
54
+ exports.PROJECT_TOOL_PREFIX = 'ms_';
55
+ exports.MAX_PROJECT_TOOLS_PER_CONNECTION = 80;
56
+ /** A project may advertise how long one of its tools can take; the client honours it up to the cap. */
57
+ exports.PROJECT_TOOL_TIMEOUT_HINT = 'mainsequence.ai/timeout-seconds/v1';
58
+ exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = 120_000;
59
+ exports.MAX_PROJECT_TOOL_TIMEOUT_MS = 300_000;
60
+ const CATALOG_FILE = 'project-tool-catalog.json';
61
+ const LOCK_STALE_MS = 10 * 60_000;
62
+ const UUID = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
63
+ const HINTS = ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'];
64
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
65
+ const text = (value, max = 300) => typeof value === 'string' ? value.trim().slice(0, max) : '';
66
+ const clean = (value, max) => value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '').slice(0, max);
67
+ /** `ms_` + the remote name (unsafe characters replaced, cut to 32) + `_` + FNV-1a 32-bit of
68
+ * `backend:environment:release:tool`. The hash binds the saved connection and environment; the
69
+ * prefix keeps the remote name readable. The cut is 32 because Codex qualifies the name as
70
+ * `mcp__mainsequence__<name>` under a 64-character limit; for remote names up to 32 characters
71
+ * the result is the name Claude Desktop has registered since 0.4.13. */
72
+ function projectToolName(backend, environmentUid, releaseUid, tool) {
73
+ const value = `${backend}:${environmentUid}:${releaseUid}:${tool}`;
74
+ let hash = 2166136261;
75
+ for (let i = 0; i < value.length; i++)
76
+ hash = Math.imul(hash ^ value.charCodeAt(i), 16777619) >>> 0;
77
+ return `${exports.PROJECT_TOOL_PREFIX}${String(tool).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 32)}_${hash.toString(16)}`;
78
+ }
79
+ const isProjectToolName = (name) => typeof name === 'string' && name.startsWith(exports.PROJECT_TOOL_PREFIX);
80
+ exports.isProjectToolName = isProjectToolName;
81
+ function hints(value) {
82
+ const result = {};
83
+ for (const flag of HINTS)
84
+ if (object(value) && typeof value[flag] === 'boolean')
85
+ result[flag] = value[flag];
86
+ return result;
87
+ }
88
+ /** Only what a host needs to register and call a tool: name, description, object schema and the four hints. */
89
+ function normalizeTool(value) {
90
+ if (!object(value) || typeof value.name !== 'string' || !value.name || !object(value.inputSchema))
91
+ return;
92
+ return { name: value.name, ...(typeof value.description === 'string' ? { description: value.description } : {}), inputSchema: value.inputSchema, annotations: hints(value.annotations) };
93
+ }
94
+ const normalizeTools = (tools) => (Array.isArray(tools) ? tools : []).map(normalizeTool).filter((tool) => Boolean(tool)).slice(0, exports.MAX_PROJECT_TOOLS_PER_CONNECTION);
95
+ /** The release's own name, else its repository's name, else the release UID. */
96
+ const projectToolConnectionLabel = (connection) => text(connection.name) || text(connection.display_name) || connection.resource_release_uid;
97
+ exports.projectToolConnectionLabel = projectToolConnectionLabel;
98
+ /** The catalog record of a saved connection and its current tools. */
99
+ function projectToolConnection(connection, tools = []) {
100
+ return { backend: connection.backend, releaseUid: connection.resource_release_uid.toLowerCase(), environmentUid: connection.mcp_connection.organization_environment_uid.toLowerCase(),
101
+ environmentName: text(connection.mcp_connection.organization_environment_name), name: (0, exports.projectToolConnectionLabel)(connection), tools: normalizeTools(tools) };
102
+ }
103
+ const source = (connection) => 'mcp_connection' in connection ? projectToolConnection(connection) : connection;
104
+ /** The host-neutral definition of one remote tool: `MainSequence <connection> [<environment>]: <description>`,
105
+ * an object schema (`type: 'object'` added when the server omitted it) and the four boolean hints,
106
+ * open-world by default. */
107
+ function projectToolDefinition(connection, tool) {
108
+ const c = source(connection), label = c.name || c.releaseUid;
109
+ const schema = object(tool.inputSchema) ? tool.inputSchema : {};
110
+ return { name: projectToolName(c.backend, c.environmentUid, c.releaseUid, tool.name), title: `${label}: ${tool.name}`,
111
+ description: clean(`MainSequence ${label} [${c.environmentName || c.environmentUid}]: ${tool.description || tool.name}`, 2000),
112
+ inputSchema: schema.type === 'object' ? schema : { type: 'object', ...schema }, annotations: { openWorldHint: true, ...hints(tool.annotations) } };
113
+ }
114
+ /** Up to 80 definitions per connection, one per distinct name. */
115
+ function projectToolDefinitions(connection, tools) {
116
+ const seen = new Set(), definitions = [];
117
+ for (const tool of normalizeTools(tools ?? ('tools' in connection ? connection.tools : []))) {
118
+ const definition = projectToolDefinition(connection, tool);
119
+ if (seen.has(definition.name))
120
+ continue;
121
+ seen.add(definition.name);
122
+ definitions.push(definition);
123
+ }
124
+ return definitions;
125
+ }
126
+ /** What a host sends back to `mcp-call` for a registered tool. */
127
+ function projectToolBinding(connection, tool) {
128
+ const c = source(connection);
129
+ return { backend: c.backend, environmentUid: c.environmentUid, environmentName: c.environmentName, releaseUid: c.releaseUid, connection: c.name || c.releaseUid, tool: typeof tool === 'string' ? tool : tool.name };
130
+ }
131
+ const inScope = (connection, scope) => (!scope?.backend || connection.backend === scope.backend) && (!scope?.environmentUid || connection.environmentUid === scope.environmentUid.toLowerCase());
132
+ /** Definitions and bindings for every tool of the catalog's connections in scope. */
133
+ function projectToolRegistration(catalog, scope) {
134
+ const definitions = [], bindings = new Map();
135
+ for (const connection of catalog?.connections ?? []) {
136
+ if (!inScope(connection, scope))
137
+ continue;
138
+ for (const tool of connection.tools.slice(0, exports.MAX_PROJECT_TOOLS_PER_CONNECTION)) {
139
+ const definition = projectToolDefinition(connection, tool);
140
+ if (bindings.has(definition.name))
141
+ continue;
142
+ bindings.set(definition.name, projectToolBinding(connection, tool));
143
+ definitions.push(definition);
144
+ }
145
+ }
146
+ return { definitions, bindings };
147
+ }
148
+ /** The host-neutral result of a forwarded `mcp-call`: the remote content blocks, then one JSON text
149
+ * block with `summary`, `effects`, `error` and `structuredContent` when the call failed, returned
150
+ * structured content or no blocks at all; `isError` on any non-success, including `MCP_TOOL_ERROR`,
151
+ * whose original remote content is preserved. A failure without structured content carries that
152
+ * JSON as `structuredContent`, so a host that reads only structured output still sees the error. */
153
+ function projectToolResult(result) {
154
+ const data = object(result.data) ? result.data : undefined;
155
+ const blocks = Array.isArray(data?.content) ? data.content.filter((block) => object(block) && typeof block.type === 'string') : [];
156
+ const failed = result.status !== 'success', structured = data?.structuredContent;
157
+ const extra = { summary: result.summary, ...(result.effects ? { effects: result.effects } : {}), ...(result.error ? { error: result.error } : {}), ...(structured !== undefined ? { structuredContent: structured } : {}) };
158
+ const content = [...blocks, ...(!blocks.length || failed || structured !== undefined ? [{ type: 'text', text: JSON.stringify(extra) }] : [])];
159
+ return { ...(failed ? { isError: true } : {}), content, ...(structured !== undefined ? { structuredContent: structured } : failed ? { structuredContent: extra } : {}) };
160
+ }
161
+ /** The HTTP budget for one `tools/call`: the default, or the tool's advertised seconds when larger, capped. */
162
+ function projectToolTimeoutMs(tool, defaultMs = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS) {
163
+ const hint = object(tool?._meta) ? tool._meta[exports.PROJECT_TOOL_TIMEOUT_HINT] : undefined;
164
+ if (typeof hint !== 'number' || !Number.isFinite(hint) || hint <= 0)
165
+ return defaultMs;
166
+ return Math.max(defaultMs, Math.min(Math.round(hint * 1000), exports.MAX_PROJECT_TOOL_TIMEOUT_MS));
167
+ }
168
+ function success(result) {
169
+ if (result.status !== 'success')
170
+ throw new models_1.ToolingError(result.error?.code ?? 'API_ERROR', result.summary);
171
+ if (result.data === undefined)
172
+ throw new models_1.ToolingError('API_ERROR', 'MainSequence returned no data.');
173
+ return result.data;
174
+ }
175
+ /** Every saved connection in scope and its current tools: `mcp-list` (read-only), then one `mcp-tools`
176
+ * per connection, two at a time (the shared transport's per-origin limit). A connection whose catalog
177
+ * cannot be read now keeps its `previous` tools and is named in `unavailable`; a removed connection
178
+ * loses them; connections outside the scope are carried over from `previous` unchanged. No account
179
+ * snapshot is attached: a registered tool is bound to a saved connection, and the session for that
180
+ * connection's backend is verified when the tool is called. */
181
+ async function readProjectToolCatalog(dispatch, options = {}) {
182
+ const status = options.session ?? success(await dispatch({ action: 'auth-status', ...(options.backend ? { backend: options.backend } : {}) }));
183
+ if (!status.authenticated || !status.backend)
184
+ throw new models_1.ToolingError('NOT_AUTHENTICATED', 'Sign in to MainSequence to register saved project MCP tools.');
185
+ const backend = status.backend, environmentUid = options.environmentUid?.toLowerCase();
186
+ if (environmentUid !== undefined && !UUID.test(environmentUid))
187
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The environment must be an organization environment UUID.');
188
+ const scope = { backend, environmentUid }, environment = environmentUid ? { environmentUid } : {};
189
+ const rows = success(await dispatch({ action: 'mcp-list', backend, readOnly: true, ...environment }));
190
+ const previous = options.previous?.connections ?? [];
191
+ const connections = previous.filter(connection => !inScope(connection, scope)), unavailable = [];
192
+ let retained = 0;
193
+ const pending = [...rows];
194
+ await Promise.all(Array.from({ length: Math.min(2, pending.length) }, async () => {
195
+ while (pending.length) {
196
+ options.signal?.throwIfAborted();
197
+ const row = pending.shift();
198
+ try {
199
+ const output = success(await dispatch({ action: 'mcp-tools', backend, releaseUid: row.resource_release_uid, ...environment }));
200
+ const current = output.connection;
201
+ connections.push(projectToolConnection({ ...row, name: row.name ?? current?.name, display_name: row.display_name ?? current?.display_name,
202
+ mcp_connection: { ...row.mcp_connection, organization_environment_name: row.mcp_connection.organization_environment_name ?? current?.mcp_connection?.organization_environment_name } }, output.tools));
203
+ }
204
+ catch (error) {
205
+ if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
206
+ throw error;
207
+ options.signal?.throwIfAborted();
208
+ const earlier = previous.find(connection => connection.backend === backend && connection.releaseUid === row.resource_release_uid.toLowerCase());
209
+ if (earlier) {
210
+ connections.push(earlier);
211
+ retained += earlier.tools.length;
212
+ }
213
+ unavailable.push((0, exports.projectToolConnectionLabel)(row));
214
+ }
215
+ }
216
+ }));
217
+ connections.sort((a, b) => a.backend.localeCompare(b.backend) || a.environmentUid.localeCompare(b.environmentUid) || a.releaseUid.localeCompare(b.releaseUid));
218
+ return { catalog: { version: 1, updatedAt: new Date().toISOString(), connections }, backend, unavailable, retained };
219
+ }
220
+ function connectionRecord(value) {
221
+ if (!object(value) || typeof value.backend !== 'string' || !UUID.test(String(value.releaseUid)) || !UUID.test(String(value.environmentUid)) || !Array.isArray(value.tools))
222
+ return;
223
+ return { backend: value.backend, releaseUid: String(value.releaseUid).toLowerCase(), environmentUid: String(value.environmentUid).toLowerCase(),
224
+ environmentName: text(value.environmentName), name: text(value.name), tools: normalizeTools(value.tools) };
225
+ }
226
+ /** Beside `mcp-connections.json`, shared by every host on the machine. */
227
+ const projectToolCatalogPath = (env = process.env) => path.join((0, stateDirectory_1.mainSequenceStateDirectory)(env), CATALOG_FILE);
228
+ exports.projectToolCatalogPath = projectToolCatalogPath;
229
+ /** A missing or damaged cache is an empty catalog, never an error. */
230
+ function readProjectToolCatalogFile(file) {
231
+ try {
232
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
233
+ if (!object(raw) || raw.version !== 1 || !Array.isArray(raw.connections))
234
+ return null;
235
+ return { version: 1, updatedAt: text(raw.updatedAt, 40), connections: raw.connections.map(connectionRecord).filter((item) => Boolean(item)) };
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ const sleep = (ms, signal) => new Promise(resolve => {
242
+ const timer = setTimeout(done, ms);
243
+ function done() { clearTimeout(timer); signal?.removeEventListener('abort', done); resolve(); }
244
+ signal?.addEventListener('abort', done, { once: true });
245
+ });
246
+ /** The file-backed catalog: read at once from the shared cache so a host can register tools before
247
+ * any network request, refreshed under a lock so one process reads the catalogs at a time while the
248
+ * others pick its result up from the file, and reloaded when another process wrote it. */
249
+ function createProjectToolCatalog(options) {
250
+ const file = options.file ?? (0, exports.projectToolCatalogPath)(options.env), staleMs = options.staleMs ?? 15 * 60_000;
251
+ let catalog = readProjectToolCatalogFile(file), loadedAt = mtime(), running = null;
252
+ let current = null;
253
+ // The scope of the last refresh: the current account's backend (learned from auth-status), so a
254
+ // listing shows this host's connections even when another backend's are cached beside them.
255
+ let known;
256
+ const state = { unavailable: [], retained: 0, error: null };
257
+ function mtime() { try {
258
+ return fs.statSync(file).mtimeMs;
259
+ }
260
+ catch {
261
+ return 0;
262
+ } }
263
+ /** Another process may have refreshed the shared cache since this one read it. */
264
+ function reload() {
265
+ const now = mtime();
266
+ if (now === loadedAt)
267
+ return;
268
+ catalog = readProjectToolCatalogFile(file);
269
+ loadedAt = now;
270
+ }
271
+ const registration = (scope) => projectToolRegistration(catalog, scope ?? known);
272
+ async function write(next) {
273
+ await fsp.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
274
+ const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
275
+ try {
276
+ await fsp.writeFile(temporary, JSON.stringify(next, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
277
+ await fsp.rename(temporary, file);
278
+ }
279
+ finally {
280
+ await fsp.rm(temporary, { force: true });
281
+ }
282
+ }
283
+ function adopt(next, scope) {
284
+ const before = JSON.stringify(registration().definitions);
285
+ if (next) {
286
+ catalog = next;
287
+ loadedAt = mtime();
288
+ }
289
+ if (scope)
290
+ known = scope;
291
+ if (JSON.stringify(registration().definitions) !== before)
292
+ options.notify?.();
293
+ }
294
+ /** One process refreshes at a time. A live lock means another process is refreshing: without a
295
+ * wait the caller reuses its result from the file; with one it waits for the lock up to `waitMs`.
296
+ * An interrupted refresh from a closed host must not block every later one (stale after 10 min). */
297
+ async function lock(waitMs, signal) {
298
+ const name = file + '.lock', deadline = Date.now() + waitMs;
299
+ for (let attempt = 0; attempt < 10_000; attempt++) {
300
+ signal.throwIfAborted();
301
+ try {
302
+ const handle = await fsp.open(name, 'wx', 0o600);
303
+ await handle.writeFile(JSON.stringify({ pid: process.pid, at: new Date().toISOString() }));
304
+ return async () => { await handle.close(); await fsp.rm(name, { force: true }); };
305
+ }
306
+ catch (error) {
307
+ if (error?.code === 'ENOENT') {
308
+ await fsp.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
309
+ continue;
310
+ }
311
+ if (error?.code !== 'EEXIST')
312
+ return null;
313
+ let stale;
314
+ try {
315
+ stale = Date.now() - (await fsp.stat(name)).mtimeMs >= LOCK_STALE_MS;
316
+ }
317
+ catch {
318
+ continue;
319
+ }
320
+ if (stale) {
321
+ await fsp.rm(name, { force: true });
322
+ continue;
323
+ }
324
+ if (Date.now() >= deadline)
325
+ return null;
326
+ await sleep(250, signal);
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+ return {
332
+ file,
333
+ /** Definitions for the host's tool list, from the newest shared cache. */
334
+ list(scope) { reload(); return registration(scope).definitions; },
335
+ binding(name) { reload(); return registration({}).bindings.get(name); },
336
+ state(scope) {
337
+ reload();
338
+ const selected = scope ?? known;
339
+ return { count: registration(scope).definitions.length,
340
+ connections: (catalog?.connections ?? []).filter(connection => inScope(connection, selected)).map(connection => ({ backend: connection.backend, releaseUid: connection.releaseUid,
341
+ environmentUid: connection.environmentUid, environmentName: connection.environmentName, name: connection.name, tools: connection.tools.length })),
342
+ updatedAt: catalog?.updatedAt || null, refreshing: running !== null, unavailable: state.unavailable, retained: state.retained, error: state.error };
343
+ },
344
+ /** The state plus definitions and bindings, as the `mcp-catalog` action returns them. */
345
+ view(scope) {
346
+ const { definitions, bindings } = registration(scope);
347
+ return { ...this.state(scope), definitions, bindings: Object.fromEntries(bindings) };
348
+ },
349
+ /** Read every saved connection's current catalog. Without `force`, a fresh cache is kept. Resolves
350
+ * true when this process read and wrote the catalog; the state carries why it did not. */
351
+ refresh(force = false, extra = {}) {
352
+ if (options.disabled)
353
+ return Promise.resolve(false);
354
+ if (running)
355
+ return running;
356
+ reload();
357
+ const controller = new AbortController();
358
+ current = { controller, locked: false };
359
+ running = (async () => {
360
+ const dispatch = options.dispatch(controller.signal), scope = extra.scope;
361
+ const fresh = () => Boolean(catalog && Date.parse(catalog.updatedAt) > Date.now() - staleMs);
362
+ try {
363
+ // A fresh cache with a known scope needs nothing; otherwise one local session read tells
364
+ // whether there is a session to read catalogs with, and which backend the listing is for.
365
+ if (!force && (known || scope) && fresh()) {
366
+ if (scope)
367
+ adopt(null, scope);
368
+ return false;
369
+ }
370
+ const session = success(await dispatch({ action: 'auth-status', ...(scope?.backend ? { backend: scope.backend } : {}) }));
371
+ if (session.backend)
372
+ adopt(null, scope ?? { backend: session.backend });
373
+ if (!session.authenticated || !session.backend)
374
+ throw new models_1.ToolingError('NOT_AUTHENTICATED', 'Sign in to MainSequence to register saved project MCP tools.');
375
+ if (!force && fresh())
376
+ return false;
377
+ const release = await lock(extra.waitMs ?? 0, controller.signal);
378
+ if (!release) {
379
+ if (extra.waitMs)
380
+ state.error = { code: 'PROJECT_BUSY', message: 'Another MainSequence process is refreshing the project tool catalog. The registered tools come from its last result; refresh again later.' };
381
+ return false;
382
+ }
383
+ current.locked = true;
384
+ try {
385
+ reload();
386
+ const read = await readProjectToolCatalog(dispatch, { ...scope, backend: session.backend, previous: catalog, signal: controller.signal, session });
387
+ await write(read.catalog);
388
+ state.unavailable = read.unavailable;
389
+ state.retained = read.retained;
390
+ state.error = null;
391
+ adopt(read.catalog, scope ?? { backend: read.backend });
392
+ return true;
393
+ }
394
+ finally {
395
+ current.locked = false;
396
+ await release();
397
+ }
398
+ }
399
+ catch (error) {
400
+ state.error = { code: error instanceof models_1.ToolingError ? error.code : 'INTERNAL_ERROR', message: error instanceof models_1.ToolingError ? error.message : 'The saved project MCP tools could not be refreshed.' };
401
+ return false;
402
+ }
403
+ })().finally(() => { running = null; current = null; });
404
+ return running;
405
+ },
406
+ /** A host process is ending: stop its refresh and free the shared lock for the next one at once. */
407
+ dispose() {
408
+ if (!current)
409
+ return;
410
+ current.controller.abort();
411
+ if (current.locked) {
412
+ try {
413
+ fs.rmSync(file + '.lock', { force: true });
414
+ }
415
+ catch { /* the stale-lock rule still applies */ }
416
+ }
417
+ },
418
+ /** A catalog just read explicitly for one saved release replaces that connection's tools without another round trip. */
419
+ async update(releaseUid, tools, backend) {
420
+ if (options.disabled)
421
+ return;
422
+ reload();
423
+ const index = catalog?.connections.findIndex(item => item.releaseUid === releaseUid.toLowerCase() && (!backend || item.backend === backend)) ?? -1;
424
+ if (!catalog || index < 0) {
425
+ void this.refresh(true);
426
+ return;
427
+ }
428
+ const connections = catalog.connections.map((item, i) => i === index ? { ...item, tools: normalizeTools(tools) } : item);
429
+ const next = { version: 1, updatedAt: new Date().toISOString(), connections };
430
+ try {
431
+ await write(next);
432
+ adopt(next);
433
+ }
434
+ catch { /* the in-memory tools are already current */ }
435
+ },
436
+ };
437
+ }
@@ -53,6 +53,7 @@ const session_1 = require("./session.js");
53
53
  const repositoryIdentity_1 = require("./repositoryIdentity.js");
54
54
  const repositoryClient_1 = require("./repositoryClient.js");
55
55
  const repositoryModels_1 = require("./repositoryModels.js");
56
+ const stateDirectory_1 = require("./stateDirectory.js");
56
57
  async function readProjectEnv(root) {
57
58
  let text;
58
59
  try {
@@ -92,8 +93,7 @@ async function resolveBackend(explicit, root, env = process.env) {
92
93
  /** Resolve non-secret workspace settings without requiring a session or writing configuration. */
93
94
  async function readWorkspacePreferences(options = {}) {
94
95
  const env = options.env ?? process.env, home = options.homeDir ?? os.homedir();
95
- const directory = process.platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'MainSequenceCLI')
96
- : process.platform === 'win32' ? path.join(env.APPDATA || home, 'MainSequenceCLI') : path.join(home, '.config', 'mainsequence');
96
+ const directory = (0, stateDirectory_1.mainSequenceStateDirectory)(env, home);
97
97
  const config = await fs.readFile(options.configPath ?? path.join(directory, 'config.json'), 'utf8')
98
98
  .then(text => JSON.parse(text)).catch(() => ({}));
99
99
  const folder = config?.mainsequence_path;