@stage5/lumine 0.2.39 → 0.2.41

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.
@@ -0,0 +1,138 @@
1
+ import readline from "node:readline";
2
+
3
+ import { resolveAuth } from "../auth.js";
4
+ import { createExternalAgentToolSession } from "./tool-session.js";
5
+ import { readAgentRuntimeFile } from "./trace.js";
6
+
7
+ const MCP_PROTOCOL_VERSION = "2025-06-18";
8
+
9
+ export async function agentMcpCommand(options) {
10
+ if (!options.runtimeFile) {
11
+ throw new Error("agent-mcp requires --runtime-file.");
12
+ }
13
+ if (!options.traceFile) {
14
+ throw new Error("agent-mcp requires --trace-file.");
15
+ }
16
+ const runtimeEnvelope = await readAgentRuntimeFile(options.runtimeFile);
17
+ const runtime = runtimeEnvelope.runtime;
18
+ const buildId = Number(options.buildIdFlag || 0);
19
+ if (!buildId) throw new Error("agent-mcp requires --build.");
20
+ if (!options.baseFilesHash) {
21
+ throw new Error("agent-mcp requires --base-files-hash.");
22
+ }
23
+ const auth = await resolveAuth(options);
24
+ const session = await createExternalAgentToolSession({
25
+ options,
26
+ auth,
27
+ buildId,
28
+ baseFilesHash: options.baseFilesHash,
29
+ dir: options.dir,
30
+ runtime,
31
+ traceFile: options.traceFile,
32
+ provider: runtimeEnvelope.provider || "external",
33
+ });
34
+ const tools = (Array.isArray(runtime.tools) ? runtime.tools : [])
35
+ .filter((tool) => tool?.type === "function" && tool?.name)
36
+ .map((tool) => ({
37
+ name: tool.name,
38
+ description: tool.description || "",
39
+ inputSchema: tool.parameters || {
40
+ type: "object",
41
+ additionalProperties: true,
42
+ },
43
+ }));
44
+
45
+ const input = readline.createInterface({
46
+ input: process.stdin,
47
+ crlfDelay: Infinity,
48
+ terminal: false,
49
+ });
50
+ const pending = new Set();
51
+ input.on("line", (line) => {
52
+ if (!line.trim()) return;
53
+ const operation = handleMcpMessage({ line, session, tools }).catch(
54
+ (error) => {
55
+ process.stderr.write(
56
+ `lumine agent-mcp: ${String(error?.message || error)}\n`,
57
+ );
58
+ },
59
+ );
60
+ pending.add(operation);
61
+ operation.finally(() => pending.delete(operation));
62
+ });
63
+ await new Promise((resolve) => input.once("close", resolve));
64
+ await Promise.allSettled(Array.from(pending));
65
+ }
66
+
67
+ async function handleMcpMessage({ line, session, tools }) {
68
+ let message;
69
+ try {
70
+ message = JSON.parse(line);
71
+ } catch {
72
+ return writeMcpError(null, -32700, "Parse error");
73
+ }
74
+ const id = message?.id;
75
+ const method = String(message?.method || "");
76
+ if (id === undefined || id === null) return;
77
+
78
+ if (method === "initialize") {
79
+ return writeMcpResult(id, {
80
+ protocolVersion:
81
+ String(message?.params?.protocolVersion || "") || MCP_PROTOCOL_VERSION,
82
+ capabilities: { tools: { listChanged: false } },
83
+ serverInfo: {
84
+ name: "lumine-external-agent-runtime",
85
+ version: "1.0.0",
86
+ },
87
+ });
88
+ }
89
+ if (method === "ping") return writeMcpResult(id, {});
90
+ if (method === "tools/list") {
91
+ return writeMcpResult(id, { tools });
92
+ }
93
+ if (method === "tools/call") {
94
+ const toolName = String(message?.params?.name || "");
95
+ const toolArguments = message?.params?.arguments || {};
96
+ try {
97
+ const output = await session.call(toolName, toolArguments);
98
+ const isError = output?.ok === false || Boolean(output?.error);
99
+ return writeMcpResult(id, {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: JSON.stringify(output),
104
+ },
105
+ ],
106
+ isError,
107
+ });
108
+ } catch (error) {
109
+ return writeMcpResult(id, {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: JSON.stringify({
114
+ ok: false,
115
+ error: String(error?.message || error),
116
+ }),
117
+ },
118
+ ],
119
+ isError: true,
120
+ });
121
+ }
122
+ }
123
+ return writeMcpError(id, -32601, `Method not found: ${method}`);
124
+ }
125
+
126
+ function writeMcpResult(id, result) {
127
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
128
+ }
129
+
130
+ function writeMcpError(id, code, message) {
131
+ process.stdout.write(
132
+ `${JSON.stringify({
133
+ jsonrpc: "2.0",
134
+ id,
135
+ error: { code, message },
136
+ })}\n`,
137
+ );
138
+ }
@@ -0,0 +1,214 @@
1
+ import { spawn } from "node:child_process";
2
+ import readline from "node:readline";
3
+
4
+ import { createSubscriptionAgentEnvironment } from "./environment.js";
5
+
6
+ const CLAUDE_RUNTIME_SUFFIX = `
7
+
8
+ EXTERNAL LUMINE RUNTIME:
9
+ - Use only the Lumine MCP tools supplied for this run.
10
+ - Do not use built-in filesystem, shell, web, skill, or subagent tools.
11
+ - The real project is available only through Lumine tools.
12
+ - Never reveal hidden chain-of-thought or private reasoning.`;
13
+
14
+ export async function runClaudeCodeAgentPass({
15
+ options,
16
+ runtime,
17
+ isolationDir,
18
+ mcp,
19
+ }) {
20
+ if (options.authToken) {
21
+ throw new Error(
22
+ "Claude Code external-agent runs require a saved `lumine login`; --auth-token is not forwarded into an MCP child process.",
23
+ );
24
+ }
25
+ const binary =
26
+ options.providerPath || process.env.LUMINE_CLAUDE_PATH || "claude";
27
+ const mcpConfig = {
28
+ mcpServers: {
29
+ lumine: {
30
+ type: "stdio",
31
+ command: process.execPath,
32
+ args: [
33
+ mcp.cliPath,
34
+ "agent-mcp",
35
+ "--dir",
36
+ mcp.dir,
37
+ "--runtime-file",
38
+ mcp.runtimeFile,
39
+ "--trace-file",
40
+ mcp.traceFile,
41
+ "--api-url",
42
+ options.apiUrl,
43
+ "--auth-file",
44
+ options.authFile,
45
+ "--build",
46
+ String(mcp.buildId),
47
+ "--base-files-hash",
48
+ mcp.baseFilesHash,
49
+ "--no-update-check",
50
+ ],
51
+ },
52
+ },
53
+ };
54
+ const args = [
55
+ "-p",
56
+ "--output-format",
57
+ "stream-json",
58
+ "--verbose",
59
+ "--no-session-persistence",
60
+ "--disable-slash-commands",
61
+ "--no-chrome",
62
+ "--permission-mode",
63
+ "dontAsk",
64
+ "--tools",
65
+ "",
66
+ "--allowedTools",
67
+ "mcp__lumine__*",
68
+ "--strict-mcp-config",
69
+ "--mcp-config",
70
+ JSON.stringify(mcpConfig),
71
+ "--system-prompt",
72
+ `${runtime.systemPrompt}${CLAUDE_RUNTIME_SUFFIX}`,
73
+ ...(options.model ? ["--model", options.model] : []),
74
+ ...(options.agentEffort ? ["--effort", options.agentEffort] : []),
75
+ runtime.initialPrompt,
76
+ ];
77
+ const result = await runClaudeProcess({
78
+ binary,
79
+ args,
80
+ cwd: isolationDir,
81
+ streamJson: true,
82
+ });
83
+ return { finalText: result.finalText };
84
+ }
85
+
86
+ export async function reviewClaudeCodeAgentLoop({
87
+ options,
88
+ isolationDir,
89
+ prompt,
90
+ systemPrompt,
91
+ outputSchema,
92
+ }) {
93
+ const binary =
94
+ options.providerPath || process.env.LUMINE_CLAUDE_PATH || "claude";
95
+ const result = await runClaudeProcess({
96
+ binary,
97
+ cwd: isolationDir,
98
+ streamJson: false,
99
+ args: [
100
+ "-p",
101
+ "--output-format",
102
+ "json",
103
+ "--no-session-persistence",
104
+ "--disable-slash-commands",
105
+ "--no-chrome",
106
+ "--permission-mode",
107
+ "dontAsk",
108
+ "--tools",
109
+ "",
110
+ "--allowedTools",
111
+ "",
112
+ "--strict-mcp-config",
113
+ "--mcp-config",
114
+ JSON.stringify({ mcpServers: {} }),
115
+ "--system-prompt",
116
+ systemPrompt,
117
+ "--json-schema",
118
+ JSON.stringify(outputSchema),
119
+ ...(options.model ? ["--model", options.model] : []),
120
+ ...(options.agentEffort ? ["--effort", options.agentEffort] : []),
121
+ prompt,
122
+ ],
123
+ });
124
+ if (result.structuredOutput) return result.structuredOutput;
125
+ try {
126
+ return JSON.parse(result.finalText);
127
+ } catch {
128
+ throw new Error("Claude Code did not return valid loop-review JSON.");
129
+ }
130
+ }
131
+
132
+ async function runClaudeProcess({ binary, args, cwd, streamJson }) {
133
+ const child = spawn(binary, args, {
134
+ cwd,
135
+ stdio: ["ignore", "pipe", "pipe"],
136
+ env: createSubscriptionAgentEnvironment(),
137
+ });
138
+ child.stderr.on("data", (chunk) => process.stderr.write(chunk));
139
+ let finalText = "";
140
+ let structuredOutput = null;
141
+ let rawOutput = "";
142
+
143
+ if (streamJson) {
144
+ const lines = readline.createInterface({
145
+ input: child.stdout,
146
+ crlfDelay: Infinity,
147
+ terminal: false,
148
+ });
149
+ lines.on("line", (line) => {
150
+ if (!line.trim()) return;
151
+ let event;
152
+ try {
153
+ event = JSON.parse(line);
154
+ } catch {
155
+ return;
156
+ }
157
+ if (event?.type === "assistant") {
158
+ const texts = (event.message?.content || [])
159
+ .filter((block) => block?.type === "text")
160
+ .map((block) => String(block.text || "").trim())
161
+ .filter(Boolean);
162
+ if (texts.length > 0) finalText = texts.join("\n\n");
163
+ }
164
+ if (event?.type === "result") {
165
+ if (typeof event.result === "string" && event.result.trim()) {
166
+ finalText = event.result.trim();
167
+ }
168
+ if (event.structured_output) {
169
+ structuredOutput = event.structured_output;
170
+ }
171
+ }
172
+ });
173
+ } else {
174
+ child.stdout.on("data", (chunk) => {
175
+ rawOutput += chunk.toString("utf8");
176
+ });
177
+ }
178
+
179
+ const result = await new Promise((resolve, reject) => {
180
+ child.once("error", (error) =>
181
+ reject(normalizeProviderLaunchError(error, binary)),
182
+ );
183
+ child.once("exit", (code, signal) => {
184
+ if (code === 0) return resolve({ code });
185
+ reject(
186
+ new Error(
187
+ `Claude Code exited before completing the Lumine pass (${signal || code}).`,
188
+ ),
189
+ );
190
+ });
191
+ });
192
+ void result;
193
+
194
+ if (!streamJson) {
195
+ let parsed;
196
+ try {
197
+ parsed = JSON.parse(rawOutput);
198
+ } catch {
199
+ throw new Error("Claude Code returned an invalid JSON result.");
200
+ }
201
+ finalText = String(parsed?.result || "").trim();
202
+ structuredOutput = parsed?.structured_output || null;
203
+ }
204
+ return { finalText, structuredOutput };
205
+ }
206
+
207
+ function normalizeProviderLaunchError(error, binary) {
208
+ if (error?.code === "ENOENT") {
209
+ return new Error(
210
+ `Claude Code CLI was not found at ${binary}. Install it, sign in with your subscription, or pass --provider-path.`,
211
+ );
212
+ }
213
+ return error;
214
+ }