replicas-engine 0.1.48 → 0.1.50

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 +336 -71
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -5,7 +5,7 @@ import "./chunk-ZXMDA7VB.js";
5
5
  import "dotenv/config";
6
6
  import { serve } from "@hono/node-server";
7
7
  import { Hono as Hono2 } from "hono";
8
- import { readFile as readFile8 } from "fs/promises";
8
+ import { readFile as readFile9 } from "fs/promises";
9
9
  import { execSync } from "child_process";
10
10
  import { randomUUID as randomUUID4 } from "crypto";
11
11
 
@@ -829,13 +829,11 @@ var EngineLogger = class {
829
829
  };
830
830
  var engineLogger = new EngineLogger();
831
831
 
832
- // src/services/replicas-config-service.ts
833
- import { readFile as readFile2, appendFile as appendFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
832
+ // src/services/environment-details-service.ts
833
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
834
834
  import { existsSync as existsSync3 } from "fs";
835
- import { join as join5 } from "path";
836
835
  import { homedir as homedir4 } from "os";
837
- import { exec } from "child_process";
838
- import { promisify } from "util";
836
+ import { join as join5 } from "path";
839
837
 
840
838
  // ../shared/src/sandbox.ts
841
839
  var SANDBOX_LIFECYCLE = {
@@ -898,6 +896,9 @@ function resolveWarmHookConfig(value) {
898
896
  };
899
897
  }
900
898
 
899
+ // ../shared/src/engine/environment.ts
900
+ var REPLICAS_ENGINE_VERSION = "09-03-2026-kipling";
901
+
901
902
  // ../shared/src/engine/types.ts
902
903
  var DEFAULT_CHAT_TITLES = {
903
904
  claude: "Claude Code",
@@ -909,9 +910,151 @@ var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
909
910
  var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
910
911
  var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
911
912
 
913
+ // src/services/environment-details-service.ts
914
+ var REPLICAS_DIR = join5(homedir4(), ".replicas");
915
+ var ENVIRONMENT_DETAILS_FILE = join5(REPLICAS_DIR, "environment-details.json");
916
+ function createExecutionItem(status, details) {
917
+ return { status, details: details ?? null };
918
+ }
919
+ function createDefaultDetails() {
920
+ return {
921
+ engineVersion: REPLICAS_ENGINE_VERSION,
922
+ globalWarmHookCompleted: createExecutionItem("n/a"),
923
+ repositories: [],
924
+ filesUploaded: [],
925
+ envVarsSet: [],
926
+ skillsInstalled: [],
927
+ runtimeEnvVarsSet: [],
928
+ repositoriesCloned: [],
929
+ gitIdentityConfigured: false,
930
+ githubCredentialsConfigured: false,
931
+ codexAuthConfigured: false,
932
+ claudeAuthConfigured: false,
933
+ bedrockAuthConfigured: false,
934
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
935
+ };
936
+ }
937
+ function mergeUnique(current, incoming) {
938
+ if (!incoming || incoming.length === 0) {
939
+ return current;
940
+ }
941
+ return Array.from(/* @__PURE__ */ new Set([...current, ...incoming]));
942
+ }
943
+ function upsertRepositoryStatus(current, incoming) {
944
+ const byName = new Map(current.map((repository) => [repository.repositoryName, repository]));
945
+ for (const repository of incoming) {
946
+ const existing = byName.get(repository.repositoryName);
947
+ byName.set(repository.repositoryName, {
948
+ repositoryName: repository.repositoryName,
949
+ warmHookCompleted: repository.warmHookCompleted !== "n/a" ? repository.warmHookCompleted : existing?.warmHookCompleted ?? "n/a",
950
+ startHookCompleted: repository.startHookCompleted !== "n/a" ? repository.startHookCompleted : existing?.startHookCompleted ?? "n/a"
951
+ });
952
+ }
953
+ return [...byName.values()].sort((a, b) => a.repositoryName.localeCompare(b.repositoryName));
954
+ }
955
+ var EnvironmentDetailsService = class {
956
+ async initialize() {
957
+ const current = await this.readDetails();
958
+ const repositories = await gitService.listRepositories();
959
+ const merged = upsertRepositoryStatus(
960
+ current.repositories,
961
+ repositories.map((repository) => ({
962
+ repositoryName: repository.name,
963
+ warmHookCompleted: "n/a",
964
+ startHookCompleted: "n/a"
965
+ }))
966
+ );
967
+ await this.writeDetails({
968
+ ...current,
969
+ engineVersion: REPLICAS_ENGINE_VERSION,
970
+ repositories: merged,
971
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
972
+ });
973
+ }
974
+ async getDetails() {
975
+ const current = await this.readDetails();
976
+ return {
977
+ ...current,
978
+ engineVersion: REPLICAS_ENGINE_VERSION
979
+ };
980
+ }
981
+ async track(update) {
982
+ const current = await this.readDetails();
983
+ const next = {
984
+ ...current,
985
+ engineVersion: REPLICAS_ENGINE_VERSION,
986
+ globalWarmHookCompleted: update.globalWarmHookCompleted ?? current.globalWarmHookCompleted,
987
+ repositories: update.repositories ? upsertRepositoryStatus(current.repositories, update.repositories) : current.repositories,
988
+ filesUploaded: mergeUnique(current.filesUploaded, update.filesUploaded),
989
+ envVarsSet: mergeUnique(current.envVarsSet, update.envVarsSet),
990
+ skillsInstalled: mergeUnique(current.skillsInstalled, update.skillsInstalled),
991
+ runtimeEnvVarsSet: mergeUnique(current.runtimeEnvVarsSet, update.runtimeEnvVarsSet),
992
+ repositoriesCloned: mergeUnique(current.repositoriesCloned, update.repositoriesCloned),
993
+ gitIdentityConfigured: update.gitIdentityConfigured ?? current.gitIdentityConfigured,
994
+ githubCredentialsConfigured: update.githubCredentialsConfigured ?? current.githubCredentialsConfigured,
995
+ codexAuthConfigured: update.codexAuthConfigured ?? current.codexAuthConfigured,
996
+ claudeAuthConfigured: update.claudeAuthConfigured ?? current.claudeAuthConfigured,
997
+ bedrockAuthConfigured: update.bedrockAuthConfigured ?? current.bedrockAuthConfigured,
998
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
999
+ };
1000
+ await this.writeDetails(next);
1001
+ return next;
1002
+ }
1003
+ async setGlobalWarmHook(status, details) {
1004
+ await this.track({
1005
+ globalWarmHookCompleted: createExecutionItem(status, details)
1006
+ });
1007
+ }
1008
+ async setRepositoryWarmHook(repositoryName, status) {
1009
+ await this.track({
1010
+ repositories: [{
1011
+ repositoryName,
1012
+ warmHookCompleted: status,
1013
+ startHookCompleted: "n/a"
1014
+ }]
1015
+ });
1016
+ }
1017
+ async setRepositoryStartHook(repositoryName, status) {
1018
+ await this.track({
1019
+ repositories: [{
1020
+ repositoryName,
1021
+ warmHookCompleted: "n/a",
1022
+ startHookCompleted: status
1023
+ }]
1024
+ });
1025
+ }
1026
+ async readDetails() {
1027
+ try {
1028
+ if (!existsSync3(ENVIRONMENT_DETAILS_FILE)) {
1029
+ return createDefaultDetails();
1030
+ }
1031
+ const raw = await readFile2(ENVIRONMENT_DETAILS_FILE, "utf-8");
1032
+ return {
1033
+ ...createDefaultDetails(),
1034
+ ...JSON.parse(raw),
1035
+ engineVersion: REPLICAS_ENGINE_VERSION
1036
+ };
1037
+ } catch {
1038
+ return createDefaultDetails();
1039
+ }
1040
+ }
1041
+ async writeDetails(details) {
1042
+ await mkdir3(REPLICAS_DIR, { recursive: true });
1043
+ await writeFile3(ENVIRONMENT_DETAILS_FILE, `${JSON.stringify(details, null, 2)}
1044
+ `, "utf-8");
1045
+ }
1046
+ };
1047
+ var environmentDetailsService = new EnvironmentDetailsService();
1048
+
912
1049
  // src/services/replicas-config-service.ts
1050
+ import { readFile as readFile3, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1051
+ import { existsSync as existsSync4 } from "fs";
1052
+ import { join as join6 } from "path";
1053
+ import { homedir as homedir5 } from "os";
1054
+ import { exec } from "child_process";
1055
+ import { promisify } from "util";
913
1056
  var execAsync = promisify(exec);
914
- var START_HOOKS_LOG = join5(homedir4(), ".replicas", "startHooks.log");
1057
+ var START_HOOKS_LOG = join6(homedir5(), ".replicas", "startHooks.log");
915
1058
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
916
1059
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
917
1060
  These hooks are currently executing in the background. You can:
@@ -991,12 +1134,12 @@ var ReplicasConfigService = class {
991
1134
  const repos = await gitService.listRepositories();
992
1135
  const configs = [];
993
1136
  for (const repo of repos) {
994
- const configPath = join5(repo.path, "replicas.json");
995
- if (!existsSync3(configPath)) {
1137
+ const configPath = join6(repo.path, "replicas.json");
1138
+ if (!existsSync4(configPath)) {
996
1139
  continue;
997
1140
  }
998
1141
  try {
999
- const data = await readFile2(configPath, "utf-8");
1142
+ const data = await readFile3(configPath, "utf-8");
1000
1143
  const config = parseReplicasConfig(JSON.parse(data));
1001
1144
  configs.push({
1002
1145
  repoName: repo.name,
@@ -1022,7 +1165,7 @@ var ReplicasConfigService = class {
1022
1165
  const logLine = `[${timestamp}] ${message}
1023
1166
  `;
1024
1167
  try {
1025
- await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
1168
+ await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1026
1169
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
1027
1170
  } catch (error) {
1028
1171
  console.error("Failed to write to start hooks log:", error);
@@ -1037,13 +1180,17 @@ var ReplicasConfigService = class {
1037
1180
  this.hooksRunning = false;
1038
1181
  this.hooksCompleted = true;
1039
1182
  this.hooksFailed = false;
1183
+ const repos = await gitService.listRepositories();
1184
+ for (const repo of repos) {
1185
+ await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
1186
+ }
1040
1187
  return;
1041
1188
  }
1042
1189
  this.hooksRunning = true;
1043
1190
  this.hooksCompleted = false;
1044
1191
  try {
1045
- await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
1046
- await writeFile3(
1192
+ await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
1193
+ await writeFile4(
1047
1194
  START_HOOKS_LOG,
1048
1195
  `=== Start Hooks Execution Log ===
1049
1196
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
@@ -1052,6 +1199,13 @@ Repositories: ${hookEntries.length}
1052
1199
  `,
1053
1200
  "utf-8"
1054
1201
  );
1202
+ const repos = await gitService.listRepositories();
1203
+ const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
1204
+ for (const repo of repos) {
1205
+ if (!reposWithHooks.has(repo.name)) {
1206
+ await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
1207
+ }
1208
+ }
1055
1209
  for (const entry of hookEntries) {
1056
1210
  const startHookConfig = entry.config.startHook;
1057
1211
  if (!startHookConfig) {
@@ -1060,9 +1214,11 @@ Repositories: ${hookEntries.length}
1060
1214
  const persistedRepoState = await loadRepoState(entry.repoName);
1061
1215
  if (persistedRepoState?.startHooksCompleted) {
1062
1216
  await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
1217
+ await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
1063
1218
  continue;
1064
1219
  }
1065
1220
  const timeout = startHookConfig.timeout ?? 3e5;
1221
+ let repoFailed = false;
1066
1222
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
1067
1223
  for (const hook of startHookConfig.commands) {
1068
1224
  try {
@@ -1082,7 +1238,9 @@ Repositories: ${hookEntries.length}
1082
1238
  } catch (error) {
1083
1239
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1084
1240
  this.hooksFailed = true;
1241
+ repoFailed = true;
1085
1242
  await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
1243
+ await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
1086
1244
  }
1087
1245
  }
1088
1246
  const fallbackRepoState = persistedRepoState ?? {
@@ -1095,6 +1253,7 @@ Repositories: ${hookEntries.length}
1095
1253
  startHooksCompleted: false
1096
1254
  };
1097
1255
  await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
1256
+ await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
1098
1257
  }
1099
1258
  this.hooksCompleted = true;
1100
1259
  await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
@@ -1152,17 +1311,17 @@ Repositories: ${hookEntries.length}
1152
1311
  var replicasConfigService = new ReplicasConfigService();
1153
1312
 
1154
1313
  // src/services/event-service.ts
1155
- import { appendFile as appendFile3, mkdir as mkdir4 } from "fs/promises";
1156
- import { homedir as homedir5 } from "os";
1157
- import { join as join6 } from "path";
1314
+ import { appendFile as appendFile3, mkdir as mkdir5 } from "fs/promises";
1315
+ import { homedir as homedir6 } from "os";
1316
+ import { join as join7 } from "path";
1158
1317
  import { randomUUID } from "crypto";
1159
- var ENGINE_DIR = join6(homedir5(), ".replicas", "engine");
1160
- var EVENTS_FILE = join6(ENGINE_DIR, "events.jsonl");
1318
+ var ENGINE_DIR = join7(homedir6(), ".replicas", "engine");
1319
+ var EVENTS_FILE = join7(ENGINE_DIR, "events.jsonl");
1161
1320
  var EventService = class {
1162
1321
  subscribers = /* @__PURE__ */ new Map();
1163
1322
  writeChain = Promise.resolve();
1164
1323
  async initialize() {
1165
- await mkdir4(ENGINE_DIR, { recursive: true });
1324
+ await mkdir5(ENGINE_DIR, { recursive: true });
1166
1325
  }
1167
1326
  subscribe(subscriber) {
1168
1327
  const id = randomUUID();
@@ -1192,21 +1351,21 @@ var EventService = class {
1192
1351
  var eventService = new EventService();
1193
1352
 
1194
1353
  // src/services/chat/chat-service.ts
1195
- import { mkdir as mkdir7, readFile as readFile5, rm, writeFile as writeFile5 } from "fs/promises";
1196
- import { homedir as homedir8 } from "os";
1197
- import { join as join9 } from "path";
1354
+ import { mkdir as mkdir8, readFile as readFile6, rm, writeFile as writeFile6 } from "fs/promises";
1355
+ import { homedir as homedir9 } from "os";
1356
+ import { join as join10 } from "path";
1198
1357
  import { randomUUID as randomUUID3 } from "crypto";
1199
1358
 
1200
1359
  // src/managers/claude-manager.ts
1201
1360
  import {
1202
1361
  query
1203
1362
  } from "@anthropic-ai/claude-agent-sdk";
1204
- import { join as join7 } from "path";
1205
- import { mkdir as mkdir5, appendFile as appendFile4 } from "fs/promises";
1206
- import { homedir as homedir6 } from "os";
1363
+ import { join as join8 } from "path";
1364
+ import { mkdir as mkdir6, appendFile as appendFile4 } from "fs/promises";
1365
+ import { homedir as homedir7 } from "os";
1207
1366
 
1208
1367
  // src/utils/jsonl-reader.ts
1209
- import { readFile as readFile3 } from "fs/promises";
1368
+ import { readFile as readFile4 } from "fs/promises";
1210
1369
  function isJsonlEvent(value) {
1211
1370
  if (!isRecord(value)) {
1212
1371
  return false;
@@ -1228,7 +1387,7 @@ function parseJsonlEvents(lines) {
1228
1387
  }
1229
1388
  async function readJSONL(filePath) {
1230
1389
  try {
1231
- const content = await readFile3(filePath, "utf-8");
1390
+ const content = await readFile4(filePath, "utf-8");
1232
1391
  const lines = content.split("\n").filter((line) => line.trim());
1233
1392
  return parseJsonlEvents(lines);
1234
1393
  } catch (error) {
@@ -1962,7 +2121,7 @@ var ClaudeManager = class extends CodingAgentManager {
1962
2121
  pendingInterrupt = false;
1963
2122
  constructor(options) {
1964
2123
  super(options);
1965
- this.historyFile = options.historyFilePath ?? join7(homedir6(), ".replicas", "claude", "history.jsonl");
2124
+ this.historyFile = options.historyFilePath ?? join8(homedir7(), ".replicas", "claude", "history.jsonl");
1966
2125
  this.initializeManager(this.processMessageInternal.bind(this));
1967
2126
  }
1968
2127
  async interruptActiveTurn() {
@@ -2100,8 +2259,8 @@ var ClaudeManager = class extends CodingAgentManager {
2100
2259
  };
2101
2260
  }
2102
2261
  async initialize() {
2103
- const historyDir = join7(homedir6(), ".replicas", "claude");
2104
- await mkdir5(historyDir, { recursive: true });
2262
+ const historyDir = join8(homedir7(), ".replicas", "claude");
2263
+ await mkdir6(historyDir, { recursive: true });
2105
2264
  if (this.initialSessionId) {
2106
2265
  this.sessionId = this.initialSessionId;
2107
2266
  console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
@@ -2130,13 +2289,13 @@ var ClaudeManager = class extends CodingAgentManager {
2130
2289
  // src/managers/codex-manager.ts
2131
2290
  import { Codex } from "@openai/codex-sdk";
2132
2291
  import { randomUUID as randomUUID2 } from "crypto";
2133
- import { readdir as readdir2, stat as stat2, writeFile as writeFile4, mkdir as mkdir6, readFile as readFile4 } from "fs/promises";
2134
- import { existsSync as existsSync4 } from "fs";
2135
- import { join as join8 } from "path";
2136
- import { homedir as homedir7 } from "os";
2292
+ import { readdir as readdir2, stat as stat2, writeFile as writeFile5, mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
2293
+ import { existsSync as existsSync5 } from "fs";
2294
+ import { join as join9 } from "path";
2295
+ import { homedir as homedir8 } from "os";
2137
2296
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2138
2297
  var DEFAULT_MODEL = "gpt-5.4";
2139
- var CODEX_CONFIG_PATH = join8(homedir7(), ".codex", "config.toml");
2298
+ var CODEX_CONFIG_PATH = join9(homedir8(), ".codex", "config.toml");
2140
2299
  function isLinearThoughtEvent2(event) {
2141
2300
  return event.content.type === "thought";
2142
2301
  }
@@ -2158,7 +2317,7 @@ var CodexManager = class extends CodingAgentManager {
2158
2317
  constructor(options) {
2159
2318
  super(options);
2160
2319
  this.codex = new Codex();
2161
- this.tempImageDir = join8(homedir7(), ".replicas", "codex", "temp-images");
2320
+ this.tempImageDir = join9(homedir8(), ".replicas", "codex", "temp-images");
2162
2321
  this.initializeManager(this.processMessageInternal.bind(this));
2163
2322
  }
2164
2323
  async initialize() {
@@ -2178,12 +2337,12 @@ var CodexManager = class extends CodingAgentManager {
2178
2337
  */
2179
2338
  async updateCodexConfig(developerInstructions) {
2180
2339
  try {
2181
- const codexDir = join8(homedir7(), ".codex");
2182
- await mkdir6(codexDir, { recursive: true });
2340
+ const codexDir = join9(homedir8(), ".codex");
2341
+ await mkdir7(codexDir, { recursive: true });
2183
2342
  let config = {};
2184
- if (existsSync4(CODEX_CONFIG_PATH)) {
2343
+ if (existsSync5(CODEX_CONFIG_PATH)) {
2185
2344
  try {
2186
- const existingContent = await readFile4(CODEX_CONFIG_PATH, "utf-8");
2345
+ const existingContent = await readFile5(CODEX_CONFIG_PATH, "utf-8");
2187
2346
  const parsed = parseToml(existingContent);
2188
2347
  if (isRecord(parsed)) {
2189
2348
  config = parsed;
@@ -2198,7 +2357,7 @@ var CodexManager = class extends CodingAgentManager {
2198
2357
  delete config.developer_instructions;
2199
2358
  }
2200
2359
  const tomlContent = stringifyToml(config);
2201
- await writeFile4(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2360
+ await writeFile5(CODEX_CONFIG_PATH, tomlContent, "utf-8");
2202
2361
  console.log("[CodexManager] Updated config.toml with developer_instructions");
2203
2362
  } catch (error) {
2204
2363
  console.error("[CodexManager] Failed to update config.toml:", error);
@@ -2209,14 +2368,14 @@ var CodexManager = class extends CodingAgentManager {
2209
2368
  * @returns Array of temp file paths
2210
2369
  */
2211
2370
  async saveImagesToTempFiles(images) {
2212
- await mkdir6(this.tempImageDir, { recursive: true });
2371
+ await mkdir7(this.tempImageDir, { recursive: true });
2213
2372
  const tempPaths = [];
2214
2373
  for (const image of images) {
2215
2374
  const ext = image.source.media_type.split("/")[1] || "png";
2216
2375
  const filename = `img_${randomUUID2()}.${ext}`;
2217
- const filepath = join8(this.tempImageDir, filename);
2376
+ const filepath = join9(this.tempImageDir, filename);
2218
2377
  const buffer = Buffer.from(image.source.data, "base64");
2219
- await writeFile4(filepath, buffer);
2378
+ await writeFile5(filepath, buffer);
2220
2379
  tempPaths.push(filepath);
2221
2380
  }
2222
2381
  return tempPaths;
@@ -2346,13 +2505,13 @@ var CodexManager = class extends CodingAgentManager {
2346
2505
  }
2347
2506
  // Helper methods for finding session files
2348
2507
  async findSessionFile(threadId) {
2349
- const sessionsDir = join8(homedir7(), ".codex", "sessions");
2508
+ const sessionsDir = join9(homedir8(), ".codex", "sessions");
2350
2509
  try {
2351
2510
  const now = /* @__PURE__ */ new Date();
2352
2511
  const year = now.getFullYear();
2353
2512
  const month = String(now.getMonth() + 1).padStart(2, "0");
2354
2513
  const day = String(now.getDate()).padStart(2, "0");
2355
- const todayDir = join8(sessionsDir, String(year), month, day);
2514
+ const todayDir = join9(sessionsDir, String(year), month, day);
2356
2515
  const file = await this.findFileInDirectory(todayDir, threadId);
2357
2516
  if (file) return file;
2358
2517
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -2361,7 +2520,7 @@ var CodexManager = class extends CodingAgentManager {
2361
2520
  const searchYear = date.getFullYear();
2362
2521
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
2363
2522
  const searchDay = String(date.getDate()).padStart(2, "0");
2364
- const searchDir = join8(sessionsDir, String(searchYear), searchMonth, searchDay);
2523
+ const searchDir = join9(sessionsDir, String(searchYear), searchMonth, searchDay);
2365
2524
  const file2 = await this.findFileInDirectory(searchDir, threadId);
2366
2525
  if (file2) return file2;
2367
2526
  }
@@ -2375,7 +2534,7 @@ var CodexManager = class extends CodingAgentManager {
2375
2534
  const files = await readdir2(directory);
2376
2535
  for (const file of files) {
2377
2536
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2378
- const fullPath = join8(directory, file);
2537
+ const fullPath = join9(directory, file);
2379
2538
  const stats = await stat2(fullPath);
2380
2539
  if (stats.isFile()) {
2381
2540
  return fullPath;
@@ -2408,7 +2567,7 @@ var CodexManager = class extends CodingAgentManager {
2408
2567
  const seenLines = /* @__PURE__ */ new Set();
2409
2568
  const seedSeenLines = async () => {
2410
2569
  try {
2411
- const content = await readFile4(sessionFile, "utf-8");
2570
+ const content = await readFile5(sessionFile, "utf-8");
2412
2571
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2413
2572
  for (const line of lines) {
2414
2573
  seenLines.add(line);
@@ -2420,7 +2579,7 @@ var CodexManager = class extends CodingAgentManager {
2420
2579
  const pump = async () => {
2421
2580
  let emitted = 0;
2422
2581
  try {
2423
- const content = await readFile4(sessionFile, "utf-8");
2582
+ const content = await readFile5(sessionFile, "utf-8");
2424
2583
  const lines = content.split("\n");
2425
2584
  const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
2426
2585
  for (const line of completeLines) {
@@ -2491,9 +2650,9 @@ var DuplicateDefaultChatError = class extends Error {
2491
2650
  };
2492
2651
 
2493
2652
  // src/services/chat/chat-service.ts
2494
- var ENGINE_DIR2 = join9(homedir8(), ".replicas", "engine");
2495
- var CHATS_FILE = join9(ENGINE_DIR2, "chats.json");
2496
- var CLAUDE_HISTORY_DIR = join9(ENGINE_DIR2, "claude-histories");
2653
+ var ENGINE_DIR2 = join10(homedir9(), ".replicas", "engine");
2654
+ var CHATS_FILE = join10(ENGINE_DIR2, "chats.json");
2655
+ var CLAUDE_HISTORY_DIR = join10(ENGINE_DIR2, "claude-histories");
2497
2656
  function isPersistedChat(value) {
2498
2657
  if (!isRecord(value)) {
2499
2658
  return false;
@@ -2508,8 +2667,8 @@ var ChatService = class {
2508
2667
  chats = /* @__PURE__ */ new Map();
2509
2668
  writeChain = Promise.resolve();
2510
2669
  async initialize() {
2511
- await mkdir7(ENGINE_DIR2, { recursive: true });
2512
- await mkdir7(CLAUDE_HISTORY_DIR, { recursive: true });
2670
+ await mkdir8(ENGINE_DIR2, { recursive: true });
2671
+ await mkdir8(CLAUDE_HISTORY_DIR, { recursive: true });
2513
2672
  const persisted = await this.loadChats();
2514
2673
  for (const chat of persisted) {
2515
2674
  const runtime = this.createRuntimeChat(chat);
@@ -2593,7 +2752,7 @@ var ChatService = class {
2593
2752
  this.chats.delete(chatId);
2594
2753
  await this.persistAllChats();
2595
2754
  if (chat.persisted.provider === "claude") {
2596
- const historyFilePath = join9(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
2755
+ const historyFilePath = join10(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
2597
2756
  await rm(historyFilePath, { force: true });
2598
2757
  }
2599
2758
  await this.publish({
@@ -2636,7 +2795,7 @@ var ChatService = class {
2636
2795
  };
2637
2796
  const provider = persisted.provider === "claude" ? new ClaudeManager({
2638
2797
  workingDirectory: this.workingDirectory,
2639
- historyFilePath: join9(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
2798
+ historyFilePath: join10(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
2640
2799
  initialSessionId: persisted.providerSessionId,
2641
2800
  onSaveSessionId: saveSession,
2642
2801
  onTurnComplete: onProviderTurnComplete,
@@ -2732,7 +2891,7 @@ var ChatService = class {
2732
2891
  }
2733
2892
  async loadChats() {
2734
2893
  try {
2735
- const content = await readFile5(CHATS_FILE, "utf-8");
2894
+ const content = await readFile6(CHATS_FILE, "utf-8");
2736
2895
  const parsed = JSON.parse(content);
2737
2896
  if (!Array.isArray(parsed)) {
2738
2897
  return [];
@@ -2749,7 +2908,7 @@ var ChatService = class {
2749
2908
  this.writeChain = this.writeChain.catch(() => {
2750
2909
  }).then(async () => {
2751
2910
  const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
2752
- await writeFile5(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
2911
+ await writeFile6(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
2753
2912
  });
2754
2913
  await this.writeChain;
2755
2914
  }
@@ -2774,12 +2933,12 @@ import { Hono } from "hono";
2774
2933
  import { z } from "zod";
2775
2934
 
2776
2935
  // src/services/plan-service.ts
2777
- import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
2778
- import { homedir as homedir9 } from "os";
2779
- import { basename, join as join10 } from "path";
2936
+ import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
2937
+ import { homedir as homedir10 } from "os";
2938
+ import { basename, join as join11 } from "path";
2780
2939
  var PLAN_DIRECTORIES = [
2781
- join10(homedir9(), ".claude", "plans"),
2782
- join10(homedir9(), ".replicas", "plans")
2940
+ join11(homedir10(), ".claude", "plans"),
2941
+ join11(homedir10(), ".replicas", "plans")
2783
2942
  ];
2784
2943
  function isMarkdownFile(filename) {
2785
2944
  return filename.toLowerCase().endsWith(".md");
@@ -2813,9 +2972,9 @@ var PlanService = class {
2813
2972
  return null;
2814
2973
  }
2815
2974
  for (const directory of PLAN_DIRECTORIES) {
2816
- const filePath = join10(directory, safeFilename);
2975
+ const filePath = join11(directory, safeFilename);
2817
2976
  try {
2818
- const content = await readFile6(filePath, "utf-8");
2977
+ const content = await readFile7(filePath, "utf-8");
2819
2978
  return { filename: safeFilename, content };
2820
2979
  } catch {
2821
2980
  }
@@ -2828,9 +2987,45 @@ var planService = new PlanService();
2828
2987
  // src/services/warm-hooks-service.ts
2829
2988
  import { execFile } from "child_process";
2830
2989
  import { promisify as promisify2 } from "util";
2831
- import { readFile as readFile7 } from "fs/promises";
2832
- import { join as join11 } from "path";
2990
+ import { readFile as readFile8 } from "fs/promises";
2991
+ import { join as join12 } from "path";
2833
2992
  var execFileAsync = promisify2(execFile);
2993
+ async function installSkill(params) {
2994
+ const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
2995
+ try {
2996
+ const { stdout, stderr } = await execFileAsync(
2997
+ "npx",
2998
+ ["skills", "add", params.source, "--all", "--global"],
2999
+ {
3000
+ cwd: params.cwd,
3001
+ timeout,
3002
+ maxBuffer: 1024 * 1024,
3003
+ env: process.env
3004
+ }
3005
+ );
3006
+ const combined = [`$ npx skills add ${params.source} --all --global`, stdout ?? "", stderr ?? ""].filter(Boolean).join("\n");
3007
+ return {
3008
+ exitCode: 0,
3009
+ output: truncateWarmHookOutput(combined),
3010
+ timedOut: false
3011
+ };
3012
+ } catch (error) {
3013
+ const execError = error;
3014
+ const timedOut = execError.signal === "SIGTERM" || execError.killed === true;
3015
+ const exitCode = typeof execError.code === "number" ? execError.code : 1;
3016
+ const combined = [
3017
+ `$ npx skills add ${params.source} --all --global`,
3018
+ execError.stdout ?? "",
3019
+ execError.stderr ?? "",
3020
+ execError.message
3021
+ ].filter(Boolean).join("\n");
3022
+ return {
3023
+ exitCode,
3024
+ output: truncateWarmHookOutput(combined),
3025
+ timedOut
3026
+ };
3027
+ }
3028
+ }
2834
3029
  async function executeHookScript(params) {
2835
3030
  const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
2836
3031
  try {
@@ -2859,9 +3054,9 @@ async function executeHookScript(params) {
2859
3054
  }
2860
3055
  }
2861
3056
  async function readRepoWarmHook(repoPath) {
2862
- const configPath = join11(repoPath, "replicas.json");
3057
+ const configPath = join12(repoPath, "replicas.json");
2863
3058
  try {
2864
- const raw = await readFile7(configPath, "utf-8");
3059
+ const raw = await readFile8(configPath, "utf-8");
2865
3060
  const parsed = JSON.parse(raw);
2866
3061
  if (!isRecord(parsed) || !("warmHook" in parsed)) {
2867
3062
  return null;
@@ -2895,7 +3090,27 @@ async function collectRepoWarmHooks() {
2895
3090
  }
2896
3091
  async function runWarmHooks(params) {
2897
3092
  const outputBlocks = [];
3093
+ const skills = params.skills ?? [];
2898
3094
  const orgHook = params.organizationWarmHook?.trim();
3095
+ for (const source of skills) {
3096
+ const installResult = await installSkill({
3097
+ source,
3098
+ cwd: gitService.getWorkspaceRoot(),
3099
+ timeoutMs: params.timeoutMs
3100
+ });
3101
+ outputBlocks.push(installResult.output);
3102
+ if (installResult.exitCode === 0) {
3103
+ await environmentDetailsService.track({ skillsInstalled: [source] });
3104
+ }
3105
+ if (installResult.exitCode !== 0) {
3106
+ await environmentDetailsService.setGlobalWarmHook("no", installResult.output);
3107
+ return {
3108
+ exitCode: installResult.exitCode,
3109
+ output: outputBlocks.join("\n\n"),
3110
+ timedOut: installResult.timedOut
3111
+ };
3112
+ }
3113
+ }
2899
3114
  if (orgHook) {
2900
3115
  const orgResult = await executeHookScript({
2901
3116
  label: "org-warm-hook",
@@ -2904,6 +3119,7 @@ async function runWarmHooks(params) {
2904
3119
  timeoutMs: params.timeoutMs
2905
3120
  });
2906
3121
  outputBlocks.push(orgResult.output);
3122
+ await environmentDetailsService.setGlobalWarmHook(orgResult.exitCode === 0 ? "yes" : "no", orgResult.output);
2907
3123
  if (orgResult.exitCode !== 0) {
2908
3124
  return {
2909
3125
  exitCode: orgResult.exitCode,
@@ -2911,9 +3127,18 @@ async function runWarmHooks(params) {
2911
3127
  timedOut: orgResult.timedOut
2912
3128
  };
2913
3129
  }
3130
+ } else {
3131
+ await environmentDetailsService.setGlobalWarmHook("n/a");
2914
3132
  }
2915
3133
  if (params.includeRepoHooks !== false) {
2916
3134
  const repoHooks = await collectRepoWarmHooks();
3135
+ const repoHookNames = new Set(repoHooks.map((repoHook) => repoHook.repoName));
3136
+ const discoveredRepos = await gitService.listRepositories();
3137
+ for (const repo of discoveredRepos) {
3138
+ if (!repoHookNames.has(repo.name)) {
3139
+ await environmentDetailsService.setRepositoryWarmHook(repo.name, "n/a");
3140
+ }
3141
+ }
2917
3142
  for (const repoHook of repoHooks) {
2918
3143
  for (const command of repoHook.commands) {
2919
3144
  const repoResult = await executeHookScript({
@@ -2923,6 +3148,10 @@ async function runWarmHooks(params) {
2923
3148
  timeoutMs: repoHook.timeoutMs
2924
3149
  });
2925
3150
  outputBlocks.push(repoResult.output);
3151
+ await environmentDetailsService.setRepositoryWarmHook(
3152
+ repoHook.repoName,
3153
+ repoResult.exitCode === 0 ? "yes" : "no"
3154
+ );
2926
3155
  if (repoResult.exitCode !== 0) {
2927
3156
  return {
2928
3157
  exitCode: repoResult.exitCode,
@@ -2932,6 +3161,11 @@ async function runWarmHooks(params) {
2932
3161
  }
2933
3162
  }
2934
3163
  }
3164
+ } else {
3165
+ const repos = await gitService.listRepositories();
3166
+ for (const repo of repos) {
3167
+ await environmentDetailsService.setRepositoryWarmHook(repo.name, "n/a");
3168
+ }
2935
3169
  }
2936
3170
  if (outputBlocks.length === 0) {
2937
3171
  return {
@@ -3167,10 +3401,40 @@ function createV1Routes(deps) {
3167
3401
  };
3168
3402
  return c.json(response);
3169
3403
  });
3404
+ app2.get("/version", (c) => {
3405
+ const response = {
3406
+ version: REPLICAS_ENGINE_VERSION
3407
+ };
3408
+ return c.json(response);
3409
+ });
3410
+ app2.get("/environment", async (c) => {
3411
+ try {
3412
+ const details = await environmentDetailsService.getDetails();
3413
+ return c.json(details);
3414
+ } catch (error) {
3415
+ return c.json(
3416
+ jsonError("Failed to load environment details", error instanceof Error ? error.message : "Unknown error"),
3417
+ 500
3418
+ );
3419
+ }
3420
+ });
3421
+ app2.post("/environment/track", async (c) => {
3422
+ try {
3423
+ const body = await c.req.json();
3424
+ const details = await environmentDetailsService.track(body);
3425
+ return c.json(details);
3426
+ } catch (error) {
3427
+ return c.json(
3428
+ jsonError("Failed to track environment details", error instanceof Error ? error.message : "Unknown error"),
3429
+ 500
3430
+ );
3431
+ }
3432
+ });
3170
3433
  app2.post("/warm-hooks/run", async (c) => {
3171
3434
  try {
3172
3435
  const body = await c.req.json();
3173
3436
  const result = await runWarmHooks({
3437
+ skills: Array.isArray(body.skills) ? body.skills : [],
3174
3438
  organizationWarmHook: body.organizationWarmHook,
3175
3439
  includeRepoHooks: body.includeRepoHooks,
3176
3440
  timeoutMs: body.timeoutMs
@@ -3193,6 +3457,7 @@ function createV1Routes(deps) {
3193
3457
  // src/index.ts
3194
3458
  await engineLogger.initialize();
3195
3459
  await eventService.initialize();
3460
+ await environmentDetailsService.initialize();
3196
3461
  var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
3197
3462
  var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
3198
3463
  function checkActiveSSHSessions() {
@@ -3227,7 +3492,7 @@ app.get("/health", async (c) => {
3227
3492
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3228
3493
  }
3229
3494
  try {
3230
- const logContent = await readFile8("/var/log/cloud-init-output.log", "utf-8");
3495
+ const logContent = await readFile9("/var/log/cloud-init-output.log", "utf-8");
3231
3496
  let status;
3232
3497
  if (logContent.includes(COMPLETION_MESSAGE)) {
3233
3498
  status = "active";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",