nomoreide 0.1.30 → 0.1.31

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,56 @@
1
+ import type { ApprovalBroker } from "./approval-broker.js";
2
+ /** Streamed back to the route, which forwards each as an SSE event. */
3
+ export type AgentStreamEvent = {
4
+ type: "session";
5
+ sessionId: string;
6
+ } | {
7
+ type: "text";
8
+ text: string;
9
+ } | {
10
+ type: "tool_use";
11
+ id: string;
12
+ name: string;
13
+ input: unknown;
14
+ } | {
15
+ type: "tool_result";
16
+ id: string;
17
+ name: string;
18
+ preview: string;
19
+ isError: boolean;
20
+ } | {
21
+ type: "approval_request";
22
+ requestId: string;
23
+ name: string;
24
+ input: unknown;
25
+ } | {
26
+ type: "done";
27
+ stopReason: string | null;
28
+ } | {
29
+ type: "error";
30
+ message: string;
31
+ };
32
+ export interface AgentRuntimeDeps {
33
+ /** Working directory the Claude Code session runs in (the workspace root). */
34
+ cwd: string;
35
+ }
36
+ export interface AgentRunOptions {
37
+ signal?: AbortSignal;
38
+ /** When present, gated tools prompt the dock for approval via this broker. */
39
+ approval?: {
40
+ broker: ApprovalBroker;
41
+ url: string;
42
+ };
43
+ }
44
+ /** True when the `claude` CLI is installed and runnable. Memoized. */
45
+ export declare function isAgentAvailable(): Promise<boolean>;
46
+ /** Whether tool calls are gated behind dock approval (vs. fully autonomous). */
47
+ export declare function approvalsEnabled(): boolean;
48
+ export declare class AgentRuntime {
49
+ private readonly deps;
50
+ constructor(deps: AgentRuntimeDeps);
51
+ /**
52
+ * Run one user turn. Streams events until the CLI exits. `resumeSessionId`
53
+ * continues a prior Claude Code session; omit it to start a fresh one.
54
+ */
55
+ run(message: string, resumeSessionId: string | undefined, onEvent: (event: AgentStreamEvent) => void, options?: AgentRunOptions): Promise<void>;
56
+ }
@@ -0,0 +1,322 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * In-dashboard AI agent backed by the real Claude Code CLI. We drive the
7
+ * installed `claude` binary in headless streaming mode (`--output-format
8
+ * stream-json`) rather than the Anthropic API directly, so the dock gets the
9
+ * full Claude Code agent — its tools, this project's .mcp.json / CLAUDE.md, and
10
+ * the user's existing Claude Code login (no separate API key).
11
+ *
12
+ * Conversation continuity uses Claude Code's own session store: the first turn
13
+ * returns a `session_id`, which the client sends back as `resumeSessionId` on
14
+ * the next turn (`--resume`). The server holds no transcript state.
15
+ *
16
+ * Tool permissions: unless NOMOREIDE_AGENT_PERMISSION_MODE=bypassPermissions,
17
+ * the agent runs in `default` mode with a PreToolUse hook on mutating tools.
18
+ * The hook blocks and asks the dock (via ApprovalBroker) for an Allow/Deny.
19
+ */
20
+ const CLAUDE_BIN = process.env.NOMOREIDE_CLAUDE_BIN || "claude";
21
+ /** "bypassPermissions" runs fully autonomous (no approval prompts). */
22
+ const PERMISSION_MODE = process.env.NOMOREIDE_AGENT_PERMISSION_MODE || "default";
23
+ /** Tools that trigger an approval prompt (mutating / side-effecting ones). */
24
+ const GATED_TOOLS = "Bash|Edit|Write|MultiEdit|NotebookEdit";
25
+ /** Truncate tool-result previews shown in the dock. */
26
+ const PREVIEW_LIMIT = 400;
27
+ let availabilityProbe = null;
28
+ /** True when the `claude` CLI is installed and runnable. Memoized. */
29
+ export function isAgentAvailable() {
30
+ if (!availabilityProbe) {
31
+ availabilityProbe = new Promise((resolve) => {
32
+ const child = spawn(CLAUDE_BIN, ["--version"], { stdio: "ignore" });
33
+ child.on("error", () => resolve(false));
34
+ child.on("close", (code) => resolve(code === 0));
35
+ }).catch(() => false);
36
+ }
37
+ return availabilityProbe;
38
+ }
39
+ /** Whether tool calls are gated behind dock approval (vs. fully autonomous). */
40
+ export function approvalsEnabled() {
41
+ return PERMISSION_MODE !== "bypassPermissions";
42
+ }
43
+ export class AgentRuntime {
44
+ deps;
45
+ constructor(deps) {
46
+ this.deps = deps;
47
+ }
48
+ /**
49
+ * Run one user turn. Streams events until the CLI exits. `resumeSessionId`
50
+ * continues a prior Claude Code session; omit it to start a fresh one.
51
+ */
52
+ async run(message, resumeSessionId, onEvent, options = {}) {
53
+ const { signal, approval } = options;
54
+ const gating = Boolean(approval) && approvalsEnabled();
55
+ const args = [
56
+ "--print",
57
+ "--output-format",
58
+ "stream-json",
59
+ "--verbose",
60
+ "--include-partial-messages",
61
+ "--permission-mode",
62
+ gating ? "default" : PERMISSION_MODE,
63
+ ];
64
+ if (gating) {
65
+ args.push("--settings", approvalSettings());
66
+ }
67
+ if (resumeSessionId)
68
+ args.push("--resume", resumeSessionId);
69
+ args.push(message);
70
+ const env = gating
71
+ ? { ...process.env, NOMOREIDE_APPROVAL_URL: approval.url }
72
+ : process.env;
73
+ await new Promise((resolve) => {
74
+ const child = spawn(CLAUDE_BIN, args, { cwd: this.deps.cwd, env });
75
+ const toolNames = new Map();
76
+ let stdout = "";
77
+ let stderr = "";
78
+ let finished = false;
79
+ let openedSession;
80
+ const finish = () => {
81
+ if (finished)
82
+ return;
83
+ finished = true;
84
+ if (openedSession)
85
+ approval?.broker.closeRun(openedSession);
86
+ resolve();
87
+ };
88
+ // Bridge the broker's approval requests onto the SSE stream the moment
89
+ // we learn this run's session id.
90
+ const onLine = (line) => {
91
+ const sessionId = handleLine(line, toolNames, onEvent);
92
+ if (sessionId && gating && !openedSession) {
93
+ openedSession = sessionId;
94
+ approval.broker.openRun(sessionId, (request) => onEvent({
95
+ type: "approval_request",
96
+ requestId: request.requestId,
97
+ name: request.name,
98
+ input: request.input,
99
+ }));
100
+ }
101
+ };
102
+ if (signal) {
103
+ if (signal.aborted)
104
+ child.kill();
105
+ else
106
+ signal.addEventListener("abort", () => child.kill(), { once: true });
107
+ }
108
+ child.on("error", (error) => {
109
+ onEvent({
110
+ type: "error",
111
+ message: error.code === "ENOENT"
112
+ ? `Could not run "${CLAUDE_BIN}". Install Claude Code or set NOMOREIDE_CLAUDE_BIN.`
113
+ : error.message,
114
+ });
115
+ finish();
116
+ });
117
+ child.stdout.setEncoding("utf8");
118
+ child.stdout.on("data", (chunk) => {
119
+ stdout += chunk;
120
+ let newline = stdout.indexOf("\n");
121
+ while (newline !== -1) {
122
+ const line = stdout.slice(0, newline).trim();
123
+ stdout = stdout.slice(newline + 1);
124
+ if (line)
125
+ onLine(line);
126
+ newline = stdout.indexOf("\n");
127
+ }
128
+ });
129
+ child.stderr.setEncoding("utf8");
130
+ child.stderr.on("data", (chunk) => {
131
+ stderr += chunk;
132
+ });
133
+ child.on("close", (code) => {
134
+ if (signal?.aborted)
135
+ return finish();
136
+ const leftover = stdout.trim();
137
+ if (leftover)
138
+ onLine(leftover);
139
+ if (code !== 0) {
140
+ onEvent({
141
+ type: "error",
142
+ message: stderr.trim() || `Claude Code exited with code ${code}.`,
143
+ });
144
+ }
145
+ finish();
146
+ });
147
+ });
148
+ }
149
+ }
150
+ /**
151
+ * Parse one NDJSON line from Claude Code's stream-json output, emitting the
152
+ * relevant dock events. Returns the session id when this line is the init event.
153
+ */
154
+ function handleLine(line, toolNames, onEvent) {
155
+ let obj;
156
+ try {
157
+ obj = JSON.parse(line);
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ switch (obj.type) {
163
+ case "system": {
164
+ if (obj.subtype === "init" && typeof obj.session_id === "string") {
165
+ onEvent({ type: "session", sessionId: obj.session_id });
166
+ return obj.session_id;
167
+ }
168
+ return undefined;
169
+ }
170
+ case "stream_event": {
171
+ // Token-level text deltas (from --include-partial-messages).
172
+ const event = obj.event;
173
+ if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
174
+ onEvent({ type: "text", text: event.delta.text ?? "" });
175
+ }
176
+ return undefined;
177
+ }
178
+ case "assistant": {
179
+ for (const block of messageContent(obj.message)) {
180
+ if (block.type === "tool_use" && typeof block.id === "string") {
181
+ toolNames.set(block.id, String(block.name));
182
+ onEvent({ type: "tool_use", id: block.id, name: String(block.name), input: block.input });
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+ case "user": {
188
+ for (const block of messageContent(obj.message)) {
189
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
190
+ onEvent({
191
+ type: "tool_result",
192
+ id: block.tool_use_id,
193
+ name: toolNames.get(block.tool_use_id) ?? "tool",
194
+ preview: previewOf(block.content),
195
+ isError: Boolean(block.is_error),
196
+ });
197
+ }
198
+ }
199
+ return undefined;
200
+ }
201
+ case "result": {
202
+ onEvent({ type: "done", stopReason: typeof obj.subtype === "string" ? obj.subtype : null });
203
+ return undefined;
204
+ }
205
+ default:
206
+ return undefined;
207
+ }
208
+ }
209
+ function messageContent(message) {
210
+ const content = message?.content;
211
+ return Array.isArray(content) ? content : [];
212
+ }
213
+ function previewOf(content) {
214
+ let text;
215
+ if (typeof content === "string") {
216
+ text = content;
217
+ }
218
+ else if (Array.isArray(content)) {
219
+ text = content
220
+ .map((block) => typeof block === "string"
221
+ ? block
222
+ : typeof block.text === "string"
223
+ ? block.text
224
+ : "")
225
+ .join(" ");
226
+ }
227
+ else {
228
+ text = "";
229
+ }
230
+ text = text.trim();
231
+ return text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT)}…` : text;
232
+ }
233
+ let hookPath;
234
+ let settingsJson;
235
+ /** Inline `--settings` JSON installing the PreToolUse approval hook. Cached. */
236
+ function approvalSettings() {
237
+ if (!settingsJson) {
238
+ const command = `node ${JSON.stringify(ensureHookScript())}`;
239
+ settingsJson = JSON.stringify({
240
+ hooks: {
241
+ PreToolUse: [{ matcher: GATED_TOOLS, hooks: [{ type: "command", command }] }],
242
+ },
243
+ });
244
+ }
245
+ return settingsJson;
246
+ }
247
+ /**
248
+ * Write the approval-hook script to a temp file once, runnable by plain `node`
249
+ * in both dev (tsx) and built modes. It POSTs the pending tool call to the web
250
+ * server and blocks until the user's decision returns, then prints the
251
+ * PreToolUse permission decision Claude Code expects.
252
+ */
253
+ function ensureHookScript() {
254
+ if (hookPath)
255
+ return hookPath;
256
+ const dir = mkdtempSync(join(tmpdir(), "nomoreide-agent-"));
257
+ hookPath = join(dir, "approval-hook.cjs");
258
+ writeFileSync(hookPath, HOOK_SOURCE, "utf8");
259
+ return hookPath;
260
+ }
261
+ const HOOK_SOURCE = `"use strict";
262
+ const http = require("http");
263
+ const { randomUUID } = require("crypto");
264
+ let body = "";
265
+ process.stdin.on("data", (d) => (body += d));
266
+ process.stdin.on("end", () => {
267
+ let input = {};
268
+ try { input = JSON.parse(body); } catch {}
269
+ const url = process.env.NOMOREIDE_APPROVAL_URL;
270
+ if (!url) return decide("deny", "Approval channel not configured.");
271
+ let target;
272
+ try { target = new URL(url); } catch { return decide("deny", "Bad approval URL."); }
273
+ const payload = JSON.stringify({
274
+ sessionId: input.session_id,
275
+ requestId: randomUUID(),
276
+ toolName: input.tool_name,
277
+ toolInput: input.tool_input,
278
+ });
279
+ const req = http.request(
280
+ {
281
+ hostname: target.hostname,
282
+ port: target.port,
283
+ path: target.pathname,
284
+ method: "POST",
285
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
286
+ },
287
+ (res) => {
288
+ let data = "";
289
+ res.setEncoding("utf8");
290
+ res.on("data", (c) => (data += c));
291
+ res.on("end", () => {
292
+ try {
293
+ const r = JSON.parse(data);
294
+ decide(r.decision === "allow" ? "allow" : "deny", r.reason);
295
+ } catch {
296
+ decide("deny", "No decision returned.");
297
+ }
298
+ });
299
+ },
300
+ );
301
+ req.on("error", () => decide("deny", "Approval request failed to reach NoMoreIDE."));
302
+ req.setTimeout(10 * 60 * 1000, () => {
303
+ req.destroy();
304
+ decide("deny", "Approval timed out.");
305
+ });
306
+ req.write(payload);
307
+ req.end();
308
+ });
309
+ function decide(permissionDecision, reason) {
310
+ process.stdout.write(
311
+ JSON.stringify({
312
+ hookSpecificOutput: {
313
+ hookEventName: "PreToolUse",
314
+ permissionDecision,
315
+ permissionDecisionReason: reason || "",
316
+ },
317
+ }),
318
+ );
319
+ process.exit(0);
320
+ }
321
+ `;
322
+ //# sourceMappingURL=agent-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-runtime.js","sourceRoot":"","sources":["../../src/core/agent-runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Bridges Claude Code's PreToolUse permission hook to the web UI. The hook
3
+ * (a child process of the spawned `claude`) POSTs an approval request and
4
+ * blocks; the broker emits it onto the active chat SSE stream and parks a
5
+ * resolver until the browser POSTs the user's decision back.
6
+ *
7
+ * Keyed by Claude Code session id, which both the SSE stream (from the init
8
+ * event) and the hook (from its stdin payload) know.
9
+ */
10
+ export interface ApprovalRequest {
11
+ requestId: string;
12
+ name: string;
13
+ input: unknown;
14
+ }
15
+ export interface ApprovalDecision {
16
+ decision: "allow" | "deny";
17
+ reason?: string;
18
+ }
19
+ export declare class ApprovalBroker {
20
+ private readonly channels;
21
+ /** Register the SSE emitter for a session so approvals can reach the browser. */
22
+ openRun(sessionId: string, emit: (request: ApprovalRequest) => void): void;
23
+ /** Tear down a session, auto-denying anything still awaiting a decision. */
24
+ closeRun(sessionId: string): void;
25
+ /** Called by the hook endpoint. Resolves once the user decides (or no channel). */
26
+ requestApproval(sessionId: string | undefined, requestId: string, name: string, input: unknown): Promise<ApprovalDecision>;
27
+ /** Called by the browser. Returns false if the request is unknown/expired. */
28
+ resolve(sessionId: string, requestId: string, decision: ApprovalDecision): boolean;
29
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Bridges Claude Code's PreToolUse permission hook to the web UI. The hook
3
+ * (a child process of the spawned `claude`) POSTs an approval request and
4
+ * blocks; the broker emits it onto the active chat SSE stream and parks a
5
+ * resolver until the browser POSTs the user's decision back.
6
+ *
7
+ * Keyed by Claude Code session id, which both the SSE stream (from the init
8
+ * event) and the hook (from its stdin payload) know.
9
+ */
10
+ export class ApprovalBroker {
11
+ channels = new Map();
12
+ /** Register the SSE emitter for a session so approvals can reach the browser. */
13
+ openRun(sessionId, emit) {
14
+ this.channels.set(sessionId, { emit, pending: new Map() });
15
+ }
16
+ /** Tear down a session, auto-denying anything still awaiting a decision. */
17
+ closeRun(sessionId) {
18
+ const channel = this.channels.get(sessionId);
19
+ if (!channel)
20
+ return;
21
+ for (const resolve of channel.pending.values()) {
22
+ resolve({ decision: "deny", reason: "The agent session ended before you responded." });
23
+ }
24
+ this.channels.delete(sessionId);
25
+ }
26
+ /** Called by the hook endpoint. Resolves once the user decides (or no channel). */
27
+ requestApproval(sessionId, requestId, name, input) {
28
+ const channel = sessionId ? this.channels.get(sessionId) : undefined;
29
+ if (!channel) {
30
+ return Promise.resolve({ decision: "deny", reason: "No active agent session to approve." });
31
+ }
32
+ return new Promise((resolve) => {
33
+ channel.pending.set(requestId, resolve);
34
+ channel.emit({ requestId, name, input });
35
+ });
36
+ }
37
+ /** Called by the browser. Returns false if the request is unknown/expired. */
38
+ resolve(sessionId, requestId, decision) {
39
+ const channel = this.channels.get(sessionId);
40
+ const resolver = channel?.pending.get(requestId);
41
+ if (!channel || !resolver)
42
+ return false;
43
+ channel.pending.delete(requestId);
44
+ resolver(decision);
45
+ return true;
46
+ }
47
+ }
48
+ //# sourceMappingURL=approval-broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-broker.js","sourceRoot":"","sources":["../../src/core/approval-broker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkBH,MAAM,OAAO,cAAc;IACR,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IAEvD,iFAAiF;IACjF,OAAO,CAAC,SAAiB,EAAE,IAAwC;QACjE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,4EAA4E;IAC5E,QAAQ,CAAC,SAAiB;QACxB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/C,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,+CAA+C,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,mFAAmF;IACnF,eAAe,CACb,SAA6B,EAC7B,SAAiB,EACjB,IAAY,EACZ,KAAc;QAEd,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,OAAO,CAAC,SAAiB,EAAE,SAAiB,EAAE,QAA0B;QACtE,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QACxC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAClC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}