melchizedek-agents 1.2.0 → 1.3.1

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 (66) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +174 -0
  3. package/DOCUMENTATION.md +107 -10
  4. package/ONBOARDING.md +50 -11
  5. package/QUICKSTART.md +1 -1
  6. package/README.md +22 -2
  7. package/config/agents/examples/connectors.yaml +82 -0
  8. package/config/agents/syndicate.schema.json +324 -26
  9. package/dist/lib/a2a/app.js +3 -0
  10. package/dist/lib/a2a/oauthSetup.js +20 -5
  11. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  12. package/dist/lib/chatgpt/adapter.js +107 -0
  13. package/dist/lib/chatgpt/cli.d.ts +23 -0
  14. package/dist/lib/chatgpt/cli.js +77 -0
  15. package/dist/lib/chatgpt/oauth.d.ts +104 -0
  16. package/dist/lib/chatgpt/oauth.js +472 -0
  17. package/dist/lib/chatgpt/state.d.ts +98 -0
  18. package/dist/lib/chatgpt/state.js +145 -0
  19. package/dist/lib/chatgpt/store.d.ts +24 -0
  20. package/dist/lib/chatgpt/store.js +94 -0
  21. package/dist/lib/compile.js +40 -25
  22. package/dist/lib/dispatch.js +6 -2
  23. package/dist/lib/doctor.d.ts +34 -3
  24. package/dist/lib/doctor.js +63 -5
  25. package/dist/lib/loadSyndicate.d.ts +6 -0
  26. package/dist/lib/mcp/server.d.ts +195 -0
  27. package/dist/lib/mcp/server.js +564 -0
  28. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  29. package/dist/lib/models/adapterResolver.js +6 -1
  30. package/dist/lib/models/gateway.d.ts +1 -1
  31. package/dist/lib/models/gateway.js +4 -2
  32. package/dist/lib/models/gptAdapter.js +11 -1
  33. package/dist/lib/onboarding.d.ts +4 -3
  34. package/dist/lib/onboarding.js +48 -9
  35. package/dist/lib/runtime/syndicateTurn.js +30 -16
  36. package/dist/lib/syndicateSchema.d.ts +146 -8
  37. package/dist/lib/syndicateSchema.js +118 -11
  38. package/dist/lib/tools/credentialUses.d.ts +7 -1
  39. package/dist/lib/tools/credentialUses.js +10 -2
  40. package/dist/lib/tools/generateImageTool.js +2 -1
  41. package/dist/lib/tools/inspectImageTool.js +1 -1
  42. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  43. package/dist/lib/tools/mcpToolFactory.js +71 -25
  44. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  45. package/dist/lib/tools/mcpTransports.js +11 -0
  46. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  47. package/dist/lib/tools/oauthConsent.js +49 -14
  48. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  49. package/dist/lib/tools/oauthDiscovery.js +616 -0
  50. package/dist/lib/tools/oauthTools.d.ts +44 -12
  51. package/dist/lib/tools/oauthTools.js +79 -22
  52. package/dist/lib/tools/xApiSearchTool.js +2 -2
  53. package/dist/scripts/a2a_server.js +8 -0
  54. package/dist/scripts/assistant_worker.js +11 -1
  55. package/dist/scripts/doctor.d.ts +1 -1
  56. package/dist/scripts/doctor.js +1 -1
  57. package/dist/scripts/mcp_server.d.ts +12 -0
  58. package/dist/scripts/mcp_server.js +345 -0
  59. package/dist/scripts/setup.d.ts +2 -0
  60. package/dist/scripts/setup.js +27 -0
  61. package/dist/scripts/syndicate_chat.js +12 -3
  62. package/package.json +15 -12
  63. package/skills/README.md +2 -2
  64. package/skills/briefs/skills-readme.md +1 -1
  65. package/skills/melchizedek-onboard/SKILL.md +28 -6
  66. package/skills/melchizedek-onboard-keys/SKILL.md +17 -5
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/mcp_server.ts — the `melchizedek-mcp` bin: syndicates as MCP tools
4
+ * (ADR 0125). Thin: it reads flags and the environment, loads the syndicates,
5
+ * refuses unsafe binds, and hands everything to lib/mcp/server.ts.
6
+ *
7
+ * claude mcp add melch -- npx melchizedek-mcp
8
+ * melchizedek-mcp --syndicate research_desk --syndicate tutor
9
+ * melchizedek-mcp --http --port 4100
10
+ *
11
+ * Every console line goes to stderr: on stdio, stdout is the protocol.
12
+ *
13
+ * Flags:
14
+ * --syndicate <id> serve this syndicate (repeatable; a bare id may also
15
+ * resolve to the shipped examples/ and templates/).
16
+ * Positional ids work too. None: every YAML at the
17
+ * agents directory's root.
18
+ * --agents-dir <dir> the agents directory (MELCHIZEDEK_AGENTS_DIR, else
19
+ * <cwd>/config/agents)
20
+ * --http Streamable HTTP at /mcp instead of stdio
21
+ * --host <addr> HTTP bind (MCP_HOST, default 127.0.0.1)
22
+ * --port <n> HTTP port (MCP_PORT, default 4100)
23
+ *
24
+ * Environment (all optional):
25
+ * MCP_SERVER_SECRET bearer every HTTP request presents; required, 32+
26
+ * characters, on a bind beyond loopback
27
+ * MCP_ALLOWED_HOSTS comma list: Host names a non-loopback bind answers to
28
+ * MCP_TRUST_PROXY Express trust proxy (hop count, true/false, subnets)
29
+ * MCP_RATE_LIMIT_PER_MINUTE HTTP requests per minute per IP (240)
30
+ * MCP_USER_ID the scope a stdio client's sessions are stored under (default)
31
+ * DATABASE_URL Postgres for sessions, memory, turn locks, audit, credentials
32
+ * A2A_TASK_TIMEOUT_MS, A2A_MAX_CONCURRENT_TASKS, A2A_MAX_CONCURRENT_PER_SCOPE,
33
+ * A2A_MAX_CONCURRENT_PER_CALLER, A2A_TURN_LOCK_WAIT_MS, A2A_BUDGETS,
34
+ * A2A_SHUTDOWN_GRACE_MS, MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI,
35
+ * OAUTH_CALLBACK_IDENTITY, MELCHIZEDEK_OAUTH_HOSTS, MELCHIZEDEK_CREDENTIAL_HOSTS
36
+ * as melchizedek-serve reads them (scripts/a2a_server.ts)
37
+ */
38
+ import { readdirSync, realpathSync } from 'node:fs';
39
+ import path from 'node:path';
40
+ import { pathToFileURL } from 'node:url';
41
+ /** The bin's flags. Throws, naming the flag, on one it does not know. */
42
+ export function parseMcpArgs(argv) {
43
+ const out = { syndicates: [], http: false, help: false };
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ const value = () => {
47
+ const v = argv[++i];
48
+ if (v === undefined || v.startsWith('--'))
49
+ throw new Error(`${a} needs a value`);
50
+ return v;
51
+ };
52
+ if (a === '--help' || a === '-h')
53
+ out.help = true;
54
+ else if (a === '--http')
55
+ out.http = true;
56
+ else if (a === '--stdio')
57
+ out.http = false;
58
+ else if (a === '--syndicate')
59
+ out.syndicates.push(value());
60
+ else if (a === '--agents-dir')
61
+ out.agentsDir = value();
62
+ else if (a === '--host')
63
+ out.host = value();
64
+ else if (a === '--port') {
65
+ const n = Number(value());
66
+ if (!Number.isInteger(n) || n < 0 || n > 65535)
67
+ throw new Error('--port must be a port number');
68
+ out.port = n;
69
+ }
70
+ else if (a.startsWith('-'))
71
+ throw new Error(`Unknown flag ${a} (see --help)`);
72
+ else
73
+ out.syndicates.push(a);
74
+ }
75
+ return out;
76
+ }
77
+ const HELP = [
78
+ 'Usage: melchizedek-mcp [--syndicate <id>]... [--agents-dir <dir>] [--http [--host <addr>] [--port <n>]]',
79
+ '',
80
+ 'Serves syndicates as MCP tools: one tool per syndicate ({ message, session_id? }),',
81
+ 'plus melch_resume for a turn that paused (an approval, a question, an authorization).',
82
+ 'stdio by default (Claude Code: claude mcp add melch -- npx melchizedek-mcp);',
83
+ '--http serves Streamable HTTP at /mcp on 127.0.0.1; a wider --host needs MCP_SERVER_SECRET.',
84
+ 'Environment variables are listed at the top of scripts/mcp_server.ts.',
85
+ ].join('\n');
86
+ function envInt(name, fallback) {
87
+ const raw = process.env[name]?.trim();
88
+ if (!raw)
89
+ return fallback;
90
+ const n = Number(raw);
91
+ if (!Number.isFinite(n) || n < 0)
92
+ throw new Error(`${name} must be a non-negative number`);
93
+ return Math.floor(n);
94
+ }
95
+ function trustProxy() {
96
+ const raw = process.env.MCP_TRUST_PROXY?.trim();
97
+ if (!raw)
98
+ return false;
99
+ if (/^\d+$/.test(raw))
100
+ return Number(raw);
101
+ if (raw === 'true')
102
+ return true;
103
+ if (raw === 'false')
104
+ return false;
105
+ return raw;
106
+ }
107
+ function fatal(message) {
108
+ console.error(`[MCP] ✗ FATAL: ${message}`);
109
+ process.exit(1);
110
+ }
111
+ export async function startMcpServer(argv = process.argv.slice(2)) {
112
+ let args;
113
+ try {
114
+ args = parseMcpArgs(argv);
115
+ }
116
+ catch (err) {
117
+ fatal(err instanceof Error ? err.message : String(err));
118
+ }
119
+ if (args.help) {
120
+ console.error(HELP);
121
+ process.exit(0);
122
+ }
123
+ if (args.agentsDir)
124
+ process.env.MELCHIZEDEK_AGENTS_DIR = path.resolve(args.agentsDir);
125
+ const { loadEnv, isPlaceholderValue } = await import('../lib/loadEnv.js');
126
+ loadEnv(import.meta.url);
127
+ // Sign in with ChatGPT is local only (ADR 0126). Over stdio this bin is one person's process, spawned by
128
+ // their own MCP client; --http can be reached by others, so it refuses like melchizedek-serve.
129
+ if (args.http) {
130
+ const { refuseChatGptSignInOnServedSurface } = await import('../lib/chatgpt/state.js');
131
+ try {
132
+ refuseChatGptSignInOnServedSurface('melchizedek-mcp --http');
133
+ }
134
+ catch (err) {
135
+ fatal(err instanceof Error ? err.message : String(err));
136
+ }
137
+ }
138
+ if (process.env.OTEL_CONSOLE_SPANS === undefined)
139
+ process.env.OTEL_CONSOLE_SPANS = 'false';
140
+ const { setLogLevel } = await import('../lib/runtime/logging.js');
141
+ setLogLevel('warn');
142
+ const log = (m) => console.error(`[MCP] ${m}`);
143
+ const warn = (m) => console.error(`[MCP] ⚠ ${m}`);
144
+ const { createMcpServer, mcpBindProblem, mcpHttpApp, serveMcpStdio, toolNameFor } = await import('../lib/mcp/server.js');
145
+ const { loadSyndicate, nestedLoader } = await import('../lib/loadSyndicate.js');
146
+ const { syndicateOAuthHostProblems } = await import('../lib/tools/oauthTools.js');
147
+ const { syndicateCredentialHostProblems } = await import('../lib/tools/credentialUses.js');
148
+ // ── Transport posture first: a refused bind loads nothing ────────────────
149
+ const host = args.host ?? (process.env.MCP_HOST?.trim() || '127.0.0.1');
150
+ const port = args.port ?? envInt('MCP_PORT', 4100);
151
+ let secret = process.env.MCP_SERVER_SECRET?.trim() || undefined;
152
+ if (secret && isPlaceholderValue(secret))
153
+ fatal('MCP_SERVER_SECRET is still a placeholder. Generate one: openssl rand -hex 32');
154
+ if (args.http) {
155
+ const problem = mcpBindProblem({ host, ...(secret ? { secret } : {}) });
156
+ if (problem)
157
+ fatal(problem);
158
+ }
159
+ // ── The syndicates: the exposure ──────────────────────────────────────────
160
+ const bindings = () => ({
161
+ current_date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
162
+ });
163
+ const agentsDir = path.resolve(process.env.MELCHIZEDEK_AGENTS_DIR ?? path.join(process.cwd(), 'config', 'agents'));
164
+ let files = args.syndicates.map((id) => (id.endsWith('.yaml') || id.endsWith('.yml') ? id : `${id}.yaml`));
165
+ const named = files.length > 0;
166
+ if (!named) {
167
+ try {
168
+ files = readdirSync(agentsDir)
169
+ .filter((f) => (f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
170
+ .sort();
171
+ }
172
+ catch {
173
+ files = [];
174
+ }
175
+ if (!files.length)
176
+ fatal(`No syndicates at the root of ${agentsDir}. Name one: melchizedek-mcp --syndicate <id> (a shipped example works: --syndicate tutor).`);
177
+ }
178
+ const syndicates = [];
179
+ for (const file of files) {
180
+ try {
181
+ const config = loadSyndicate(file, { bindings: bindings(), shippedFallback: named });
182
+ // The operator's host allowlists hold here as on the A2A server (ADR 0114, ADR 0122).
183
+ const nested = nestedLoader(config, (ref) => loadSyndicate(ref, { bindings: bindings() }));
184
+ const load = (ref) => {
185
+ try {
186
+ return nested(ref);
187
+ }
188
+ catch {
189
+ return {};
190
+ }
191
+ };
192
+ const problems = [...syndicateOAuthHostProblems([config], { load }), ...syndicateCredentialHostProblems([config], { load })];
193
+ if (problems.length)
194
+ throw new Error(`refused by the operator's host allowlists:\n - ${problems.join('\n - ')}`);
195
+ syndicates.push({ id: toolNameFor(path.basename(file)), config });
196
+ }
197
+ catch (err) {
198
+ const why = err?.code === 'ENOENT' ? 'not found' : err instanceof Error ? err.message : String(err);
199
+ if (named)
200
+ fatal(`${file}: ${why}`);
201
+ warn(`Skipped ${file}: ${why.split('\n')[0]}`);
202
+ }
203
+ }
204
+ if (!syndicates.length)
205
+ fatal('No syndicate loaded.');
206
+ // ── Storage, budgets, tool credentials: as melchizedek-serve ─────────────
207
+ const databaseUrl = process.env.DATABASE_URL?.trim() || undefined;
208
+ let pg;
209
+ if (databaseUrl) {
210
+ const { postgresStorage } = await import('../lib/storage/postgres/index.js');
211
+ const { dbSchema } = await import('../lib/storage/schema.js');
212
+ pg = postgresStorage({
213
+ connectionString: databaseUrl,
214
+ schema: dbSchema(),
215
+ memory: { apiKey: process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY || '' },
216
+ });
217
+ const { compareSchema, schemaBehindMessage, shippedSchemaVersion } = await import('../lib/storage/schemaVersion.js');
218
+ const check = compareSchema(await pg.schemaVersion(), shippedSchemaVersion());
219
+ if (check.state === 'behind' && process.env.ALLOW_SCHEMA_MISMATCH?.trim().toLowerCase() !== 'true')
220
+ fatal(schemaBehindMessage(check));
221
+ }
222
+ let policy;
223
+ const budgetsJson = process.env.A2A_BUDGETS?.trim();
224
+ if (budgetsJson) {
225
+ const { budgets, memoryUsageStore, parseBudgets, postgresUsageStore } = await import('../lib/a2a/policy.js');
226
+ try {
227
+ policy = budgets(parseBudgets(budgetsJson), { store: pg ? postgresUsageStore(pg.pool) : memoryUsageStore() });
228
+ }
229
+ catch (err) {
230
+ fatal(err instanceof Error ? err.message : String(err));
231
+ }
232
+ }
233
+ const { oauthServerSetup, callbackIdentity } = await import('../lib/a2a/oauthSetup.js');
234
+ const { postgresCredentialRows } = await import('../lib/storage/postgres/credentialStore.js');
235
+ let oauth;
236
+ try {
237
+ oauth = oauthServerSetup({
238
+ configs: syndicates.map((s) => s.config),
239
+ load: (ref) => loadSyndicate(ref),
240
+ env: process.env,
241
+ ...(pg ? { rows: postgresCredentialRows(pg.pool), audit: pg.audit } : {}),
242
+ allowPrivate: process.env.ALLOW_PRIVATE_OPENAPI === 'true' || process.env.ALLOW_PRIVATE_MCP === 'true',
243
+ });
244
+ }
245
+ catch (err) {
246
+ fatal(err instanceof Error ? err.message : String(err));
247
+ }
248
+ for (const w of oauth.warnings)
249
+ warn(w);
250
+ const consent = oauth.toolCredentials?.consent;
251
+ if (consent && !args.http) {
252
+ warn('stdio serves no OAuth consent callback: a consent pause completes only where OAUTH_REDIRECT_URI is served by a server sharing this credential store.');
253
+ }
254
+ const mcp = createMcpServer({
255
+ syndicates,
256
+ ...(pg ? { sessionService: pg.sessionService, turnLock: pg.turnLock, audit: pg.audit } : {}),
257
+ ...(pg?.memoryService ? { memoryService: pg.memoryService } : {}),
258
+ taskTimeoutMs: envInt('A2A_TASK_TIMEOUT_MS', 15 * 60 * 1000),
259
+ maxConcurrentTasks: envInt('A2A_MAX_CONCURRENT_TASKS', 0),
260
+ maxConcurrentPerScope: envInt('A2A_MAX_CONCURRENT_PER_SCOPE', 4),
261
+ maxConcurrentPerCaller: envInt('A2A_MAX_CONCURRENT_PER_CALLER', 0),
262
+ turnLockWaitMs: envInt('A2A_TURN_LOCK_WAIT_MS', 30_000),
263
+ ...(policy ? { policy } : {}),
264
+ ...(oauth.toolCredentials ? { toolCredentials: oauth.toolCredentials } : {}),
265
+ log,
266
+ warn,
267
+ });
268
+ const toolList = syndicates.map((s) => s.id).join(', ');
269
+ const graceMs = envInt('A2A_SHUTDOWN_GRACE_MS', 25_000);
270
+ let stopping = false;
271
+ let closeHttp;
272
+ const stop = async (why) => {
273
+ if (stopping)
274
+ return;
275
+ stopping = true;
276
+ log(`${why}: waiting up to ${graceMs} ms for running turns.`);
277
+ const canceled = await mcp.shutdown(graceMs);
278
+ if (canceled > 0)
279
+ warn(`${canceled} turn(s) did not finish in time and were canceled.`);
280
+ await closeHttp?.().catch(() => { });
281
+ const { flushTracing } = await import('../lib/observability/tracer.js');
282
+ await flushTracing().catch(() => { });
283
+ await pg?.close().catch(() => { });
284
+ process.exit(0);
285
+ };
286
+ process.on('SIGTERM', () => void stop('SIGTERM'));
287
+ process.on('SIGINT', () => void stop('SIGINT'));
288
+ if (!args.http) {
289
+ const scope = process.env.MCP_USER_ID?.trim() || 'default';
290
+ const { HEADER_VALUE_PATTERN } = await import('../lib/a2a/identity.js');
291
+ if (!HEADER_VALUE_PATTERN.test(scope))
292
+ fatal('MCP_USER_ID must match [A-Za-z0-9._-]{1,64}');
293
+ await serveMcpStdio(mcp, { caller: { scopeKey: scope, caller: 'local' } });
294
+ log(`✓ Serving ${syndicates.length} syndicate tool(s) over stdio: ${toolList}`);
295
+ process.stdin.on('end', () => void stop('stdin closed'));
296
+ return;
297
+ }
298
+ const allowedHosts = process.env.MCP_ALLOWED_HOSTS?.split(',').map((s) => s.trim()).filter(Boolean);
299
+ const { consentCallback } = await import('../lib/a2a/app.js');
300
+ const app = mcpHttpApp(mcp, {
301
+ host,
302
+ port,
303
+ ...(secret ? { secret } : {}),
304
+ ...(allowedHosts?.length ? { allowedHosts } : {}),
305
+ trustProxy: trustProxy(),
306
+ rateLimitPerMinute: envInt('MCP_RATE_LIMIT_PER_MINUTE', 240),
307
+ ...(consent
308
+ ? {
309
+ beforeAuth: (a) => a.get(new URL(consent.redirectUri).pathname, consentCallback(consent, { requireCallerIdentity: callbackIdentity(process.env) === 'required', log, warn })),
310
+ }
311
+ : {}),
312
+ warn,
313
+ });
314
+ closeHttp = app.closeSessions;
315
+ // The secret is not needed past this point.
316
+ secret = undefined;
317
+ await new Promise((resolve) => {
318
+ app.listen(port, host, () => resolve());
319
+ });
320
+ log(`✓ Serving ${syndicates.length} syndicate tool(s) at http://${host.includes(':') ? `[${host}]` : host}:${port}/mcp: ${toolList}`);
321
+ log(` auth ${process.env.MCP_SERVER_SECRET?.trim() ? 'bearer MCP_SERVER_SECRET' : 'none (loopback only)'}`);
322
+ log(` storage ${pg ? 'postgres' : 'in-memory (lost on restart)'}`);
323
+ log(` oauth ${oauth.summary}`);
324
+ }
325
+ // Run-as-main guard on the realpath of argv[1] (the bin is a symlink; see a2a_server.ts).
326
+ const invokedAsMain = (() => {
327
+ if (!process.argv[1])
328
+ return false;
329
+ try {
330
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
331
+ }
332
+ catch {
333
+ return import.meta.url === `file://${process.argv[1]}`;
334
+ }
335
+ })();
336
+ if (invokedAsMain) {
337
+ // Before anything else loads: stdout is the stdio protocol channel.
338
+ console.log = console.error;
339
+ console.info = console.error;
340
+ console.debug = console.error;
341
+ startMcpServer().catch((error) => {
342
+ console.error(`[MCP] ✗ ${error instanceof Error ? error.message : String(error)}`);
343
+ process.exit(1);
344
+ });
345
+ }
@@ -16,6 +16,8 @@
16
16
  * npx melchizedek-setup --level 2 --write-env # also create .env from .env.example
17
17
  * npx melchizedek-setup --list
18
18
  * npx melchizedek-setup --markdown # ONBOARDING.md, regenerated
19
+ * npx melchizedek-setup --chatgpt-signin # Sign in with ChatGPT, local only (ADR 0126)
20
+ * npx melchizedek-setup --chatgpt-signout | --chatgpt-status
19
21
  *
20
22
  * Never prints a value. `--write-env` never overwrites `.env`, leaves every
21
23
  * name blank, and refuses unless `git check-ignore` confirms `.env` is ignored.
@@ -16,6 +16,8 @@
16
16
  * npx melchizedek-setup --level 2 --write-env # also create .env from .env.example
17
17
  * npx melchizedek-setup --list
18
18
  * npx melchizedek-setup --markdown # ONBOARDING.md, regenerated
19
+ * npx melchizedek-setup --chatgpt-signin # Sign in with ChatGPT, local only (ADR 0126)
20
+ * npx melchizedek-setup --chatgpt-signout | --chatgpt-status
19
21
  *
20
22
  * Never prints a value. `--write-env` never overwrites `.env`, leaves every
21
23
  * name blank, and refuses unless `git check-ignore` confirms `.env` is ignored.
@@ -24,6 +26,7 @@ import { realpathSync } from 'node:fs';
24
26
  import { createInterface } from 'node:readline/promises';
25
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
28
  import { loadEnv } from '../lib/loadEnv.js';
29
+ import { runChatGptSignIn, runChatGptSignOut, runChatGptStatus } from '../lib/chatgpt/cli.js';
27
30
  import { runDoctor } from '../lib/doctor.js';
28
31
  import { detectLevels, highestLevel, LEVELS, levelById, renderAuto, renderGuide, renderMenu, renderOnboardingDoc, writeEnvForLevel, } from '../lib/onboarding.js';
29
32
  const USAGE = `melchizedek-setup — start from what you have
@@ -34,6 +37,12 @@ const USAGE = `melchizedek-setup — start from what you have
34
37
  melchizedek-setup --list the levels
35
38
  melchizedek-setup --markdown every guide as Markdown (ONBOARDING.md)
36
39
 
40
+ melchizedek-setup --chatgpt-signin [--port <n>]
41
+ sign in with ChatGPT in your browser; OpenAI ids then run
42
+ on your ChatGPT plan on this machine (local only)
43
+ melchizedek-setup --chatgpt-signout remove the stored tokens and revoke them at OpenAI
44
+ melchizedek-setup --chatgpt-status where the sign-in is stored, and whether it is used
45
+
37
46
  --write-env with --level or --auto: create .env from .env.example with the level's
38
47
  names left blank (never overwrites; requires git to ignore .env)
39
48
  --clone | --package spell commands for a clone or an installed package (default: detected)
@@ -80,6 +89,24 @@ export async function main(argv = process.argv.slice(2)) {
80
89
  console.log(renderMenu());
81
90
  return 0;
82
91
  }
92
+ if (args.includes('--chatgpt-signin')) {
93
+ loadEnv();
94
+ const port = flag('port');
95
+ const n = port === undefined ? undefined : Number(port);
96
+ if (n !== undefined && !(Number.isInteger(n) && n >= 0 && n <= 65535)) {
97
+ console.error('✗ --port must be a port number (0 picks a free one)');
98
+ return 2;
99
+ }
100
+ return runChatGptSignIn(n !== undefined ? { port: n } : {});
101
+ }
102
+ if (args.includes('--chatgpt-signout')) {
103
+ loadEnv();
104
+ return runChatGptSignOut();
105
+ }
106
+ if (args.includes('--chatgpt-status')) {
107
+ loadEnv();
108
+ return runChatGptStatus();
109
+ }
83
110
  if (args.includes('--markdown')) {
84
111
  process.stdout.write(renderOnboardingDoc());
85
112
  return 0;
@@ -11,6 +11,8 @@ import { runtimeSetting } from '../lib/runtime/runtimeFlag.js';
11
11
  import { setLogLevel } from '../lib/runtime/logging.js';
12
12
  import { randomUUID } from 'node:crypto';
13
13
  import { loadEnv } from '../lib/loadEnv.js';
14
+ import { runChatGptSignIn } from '../lib/chatgpt/cli.js';
15
+ import { chatGptSignInRoutesOpenAi } from '../lib/chatgpt/state.js';
14
16
  // ── Model routing ─────────────────────────────────────────────────────────────
15
17
  // A model id resolves when an agent first calls it (lib/models/registry.ts),
16
18
  // after loadEnv() has read the keys from .env.
@@ -97,6 +99,11 @@ async function main() {
97
99
  process.env.OTEL_CONSOLE_SPANS = 'false';
98
100
  // MELCHIZEDEK_RUNTIME=adk (removed in 1.0.0) stops the chat here, naming the release.
99
101
  runtimeSetting();
102
+ // --chatgpt-signin: the browser sign-in first, then the chat (ADR 0126, local only).
103
+ if (process.argv.includes('--chatgpt-signin')) {
104
+ if ((await runChatGptSignIn()) !== 0)
105
+ process.exit(1);
106
+ }
100
107
  const apiKey = process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY;
101
108
  if (apiKey) {
102
109
  process.env.GEMINI_API_KEY = apiKey;
@@ -148,10 +155,12 @@ async function main() {
148
155
  : ['gemini']);
149
156
  if (!config.orchestrator.model)
150
157
  requiredProviders.add('gemini');
151
- const missingKeys = [...requiredProviders].filter((p) => !providerKeyPresent(p));
158
+ const missingKeys = [...requiredProviders].filter((p) => !providerKeyPresent(p) && !(p === 'openai' && chatGptSignInRoutesOpenAi()));
152
159
  if (missingKeys.length > 0) {
153
160
  for (const p of missingKeys) {
154
161
  console.error(`${c.yellow}⚠ ${PROVIDERS[p].label} requires ${PROVIDERS[p].keyEnv}, which is not set.${c.reset}`);
162
+ if (p === 'openai')
163
+ console.error(`${c.dim} (Or sign in with ChatGPT on this machine: add --chatgpt-signin, or run melchizedek-setup --chatgpt-signin.)${c.reset}`);
155
164
  }
156
165
  console.error(`${c.dim} (Only syndicates whose every agent uses an ollama/* model run keyless.)${c.reset}`);
157
166
  process.exit(1);
@@ -202,8 +211,8 @@ async function main() {
202
211
  const rawArgs = process.argv.slice(2);
203
212
  const queryParts = [];
204
213
  for (let i = 0; i < rawArgs.length; i++) {
205
- if (rawArgs[i] === '--')
206
- continue; // drop bare separator
214
+ if (rawArgs[i] === '--' || rawArgs[i] === '--chatgpt-signin')
215
+ continue; // drop bare separator and the sign-in flag
207
216
  if ((rawArgs[i] === '--bind' || rawArgs[i] === '--bindings' || rawArgs[i] === '--syndicate') && i + 1 < rawArgs.length) {
208
217
  i++; // skip the value
209
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "melchizedek-agents",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "A multi-model, multi-agent orchestration framework — YAML-defined syndicates on the engine's own agent loop, Supabase or Postgres session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "./a2a/identity": "./dist/lib/a2a/identity.js",
30
30
  "./a2a/policy": "./dist/lib/a2a/policy.js",
31
31
  "./a2a/limits": "./dist/lib/a2a/redisLimits.js",
32
+ "./mcp": "./dist/lib/mcp/server.js",
32
33
  "./registry": "./dist/lib/registry.js",
33
34
  "./server": "./dist/scripts/a2a_server.js",
34
35
  "./guards": "./dist/lib/guards/index.js",
@@ -47,16 +48,17 @@
47
48
  "./package.json": "./package.json"
48
49
  },
49
50
  "bin": {
50
- "melchizedek-serve": "./dist/scripts/a2a_server.js",
51
- "melchizedek-chat": "./dist/scripts/syndicate_chat.js",
52
- "melchizedek-doctor": "./dist/scripts/doctor.js",
53
- "melchizedek-skills": "./dist/scripts/skills_install.js",
54
- "melchizedek-worker": "./dist/scripts/assistant_worker.js",
55
- "melchizedek-db": "./dist/scripts/db.js",
56
- "melchizedek-init": "./dist/scripts/init.js",
57
- "melchizedek-setup": "./dist/scripts/setup.js",
58
- "melchizedek-registry": "./dist/scripts/registry.js",
59
- "melchizedek-codemod": "./dist/scripts/yaml_codemod.js"
51
+ "melchizedek-serve": "dist/scripts/a2a_server.js",
52
+ "melchizedek-mcp": "dist/scripts/mcp_server.js",
53
+ "melchizedek-chat": "dist/scripts/syndicate_chat.js",
54
+ "melchizedek-doctor": "dist/scripts/doctor.js",
55
+ "melchizedek-skills": "dist/scripts/skills_install.js",
56
+ "melchizedek-worker": "dist/scripts/assistant_worker.js",
57
+ "melchizedek-db": "dist/scripts/db.js",
58
+ "melchizedek-init": "dist/scripts/init.js",
59
+ "melchizedek-setup": "dist/scripts/setup.js",
60
+ "melchizedek-registry": "dist/scripts/registry.js",
61
+ "melchizedek-codemod": "dist/scripts/yaml_codemod.js"
60
62
  },
61
63
  "files": [
62
64
  "dist",
@@ -112,13 +114,14 @@
112
114
  "mcp:demo": "node --disable-warning=DEP0040 --experimental-strip-types scripts/demo_mcp_server.ts",
113
115
  "mcp:wiki": "node --disable-warning=DEP0040 --experimental-strip-types scripts/wiki/mcp_server.ts",
114
116
  "mcp:science": "node --disable-warning=DEP0040 --experimental-strip-types scripts/science_mcp_server.ts",
117
+ "mcp:serve": "node --disable-warning=DEP0040 --experimental-strip-types scripts/mcp_server.ts",
115
118
  "wiki:init": "node --disable-warning=DEP0040 --experimental-strip-types scripts/wiki/init.ts",
116
119
  "wiki:build": "node --disable-warning=DEP0040 --experimental-strip-types scripts/wiki/build.ts",
117
120
  "wiki:check": "node --disable-warning=DEP0040 --experimental-strip-types scripts/wiki/check.ts",
118
121
  "schema:gen": "node --disable-warning=DEP0040 --experimental-strip-types scripts/gen_syndicate_schema.ts",
119
122
  "yaml:codemod": "node --disable-warning=DEP0040 --experimental-strip-types scripts/yaml_codemod.ts",
120
123
  "start:a2a": "node --disable-warning=DEP0040 --experimental-strip-types scripts/a2a_server.ts",
121
- "test": "node --disable-warning=DEP0040 --experimental-strip-types --test tests/**/*.test.ts",
124
+ "test": "node --disable-warning=DEP0040 --experimental-strip-types --import ./tests/helpers/isolateLocalState.ts --test tests/**/*.test.ts",
122
125
  "setup": "node --disable-warning=DEP0040 --experimental-strip-types scripts/setup.ts",
123
126
  "init": "node --disable-warning=DEP0040 --experimental-strip-types scripts/init.ts",
124
127
  "db": "node --disable-warning=DEP0040 --experimental-strip-types scripts/db.ts",
package/skills/README.md CHANGED
@@ -15,9 +15,9 @@ This directory contains a suite of eleven Agent Skills built to the open SKILL.m
15
15
 
16
16
  Five onboarding skills route a person by what they have and walk the startup guide for it (`npx melchizedek-setup --level <id>` prints each guide):
17
17
 
18
- - `melchizedek-onboard`: The triage. Asks what the person has and routes to one of nine authentication levels and the skill below that owns it.
18
+ - `melchizedek-onboard`: The triage. Asks what the person has and routes to one of ten authentication levels and the skill below that owns it.
19
19
  - `melchizedek-onboard-local`: No key: Ollama and the keyless syndicates.
20
- - `melchizedek-onboard-keys`: One provider's key, several, a gateway key, and the honest answer for a ChatGPT / Codex, Claude.ai or Gemini CLI sign-in.
20
+ - `melchizedek-onboard-keys`: One provider's key, several, a gateway key, Sign in with ChatGPT on the person's own machine, and the honest answer for a Codex, Claude.ai or Gemini CLI sign-in.
21
21
  - `melchizedek-onboard-cloud`: Vertex AI, Amazon Bedrock and Azure OpenAI credentials.
22
22
  - `melchizedek-onboard-serve`: Serving to others: per-caller keys (BYOK), caller identities (`A2A_AUTH`), and OAuth tool grants.
23
23
 
@@ -14,7 +14,7 @@ The skills (name → what it teaches):
14
14
  - `melchizedek-memory` → Supabase sessions and long-term memory: modes, schema, tools, extraction rules, inspection, erasure.
15
15
  - `melchizedek-models` → model ids and providers, keys, keyless Ollama, the gateway fallback, per-agent settings, the errors.
16
16
  - `melchizedek-scribe` → writing documents from a brief with the Scribe syndicate.
17
- - Five onboarding skills, written by hand rather than by the Scribe (they print their facts from `npx melchizedek-setup --level <id>`): `melchizedek-onboard` (triage to one of nine authentication levels), `melchizedek-onboard-local`, `melchizedek-onboard-keys`, `melchizedek-onboard-cloud`, `melchizedek-onboard-serve`.
17
+ - Five onboarding skills, written by hand rather than by the Scribe (they print their facts from `npx melchizedek-setup --level <id>`): `melchizedek-onboard` (triage to one of ten authentication levels), `melchizedek-onboard-local`, `melchizedek-onboard-keys`, `melchizedek-onboard-cloud`, `melchizedek-onboard-serve`.
18
18
  - `--agents-md` also writes a pointer block into the project's `AGENTS.md`.
19
19
  Install (three ways):
20
20
  - From the package, into this project's `.claude/skills/` and `.agents/skills/` (the two locations that between them reach Claude Code, Codex, Cursor, OpenCode and Gemini CLI): `npx melchizedek-skills install` after `npm install melchizedek-agents`. `--for <targets>` picks platforms from `claude, agents, codex, cursor, opencode, gemini, all`; `--global` writes the home-directory locations instead; `--dir <path>` writes one explicit directory; `--only <names>` installs a subset; `--force` overwrites a file that differs; `--dry-run` prints without writing. `npx melchizedek-skills paths` prints the locations; `npx melchizedek-skills list` prints the skills. In a clone: `npm run skills:install -- <same flags>`.
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: melchizedek-onboard
3
- description: "Onboard a person to Melchizedek from whatever credentials they have: ask what they have (nothing, one or several provider API keys, a gateway key, Vertex AI / Bedrock / Azure access, a server to put in front of other people, OAuth tools, or a ChatGPT / Codex, Claude.ai or Gemini CLI sign-in), route them to one authentication level, and walk its startup guide without ever handling a key. Use when someone is setting up melchizedek-agents for the first time, asks which keys they need, says 'I have an X key' or 'I have a Codex login', or asks how to get started."
3
+ description: "Onboard a person to Melchizedek from whatever credentials they have: ask what they have (nothing, one or several provider API keys, a gateway key, Vertex AI / Bedrock / Azure access, a server to put in front of other people, OAuth tools, a ChatGPT plan to use on their own machine, or a Codex, Claude.ai or Gemini CLI sign-in), route them to one authentication level, and walk its startup guide without ever handling a key. Use when someone is setting up melchizedek-agents for the first time, asks which keys they need, says 'I have an X key', 'I have ChatGPT Plus' or 'I have a Codex login', or asks how to get started."
4
4
  ---
5
5
 
6
6
  ## What this skill does
7
7
 
8
- It turns "what do you have?" into one of nine authentication levels and the startup guide for it. The guides are generated by the engine itself, so you print them instead of restating them:
8
+ It turns "what do you have?" into one of ten authentication levels and the startup guide for it. The guides are generated by the engine itself, so you print them instead of restating them:
9
9
 
10
10
  ```bash
11
- npx melchizedek-setup --list # the nine levels
11
+ npx melchizedek-setup --list # the ten levels
12
12
  npx melchizedek-setup --level <id|number> # one level's guide
13
13
  npx melchizedek-setup --auto # detect from the environment, names only
14
14
  ```
@@ -20,12 +20,13 @@ In a clone of the repository, use `npm run setup -- <flags>` instead of `npx mel
20
20
  - Never ask the person to paste a key, token or secret into the chat, and never read one from their files or shell history. A pasted key lives in the transcript forever.
21
21
  - Never print a value from `.env`. To check what is set, run `npx melchizedek-setup --auto` or `npx melchizedek-doctor`; both report names and set or unset only.
22
22
  - The person types every value into `.env` in their own editor. You may create `.env` with names left blank: `npx melchizedek-setup --level <id> --write-env`. It never overwrites an existing `.env` and refuses unless git ignores `.env`.
23
- - Never reuse a consumer subscription sign-in (ChatGPT / Codex, Claude.ai, Gemini CLI) as a model credential. Route that person to level 9, which names the sanctioned alternative.
23
+ - Never reuse another app's sign-in (the Codex CLI's login, Claude.ai, Gemini CLI) as a model credential, and never read its token files. Route that person to level 10, which names the sanctioned alternative.
24
+ - A ChatGPT plan is used only through the engine's own Sign in with ChatGPT (level 9): the person runs `npx melchizedek-setup --chatgpt-signin` and approves in their own browser. It is for their own machine only; never set it up for a server.
24
25
 
25
26
  ## Triage
26
27
 
27
28
  1. Run `npx melchizedek-setup --auto`. If it already detects a level beyond 1, confirm with the person that this is what they want and continue from that guide.
28
- 2. Otherwise ask one question: "What do you have: nothing yet, an API key from a model provider, a gateway key, cloud platform access, or a ChatGPT, Claude or Gemini sign-in? And will other people or backends call your agents?"
29
+ 2. Otherwise ask one question: "What do you have: nothing yet, an API key from a model provider, a gateway key, cloud platform access, a ChatGPT plan, or a Claude or Gemini sign-in? And will other people or backends call your agents?"
29
30
  3. Route by the answer:
30
31
 
31
32
  | They have | Level | Follow |
@@ -38,12 +39,33 @@ In a clone of the repository, use `npm run setup -- <flags>` instead of `npx mel
38
39
  | Callers who fund their own model calls | 6 `byok` | `melchizedek-onboard-serve` |
39
40
  | Other backends or users calling the server | 7 `caller-tokens` | `melchizedek-onboard-serve` |
40
41
  | Tools that act for each user on a third-party API | 8 `oauth-grants` | `melchizedek-onboard-serve` |
41
- | A ChatGPT / Codex, Claude.ai or Gemini CLI sign-in only | 9 `subscription-signin` | `melchizedek-onboard-keys` |
42
+ | A ChatGPT plan, to use on their own machine | 9 `chatgpt-signin` | `melchizedek-onboard-keys` |
43
+ | A Codex CLI login, a Claude.ai or a Gemini CLI sign-in only | 10 `subscription-signin` | `melchizedek-onboard-keys` |
42
44
 
43
45
  Levels 6 to 8 sit on top of a model level (1 to 5): settle how models are paid for first.
44
46
 
45
47
  4. Print the level's guide with `--level <id>` and walk it step by step: the person sets the variables, you run the doctor command the guide names and read its verdicts back to them, then you run the first command.
46
48
 
49
+ ## Use melch from Claude Code or Codex
50
+
51
+ Once a level is confirmed, the person can call their syndicates from inside their coding agent: `melchizedek-mcp` makes each syndicate an MCP tool (`{ message, session_id? }`), plus `melch_resume` for a turn that pauses. Register it from the project directory, where `.env` and `config/agents/` live:
52
+
53
+ ```bash
54
+ claude mcp add melch -- npx melchizedek-mcp --syndicate <id>
55
+ ```
56
+
57
+ For Codex, add to `~/.codex/config.toml`:
58
+
59
+ ```toml
60
+ [mcp_servers.melch]
61
+ command = "npx"
62
+ args = ["-y", "-p", "melchizedek-agents", "melchizedek-mcp", "--syndicate", "<id>"]
63
+ cwd = "/path/to/the/project"
64
+ tool_timeout_sec = 900
65
+ ```
66
+
67
+ When a result says an approval is waiting, ask the person and pass their decision to `melch_resume` with `approve`; never approve on their behalf. Keys stay in `.env`: the MCP registration names no key.
68
+
47
69
  ## Done when
48
70
 
49
71
  The doctor shows the level's confirmation (the guide's **Confirm** line), and the first command has answered once. Then hand over to the `melchizedek` skill for the catalog of syndicates.