replicas-engine 0.1.173 → 0.1.174

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 (2) hide show
  1. package/dist/src/index.js +223 -20
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -5,7 +5,7 @@ import { serve } from "@hono/node-server";
5
5
  import { Hono as Hono2 } from "hono";
6
6
  import { readFile as readFile14 } from "fs/promises";
7
7
  import { execSync as execSync2 } from "child_process";
8
- import { randomUUID as randomUUID5 } from "crypto";
8
+ import { randomUUID as randomUUID6 } from "crypto";
9
9
 
10
10
  // src/managers/github-token-manager.ts
11
11
  import { promises as fs } from "fs";
@@ -375,7 +375,7 @@ function parseReplicasConfigString(content, filename) {
375
375
  }
376
376
 
377
377
  // ../shared/src/engine/environment.ts
378
- var DAYTONA_SNAPSHOT_ID = "14-05-2026-royal-york-v18";
378
+ var DAYTONA_SNAPSHOT_ID = "15-05-2026-royal-york-v19";
379
379
 
380
380
  // ../shared/src/engine/types.ts
381
381
  var DEFAULT_CHAT_TITLES = {
@@ -2077,12 +2077,13 @@ import { existsSync as existsSync7 } from "fs";
2077
2077
  import { appendFile as appendFile5, mkdir as mkdir10, readFile as readFile8, rm, writeFile as writeFile8 } from "fs/promises";
2078
2078
  import { homedir as homedir9 } from "os";
2079
2079
  import { join as join12 } from "path";
2080
- import { randomUUID as randomUUID4 } from "crypto";
2080
+ import { randomUUID as randomUUID5 } from "crypto";
2081
2081
 
2082
2082
  // src/managers/claude-manager.ts
2083
2083
  import {
2084
2084
  query
2085
2085
  } from "@anthropic-ai/claude-agent-sdk";
2086
+ import { randomUUID as randomUUID3 } from "crypto";
2086
2087
  import { join as join10 } from "path";
2087
2088
  import { mkdir as mkdir8, appendFile as appendFile4 } from "fs/promises";
2088
2089
  import { homedir as homedir7 } from "os";
@@ -2940,6 +2941,82 @@ var PromptStream = class {
2940
2941
  function isLinearThoughtEvent(event) {
2941
2942
  return event.content.type === "thought";
2942
2943
  }
2944
+ var INTERACTIVE_TOOL_NAMES = ["AskUserQuestion", "ExitPlanMode"];
2945
+ var TOOL_INPUT_HANDLERS = {
2946
+ ExitPlanMode: {
2947
+ getRequest: () => ({
2948
+ options: [
2949
+ { id: "approve", label: "Start implementing", variant: "primary" },
2950
+ { id: "deny", label: "Keep planning", variant: "secondary" }
2951
+ ]
2952
+ }),
2953
+ resolve: (selectionId, input) => {
2954
+ if (selectionId === "approve") {
2955
+ return { result: { behavior: "allow", updatedInput: input }, summary: "Start implementing" };
2956
+ }
2957
+ return { result: { behavior: "deny", message: "User declined to exit plan mode." }, summary: "Keep planning" };
2958
+ }
2959
+ },
2960
+ AskUserQuestion: {
2961
+ getRequest: (input) => {
2962
+ const questions = Array.isArray(input.questions) ? input.questions : [];
2963
+ return {
2964
+ questions: questions.map((q, qi) => ({
2965
+ id: String(qi),
2966
+ prompt: q.question,
2967
+ multiSelect: Boolean(q.multiSelect),
2968
+ options: (q.options ?? []).map((opt, oi) => ({
2969
+ id: String(oi),
2970
+ label: opt.label,
2971
+ description: opt.description,
2972
+ preview: opt.preview
2973
+ }))
2974
+ }))
2975
+ };
2976
+ },
2977
+ resolve: (selectionId, input) => {
2978
+ if (selectionId === "declined") {
2979
+ return {
2980
+ result: { behavior: "deny", message: "User declined to answer questions." },
2981
+ summary: "Declined to answer"
2982
+ };
2983
+ }
2984
+ let parsed;
2985
+ try {
2986
+ parsed = JSON.parse(selectionId);
2987
+ } catch {
2988
+ return {
2989
+ result: { behavior: "deny", message: "Invalid selection payload." }
2990
+ };
2991
+ }
2992
+ const questions = Array.isArray(input.questions) ? input.questions : [];
2993
+ const answers = {};
2994
+ const summaryParts = [];
2995
+ for (let qi = 0; qi < questions.length; qi++) {
2996
+ const q = questions[qi];
2997
+ const entry = parsed[String(qi)];
2998
+ if (!entry) continue;
2999
+ let answer = null;
3000
+ if (typeof entry.custom === "string" && entry.custom.trim()) {
3001
+ answer = entry.custom.trim();
3002
+ } else if (Array.isArray(entry.options) && entry.options.length > 0) {
3003
+ const labels = entry.options.map((idx) => q.options[Number(idx)]?.label).filter((l) => Boolean(l));
3004
+ if (labels.length > 0) {
3005
+ answer = labels.join(", ");
3006
+ }
3007
+ }
3008
+ if (answer) {
3009
+ answers[q.question] = answer;
3010
+ summaryParts.push(`${q.question}: ${answer}`);
3011
+ }
3012
+ }
3013
+ return {
3014
+ result: { behavior: "allow", updatedInput: { ...input, answers } },
3015
+ summary: summaryParts.join(" \u2022 ")
3016
+ };
3017
+ }
3018
+ }
3019
+ };
2943
3020
  var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2944
3021
  historyFile;
2945
3022
  sessionId = null;
@@ -2951,6 +3028,8 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2951
3028
  mcpServersConfig;
2952
3029
  envOverrides;
2953
3030
  disallowedToolsOverride;
3031
+ /** Active tool-input requests keyed by requestId; resolved when the user selects an option. */
3032
+ pendingToolInputs = /* @__PURE__ */ new Map();
2954
3033
  constructor(options) {
2955
3034
  super(options);
2956
3035
  this.historyFile = options.historyFilePath ?? join10(homedir7(), ".replicas", "claude", "history.jsonl");
@@ -2967,6 +3046,96 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2967
3046
  if (this.activeQuery) {
2968
3047
  await this.activeQuery.interrupt();
2969
3048
  }
3049
+ await this.abortAllPendingToolInputs();
3050
+ }
3051
+ async abortAllPendingToolInputs() {
3052
+ const pending = Array.from(this.pendingToolInputs.values());
3053
+ this.pendingToolInputs.clear();
3054
+ for (const entry of pending) {
3055
+ entry.resolve({ behavior: "deny", message: "Turn interrupted" });
3056
+ await this.emitToolInputEvent("replicas-tool-input-resolved", {
3057
+ requestId: entry.requestId,
3058
+ toolUseId: entry.toolUseId,
3059
+ toolName: entry.toolName,
3060
+ selectionId: "aborted"
3061
+ });
3062
+ }
3063
+ }
3064
+ async respondToToolInput(requestId, selectionId) {
3065
+ const pending = this.pendingToolInputs.get(requestId);
3066
+ if (!pending) {
3067
+ return false;
3068
+ }
3069
+ this.pendingToolInputs.delete(requestId);
3070
+ const resolution = pending.handler.resolve(selectionId, pending.input);
3071
+ pending.resolve(resolution.result);
3072
+ await this.emitToolInputEvent("replicas-tool-input-resolved", {
3073
+ requestId,
3074
+ toolUseId: pending.toolUseId,
3075
+ toolName: pending.toolName,
3076
+ selectionId,
3077
+ ...resolution.summary ? { selectionSummary: resolution.summary } : {}
3078
+ });
3079
+ return true;
3080
+ }
3081
+ async emitToolInputEvent(type, payload) {
3082
+ const event = {
3083
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3084
+ type,
3085
+ payload: { ...payload, parent_tool_use_id: null }
3086
+ };
3087
+ await appendFile4(this.historyFile, JSON.stringify(event) + "\n", "utf-8");
3088
+ this.onEvent(event);
3089
+ }
3090
+ buildCanUseTool() {
3091
+ return async (toolName, input, options) => {
3092
+ const handler = TOOL_INPUT_HANDLERS[toolName];
3093
+ if (!handler) {
3094
+ return { behavior: "allow" };
3095
+ }
3096
+ const requestId = randomUUID3();
3097
+ const toolUseId = options.toolUseID;
3098
+ const { options: requestOptions, questions: requestQuestions } = handler.getRequest(input);
3099
+ const result = await new Promise((resolve3) => {
3100
+ this.pendingToolInputs.set(requestId, {
3101
+ requestId,
3102
+ toolUseId,
3103
+ toolName,
3104
+ input,
3105
+ handler,
3106
+ resolve: resolve3
3107
+ });
3108
+ const onAbort = () => {
3109
+ const pending = this.pendingToolInputs.get(requestId);
3110
+ if (!pending) return;
3111
+ this.pendingToolInputs.delete(requestId);
3112
+ pending.resolve({ behavior: "deny", message: "Aborted." });
3113
+ this.emitToolInputEvent("replicas-tool-input-resolved", {
3114
+ requestId: pending.requestId,
3115
+ toolUseId: pending.toolUseId,
3116
+ toolName: pending.toolName,
3117
+ selectionId: "aborted"
3118
+ }).catch((err) => {
3119
+ console.error("[ClaudeManager] Failed to emit resolved event on abort:", err);
3120
+ });
3121
+ };
3122
+ if (options.signal.aborted) {
3123
+ onAbort();
3124
+ return;
3125
+ }
3126
+ options.signal.addEventListener("abort", onAbort, { once: true });
3127
+ this.emitToolInputEvent("replicas-tool-input-request", {
3128
+ requestId,
3129
+ toolUseId,
3130
+ toolName,
3131
+ ...requestOptions ? { options: requestOptions } : {},
3132
+ ...requestQuestions ? { questions: requestQuestions } : {}
3133
+ }).catch((err) => {
3134
+ console.error("[ClaudeManager] Failed to emit tool-input request event:", err);
3135
+ });
3136
+ });
3137
+ return result;
3138
+ };
2970
3139
  }
2971
3140
  /**
2972
3141
  * Internal method that actually processes the message.
@@ -2997,7 +3166,8 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2997
3166
  customInstructions,
2998
3167
  images,
2999
3168
  permissionMode,
3000
- thinkingLevel
3169
+ thinkingLevel,
3170
+ enableInteractiveTools
3001
3171
  } = request;
3002
3172
  const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
3003
3173
  if (!message || !message.trim()) {
@@ -3045,6 +3215,10 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
3045
3215
  };
3046
3216
  const queryEnv = buildClaudeAgentEnv(this.envOverrides);
3047
3217
  const resolvedModel = normalizeClaudeModel(model) || CLAUDE_OPUS_1M_MODEL;
3218
+ const disallowedTools = [
3219
+ ...this.disallowedToolsOverride ?? [],
3220
+ ...enableInteractiveTools ? [] : INTERACTIVE_TOOL_NAMES
3221
+ ];
3048
3222
  const response = query({
3049
3223
  prompt: promptStream,
3050
3224
  options: {
@@ -3053,13 +3227,14 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
3053
3227
  permissionMode: permissionMode === "read" ? "plan" : "bypassPermissions",
3054
3228
  allowDangerouslySkipPermissions: permissionMode !== "read",
3055
3229
  ...this.toolsOverride ? { tools: this.toolsOverride } : {},
3056
- disallowedTools: this.disallowedToolsOverride ?? ["ExitPlanMode", "AskUserQuestion"],
3230
+ ...disallowedTools.length > 0 ? { disallowedTools } : {},
3057
3231
  settingSources: ["user", "project", "local"],
3058
3232
  systemPrompt,
3059
3233
  ...this.mcpServersConfig ? { mcpServers: this.mcpServersConfig } : {},
3060
3234
  env: queryEnv,
3061
3235
  model: resolvedModel,
3062
- ...thinkingLevel ? { effort: thinkingLevel } : {}
3236
+ ...thinkingLevel ? { effort: thinkingLevel } : {},
3237
+ canUseTool: this.buildCanUseTool()
3063
3238
  }
3064
3239
  });
3065
3240
  this.activeQuery = response;
@@ -3229,7 +3404,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
3229
3404
 
3230
3405
  // src/managers/codex-manager.ts
3231
3406
  import { Codex } from "@openai/codex-sdk";
3232
- import { randomUUID as randomUUID3 } from "crypto";
3407
+ import { randomUUID as randomUUID4 } from "crypto";
3233
3408
  import { readdir as readdir3, stat as stat2, writeFile as writeFile7, mkdir as mkdir9, readFile as readFile7 } from "fs/promises";
3234
3409
  import { existsSync as existsSync6 } from "fs";
3235
3410
  import { join as join11 } from "path";
@@ -3412,7 +3587,7 @@ var CodexManager = class extends CodingAgentManager {
3412
3587
  const tempPaths = [];
3413
3588
  for (const image of images) {
3414
3589
  const ext = image.source.media_type.split("/")[1] || "png";
3415
- const filename = `img_${randomUUID3()}.${ext}`;
3590
+ const filename = `img_${randomUUID4()}.${ext}`;
3416
3591
  const filepath = join11(this.tempImageDir, filename);
3417
3592
  const buffer = Buffer.from(image.source.data, "base64");
3418
3593
  await writeFile7(filepath, buffer);
@@ -4387,7 +4562,7 @@ var ChatService = class {
4387
4562
  throw new ChatNotFoundError(parentChatId);
4388
4563
  }
4389
4564
  const persisted = {
4390
- id: randomUUID4(),
4565
+ id: randomUUID5(),
4391
4566
  provider: request.provider,
4392
4567
  title,
4393
4568
  createdAt: now,
@@ -4509,6 +4684,13 @@ var ChatService = class {
4509
4684
  queue: chat.provider.getQueue()
4510
4685
  };
4511
4686
  }
4687
+ async respondToToolInput(chatId, requestId, selectionId) {
4688
+ const chat = this.requireChat(chatId);
4689
+ if (!chat.provider.respondToToolInput) {
4690
+ return false;
4691
+ }
4692
+ return chat.provider.respondToToolInput(requestId, selectionId);
4693
+ }
4512
4694
  async deleteChat(chatId) {
4513
4695
  const chat = this.requireChat(chatId);
4514
4696
  if (chat.persisted.title === DEFAULT_CHAT_TITLES[chat.persisted.provider]) {
@@ -4733,7 +4915,7 @@ var ChatService = class {
4733
4915
  }
4734
4916
  async publish(input) {
4735
4917
  const event = {
4736
- id: randomUUID4(),
4918
+ id: randomUUID5(),
4737
4919
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4738
4920
  ...input
4739
4921
  };
@@ -5506,10 +5688,15 @@ var sendMessageSchema = z2.object({
5506
5688
  ])
5507
5689
  })).optional(),
5508
5690
  thinkingLevel: z2.enum(["low", "medium", "high", "max"]).optional(),
5691
+ enableInteractiveTools: z2.boolean().optional(),
5509
5692
  senderUserId: z2.string().optional(),
5510
5693
  senderEmail: z2.string().optional(),
5511
5694
  senderDisplayName: z2.string().optional()
5512
5695
  });
5696
+ var respondToolInputSchema = z2.object({
5697
+ requestId: z2.string().min(1),
5698
+ selectionId: z2.string().min(1)
5699
+ });
5513
5700
  function jsonError(message, details) {
5514
5701
  return { error: message, details };
5515
5702
  }
@@ -5708,6 +5895,22 @@ function createV1Routes(deps) {
5708
5895
  return c.json(jsonError("Failed to reorder queue", error instanceof Error ? error.message : "Unknown error"), 500);
5709
5896
  }
5710
5897
  });
5898
+ app2.post("/chats/:chatId/tool-input", async (c) => {
5899
+ try {
5900
+ const body = respondToolInputSchema.parse(await c.req.json());
5901
+ const resolved = await deps.chatService.respondToToolInput(
5902
+ c.req.param("chatId"),
5903
+ body.requestId,
5904
+ body.selectionId
5905
+ );
5906
+ return c.json({ resolved });
5907
+ } catch (error) {
5908
+ if (error instanceof ChatNotFoundError) {
5909
+ return c.json(jsonError("Failed to resolve tool input", error.message), 404);
5910
+ }
5911
+ return c.json(jsonError("Failed to resolve tool input", error instanceof Error ? error.message : "Unknown error"), 400);
5912
+ }
5913
+ });
5711
5914
  app2.get("/repos", async (c) => {
5712
5915
  const includeDiffs = c.req.query("includeDiffs") === "true";
5713
5916
  const repos = await gitService.listRepos({ includeDiffs });
@@ -6170,7 +6373,7 @@ function startStatusBroadcaster() {
6170
6373
  if (serialized !== previousRepoStatus) {
6171
6374
  previousRepoStatus = serialized;
6172
6375
  eventService.publish({
6173
- id: randomUUID5(),
6376
+ id: randomUUID6(),
6174
6377
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6175
6378
  type: "repo.status.changed",
6176
6379
  payload: { repos }
@@ -6190,7 +6393,7 @@ function startStatusBroadcaster() {
6190
6393
  if (engineStatusJson !== previousEngineStatus) {
6191
6394
  previousEngineStatus = engineStatusJson;
6192
6395
  eventService.publish({
6193
- id: randomUUID5(),
6396
+ id: randomUUID6(),
6194
6397
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6195
6398
  type: "engine.status.changed",
6196
6399
  payload: { status: engineStatus }
@@ -6211,7 +6414,7 @@ function startStatusBroadcaster() {
6211
6414
  previousHookStatus = hookSnapshot;
6212
6415
  if (!lastHooksRunning && hooksRunning) {
6213
6416
  eventService.publish({
6214
- id: randomUUID5(),
6417
+ id: randomUUID6(),
6215
6418
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6216
6419
  type: "hooks.started",
6217
6420
  payload: { running: true, completed: false }
@@ -6220,7 +6423,7 @@ function startStatusBroadcaster() {
6220
6423
  }
6221
6424
  if (hooksRunning) {
6222
6425
  eventService.publish({
6223
- id: randomUUID5(),
6426
+ id: randomUUID6(),
6224
6427
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6225
6428
  type: "hooks.progress",
6226
6429
  payload: { running: true, completed: false }
@@ -6229,7 +6432,7 @@ function startStatusBroadcaster() {
6229
6432
  }
6230
6433
  if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
6231
6434
  eventService.publish({
6232
- id: randomUUID5(),
6435
+ id: randomUUID6(),
6233
6436
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6234
6437
  type: "hooks.completed",
6235
6438
  payload: { running: false, completed: true }
@@ -6238,7 +6441,7 @@ function startStatusBroadcaster() {
6238
6441
  }
6239
6442
  if (lastHooksRunning && !hooksRunning && hooksFailed) {
6240
6443
  eventService.publish({
6241
- id: randomUUID5(),
6444
+ id: randomUUID6(),
6242
6445
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6243
6446
  type: "hooks.failed",
6244
6447
  payload: { running: false, completed: hooksCompleted }
@@ -6246,7 +6449,7 @@ function startStatusBroadcaster() {
6246
6449
  });
6247
6450
  }
6248
6451
  eventService.publish({
6249
- id: randomUUID5(),
6452
+ id: randomUUID6(),
6250
6453
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6251
6454
  type: "hooks.status",
6252
6455
  payload: {
@@ -6287,20 +6490,20 @@ serve(
6287
6490
  }
6288
6491
  const repos = await gitService.listRepos();
6289
6492
  await eventService.publish({
6290
- id: randomUUID5(),
6493
+ id: randomUUID6(),
6291
6494
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6292
6495
  type: "repo.discovered",
6293
6496
  payload: { repos }
6294
6497
  });
6295
6498
  const repoStatuses = await gitService.listRepos();
6296
6499
  await eventService.publish({
6297
- id: randomUUID5(),
6500
+ id: randomUUID6(),
6298
6501
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6299
6502
  type: "repo.status.changed",
6300
6503
  payload: { repos: repoStatuses }
6301
6504
  });
6302
6505
  await eventService.publish({
6303
- id: randomUUID5(),
6506
+ id: randomUUID6(),
6304
6507
  ts: (/* @__PURE__ */ new Date()).toISOString(),
6305
6508
  type: "engine.ready",
6306
6509
  payload: { version: "v1" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.173",
3
+ "version": "0.1.174",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",