replicas-engine 0.1.49 → 0.1.51
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.
- package/dist/src/index.js +315 -71
- package/package.json +1 -1
- package/dist/tsup.config.js +0 -28
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
|
|
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/
|
|
833
|
-
import {
|
|
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 {
|
|
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,183 @@ 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
|
+
var CLAUDE_CREDENTIALS_PATH = join5(homedir4(), ".claude", ".credentials.json");
|
|
917
|
+
var CODEX_AUTH_PATH = join5(homedir4(), ".codex", "auth.json");
|
|
918
|
+
function createExecutionItem(status, details) {
|
|
919
|
+
return { status, details: details ?? null };
|
|
920
|
+
}
|
|
921
|
+
function createDefaultDetails() {
|
|
922
|
+
return {
|
|
923
|
+
engineVersion: REPLICAS_ENGINE_VERSION,
|
|
924
|
+
globalWarmHookCompleted: createExecutionItem("n/a"),
|
|
925
|
+
repositories: [],
|
|
926
|
+
filesUploaded: [],
|
|
927
|
+
envVarsSet: [],
|
|
928
|
+
skillsInstalled: [],
|
|
929
|
+
runtimeEnvVarsSet: [],
|
|
930
|
+
repositoriesCloned: [],
|
|
931
|
+
gitIdentityConfigured: false,
|
|
932
|
+
githubCredentialsConfigured: false,
|
|
933
|
+
linearAccessConfigured: false,
|
|
934
|
+
slackAccessConfigured: false,
|
|
935
|
+
githubAccessConfigured: false,
|
|
936
|
+
claudeAuthMethod: "none",
|
|
937
|
+
codexAuthMethod: "none",
|
|
938
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function getClaudeAuthMethod() {
|
|
942
|
+
if (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
|
|
943
|
+
return "oauth";
|
|
944
|
+
}
|
|
945
|
+
if (ENGINE_ENV.CLAUDE_CODE_USE_BEDROCK && ENGINE_ENV.AWS_ACCESS_KEY_ID && ENGINE_ENV.AWS_SECRET_ACCESS_KEY) {
|
|
946
|
+
return "bedrock";
|
|
947
|
+
}
|
|
948
|
+
if (ENGINE_ENV.ANTHROPIC_API_KEY) {
|
|
949
|
+
return "api_key";
|
|
950
|
+
}
|
|
951
|
+
return "none";
|
|
952
|
+
}
|
|
953
|
+
function getCodexAuthMethod() {
|
|
954
|
+
if (existsSync3(CODEX_AUTH_PATH)) {
|
|
955
|
+
return "oauth";
|
|
956
|
+
}
|
|
957
|
+
if (ENGINE_ENV.OPENAI_API_KEY) {
|
|
958
|
+
return "api_key";
|
|
959
|
+
}
|
|
960
|
+
return "none";
|
|
961
|
+
}
|
|
962
|
+
function getLiveDetails(current) {
|
|
963
|
+
const claudeAuthMethod = getClaudeAuthMethod();
|
|
964
|
+
const codexAuthMethod = getCodexAuthMethod();
|
|
965
|
+
return {
|
|
966
|
+
...current,
|
|
967
|
+
engineVersion: REPLICAS_ENGINE_VERSION,
|
|
968
|
+
linearAccessConfigured: Boolean(ENGINE_ENV.LINEAR_SESSION_ID || ENGINE_ENV.LINEAR_ACCESS_TOKEN),
|
|
969
|
+
slackAccessConfigured: Boolean(ENGINE_ENV.SLACK_BOT_TOKEN),
|
|
970
|
+
githubAccessConfigured: Boolean(ENGINE_ENV.GH_TOKEN),
|
|
971
|
+
githubCredentialsConfigured: Boolean(ENGINE_ENV.GH_TOKEN),
|
|
972
|
+
claudeAuthMethod,
|
|
973
|
+
codexAuthMethod
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function mergeUnique(current, incoming) {
|
|
977
|
+
if (!incoming || incoming.length === 0) {
|
|
978
|
+
return current;
|
|
979
|
+
}
|
|
980
|
+
return Array.from(/* @__PURE__ */ new Set([...current, ...incoming]));
|
|
981
|
+
}
|
|
982
|
+
function upsertRepositoryStatus(current, incoming) {
|
|
983
|
+
const byName = new Map(current.map((repository) => [repository.repositoryName, repository]));
|
|
984
|
+
for (const repository of incoming) {
|
|
985
|
+
const existing = byName.get(repository.repositoryName);
|
|
986
|
+
byName.set(repository.repositoryName, {
|
|
987
|
+
repositoryName: repository.repositoryName,
|
|
988
|
+
warmHookCompleted: repository.warmHookCompleted !== "n/a" ? repository.warmHookCompleted : existing?.warmHookCompleted ?? "n/a",
|
|
989
|
+
startHookCompleted: repository.startHookCompleted !== "n/a" ? repository.startHookCompleted : existing?.startHookCompleted ?? "n/a"
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return [...byName.values()].sort((a, b) => a.repositoryName.localeCompare(b.repositoryName));
|
|
993
|
+
}
|
|
994
|
+
var EnvironmentDetailsService = class {
|
|
995
|
+
async initialize() {
|
|
996
|
+
const current = await this.readDetails();
|
|
997
|
+
const repositories = await gitService.listRepositories();
|
|
998
|
+
const merged = upsertRepositoryStatus(
|
|
999
|
+
current.repositories,
|
|
1000
|
+
repositories.map((repository) => ({
|
|
1001
|
+
repositoryName: repository.name,
|
|
1002
|
+
warmHookCompleted: "n/a",
|
|
1003
|
+
startHookCompleted: "n/a"
|
|
1004
|
+
}))
|
|
1005
|
+
);
|
|
1006
|
+
await this.writeDetails({
|
|
1007
|
+
...current,
|
|
1008
|
+
engineVersion: REPLICAS_ENGINE_VERSION,
|
|
1009
|
+
repositories: merged,
|
|
1010
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
async getDetails() {
|
|
1014
|
+
const current = await this.readDetails();
|
|
1015
|
+
return getLiveDetails(current);
|
|
1016
|
+
}
|
|
1017
|
+
async track(update) {
|
|
1018
|
+
const current = await this.readDetails();
|
|
1019
|
+
const next = {
|
|
1020
|
+
...current,
|
|
1021
|
+
engineVersion: REPLICAS_ENGINE_VERSION,
|
|
1022
|
+
globalWarmHookCompleted: update.globalWarmHookCompleted ?? current.globalWarmHookCompleted,
|
|
1023
|
+
repositories: update.repositories ? upsertRepositoryStatus(current.repositories, update.repositories) : current.repositories,
|
|
1024
|
+
filesUploaded: mergeUnique(current.filesUploaded, update.filesUploaded),
|
|
1025
|
+
envVarsSet: mergeUnique(current.envVarsSet, update.envVarsSet),
|
|
1026
|
+
skillsInstalled: mergeUnique(current.skillsInstalled, update.skillsInstalled),
|
|
1027
|
+
runtimeEnvVarsSet: mergeUnique(current.runtimeEnvVarsSet, update.runtimeEnvVarsSet),
|
|
1028
|
+
repositoriesCloned: mergeUnique(current.repositoriesCloned, update.repositoriesCloned),
|
|
1029
|
+
gitIdentityConfigured: update.gitIdentityConfigured ?? current.gitIdentityConfigured,
|
|
1030
|
+
githubCredentialsConfigured: update.githubCredentialsConfigured ?? current.githubCredentialsConfigured,
|
|
1031
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1032
|
+
};
|
|
1033
|
+
await this.writeDetails(next);
|
|
1034
|
+
return next;
|
|
1035
|
+
}
|
|
1036
|
+
async setGlobalWarmHook(status, details) {
|
|
1037
|
+
await this.track({
|
|
1038
|
+
globalWarmHookCompleted: createExecutionItem(status, details)
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
async setRepositoryWarmHook(repositoryName, status) {
|
|
1042
|
+
await this.track({
|
|
1043
|
+
repositories: [{
|
|
1044
|
+
repositoryName,
|
|
1045
|
+
warmHookCompleted: status,
|
|
1046
|
+
startHookCompleted: "n/a"
|
|
1047
|
+
}]
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
async setRepositoryStartHook(repositoryName, status) {
|
|
1051
|
+
await this.track({
|
|
1052
|
+
repositories: [{
|
|
1053
|
+
repositoryName,
|
|
1054
|
+
warmHookCompleted: "n/a",
|
|
1055
|
+
startHookCompleted: status
|
|
1056
|
+
}]
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
async readDetails() {
|
|
1060
|
+
try {
|
|
1061
|
+
if (!existsSync3(ENVIRONMENT_DETAILS_FILE)) {
|
|
1062
|
+
return createDefaultDetails();
|
|
1063
|
+
}
|
|
1064
|
+
const raw = await readFile2(ENVIRONMENT_DETAILS_FILE, "utf-8");
|
|
1065
|
+
return getLiveDetails({
|
|
1066
|
+
...createDefaultDetails(),
|
|
1067
|
+
...JSON.parse(raw)
|
|
1068
|
+
});
|
|
1069
|
+
} catch {
|
|
1070
|
+
return createDefaultDetails();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async writeDetails(details) {
|
|
1074
|
+
await mkdir3(REPLICAS_DIR, { recursive: true });
|
|
1075
|
+
await writeFile3(ENVIRONMENT_DETAILS_FILE, `${JSON.stringify(details, null, 2)}
|
|
1076
|
+
`, "utf-8");
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
var environmentDetailsService = new EnvironmentDetailsService();
|
|
1080
|
+
|
|
912
1081
|
// src/services/replicas-config-service.ts
|
|
1082
|
+
import { readFile as readFile3, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
1083
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1084
|
+
import { join as join6 } from "path";
|
|
1085
|
+
import { homedir as homedir5 } from "os";
|
|
1086
|
+
import { exec } from "child_process";
|
|
1087
|
+
import { promisify } from "util";
|
|
913
1088
|
var execAsync = promisify(exec);
|
|
914
|
-
var START_HOOKS_LOG =
|
|
1089
|
+
var START_HOOKS_LOG = join6(homedir5(), ".replicas", "startHooks.log");
|
|
915
1090
|
var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
|
|
916
1091
|
Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
|
|
917
1092
|
These hooks are currently executing in the background. You can:
|
|
@@ -991,12 +1166,12 @@ var ReplicasConfigService = class {
|
|
|
991
1166
|
const repos = await gitService.listRepositories();
|
|
992
1167
|
const configs = [];
|
|
993
1168
|
for (const repo of repos) {
|
|
994
|
-
const configPath =
|
|
995
|
-
if (!
|
|
1169
|
+
const configPath = join6(repo.path, "replicas.json");
|
|
1170
|
+
if (!existsSync4(configPath)) {
|
|
996
1171
|
continue;
|
|
997
1172
|
}
|
|
998
1173
|
try {
|
|
999
|
-
const data = await
|
|
1174
|
+
const data = await readFile3(configPath, "utf-8");
|
|
1000
1175
|
const config = parseReplicasConfig(JSON.parse(data));
|
|
1001
1176
|
configs.push({
|
|
1002
1177
|
repoName: repo.name,
|
|
@@ -1022,7 +1197,7 @@ var ReplicasConfigService = class {
|
|
|
1022
1197
|
const logLine = `[${timestamp}] ${message}
|
|
1023
1198
|
`;
|
|
1024
1199
|
try {
|
|
1025
|
-
await
|
|
1200
|
+
await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
|
|
1026
1201
|
await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
|
|
1027
1202
|
} catch (error) {
|
|
1028
1203
|
console.error("Failed to write to start hooks log:", error);
|
|
@@ -1037,13 +1212,17 @@ var ReplicasConfigService = class {
|
|
|
1037
1212
|
this.hooksRunning = false;
|
|
1038
1213
|
this.hooksCompleted = true;
|
|
1039
1214
|
this.hooksFailed = false;
|
|
1215
|
+
const repos = await gitService.listRepositories();
|
|
1216
|
+
for (const repo of repos) {
|
|
1217
|
+
await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
|
|
1218
|
+
}
|
|
1040
1219
|
return;
|
|
1041
1220
|
}
|
|
1042
1221
|
this.hooksRunning = true;
|
|
1043
1222
|
this.hooksCompleted = false;
|
|
1044
1223
|
try {
|
|
1045
|
-
await
|
|
1046
|
-
await
|
|
1224
|
+
await mkdir4(join6(homedir5(), ".replicas"), { recursive: true });
|
|
1225
|
+
await writeFile4(
|
|
1047
1226
|
START_HOOKS_LOG,
|
|
1048
1227
|
`=== Start Hooks Execution Log ===
|
|
1049
1228
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
@@ -1052,6 +1231,13 @@ Repositories: ${hookEntries.length}
|
|
|
1052
1231
|
`,
|
|
1053
1232
|
"utf-8"
|
|
1054
1233
|
);
|
|
1234
|
+
const repos = await gitService.listRepositories();
|
|
1235
|
+
const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
|
|
1236
|
+
for (const repo of repos) {
|
|
1237
|
+
if (!reposWithHooks.has(repo.name)) {
|
|
1238
|
+
await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1055
1241
|
for (const entry of hookEntries) {
|
|
1056
1242
|
const startHookConfig = entry.config.startHook;
|
|
1057
1243
|
if (!startHookConfig) {
|
|
@@ -1060,9 +1246,11 @@ Repositories: ${hookEntries.length}
|
|
|
1060
1246
|
const persistedRepoState = await loadRepoState(entry.repoName);
|
|
1061
1247
|
if (persistedRepoState?.startHooksCompleted) {
|
|
1062
1248
|
await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
|
|
1249
|
+
await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
|
|
1063
1250
|
continue;
|
|
1064
1251
|
}
|
|
1065
1252
|
const timeout = startHookConfig.timeout ?? 3e5;
|
|
1253
|
+
let repoFailed = false;
|
|
1066
1254
|
await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
|
|
1067
1255
|
for (const hook of startHookConfig.commands) {
|
|
1068
1256
|
try {
|
|
@@ -1082,7 +1270,9 @@ Repositories: ${hookEntries.length}
|
|
|
1082
1270
|
} catch (error) {
|
|
1083
1271
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1084
1272
|
this.hooksFailed = true;
|
|
1273
|
+
repoFailed = true;
|
|
1085
1274
|
await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
|
|
1275
|
+
await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
|
|
1086
1276
|
}
|
|
1087
1277
|
}
|
|
1088
1278
|
const fallbackRepoState = persistedRepoState ?? {
|
|
@@ -1095,6 +1285,7 @@ Repositories: ${hookEntries.length}
|
|
|
1095
1285
|
startHooksCompleted: false
|
|
1096
1286
|
};
|
|
1097
1287
|
await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
|
|
1288
|
+
await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
|
|
1098
1289
|
}
|
|
1099
1290
|
this.hooksCompleted = true;
|
|
1100
1291
|
await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
|
|
@@ -1152,17 +1343,17 @@ Repositories: ${hookEntries.length}
|
|
|
1152
1343
|
var replicasConfigService = new ReplicasConfigService();
|
|
1153
1344
|
|
|
1154
1345
|
// src/services/event-service.ts
|
|
1155
|
-
import { appendFile as appendFile3, mkdir as
|
|
1156
|
-
import { homedir as
|
|
1157
|
-
import { join as
|
|
1346
|
+
import { appendFile as appendFile3, mkdir as mkdir5 } from "fs/promises";
|
|
1347
|
+
import { homedir as homedir6 } from "os";
|
|
1348
|
+
import { join as join7 } from "path";
|
|
1158
1349
|
import { randomUUID } from "crypto";
|
|
1159
|
-
var ENGINE_DIR =
|
|
1160
|
-
var EVENTS_FILE =
|
|
1350
|
+
var ENGINE_DIR = join7(homedir6(), ".replicas", "engine");
|
|
1351
|
+
var EVENTS_FILE = join7(ENGINE_DIR, "events.jsonl");
|
|
1161
1352
|
var EventService = class {
|
|
1162
1353
|
subscribers = /* @__PURE__ */ new Map();
|
|
1163
1354
|
writeChain = Promise.resolve();
|
|
1164
1355
|
async initialize() {
|
|
1165
|
-
await
|
|
1356
|
+
await mkdir5(ENGINE_DIR, { recursive: true });
|
|
1166
1357
|
}
|
|
1167
1358
|
subscribe(subscriber) {
|
|
1168
1359
|
const id = randomUUID();
|
|
@@ -1192,21 +1383,21 @@ var EventService = class {
|
|
|
1192
1383
|
var eventService = new EventService();
|
|
1193
1384
|
|
|
1194
1385
|
// src/services/chat/chat-service.ts
|
|
1195
|
-
import { mkdir as
|
|
1196
|
-
import { homedir as
|
|
1197
|
-
import { join as
|
|
1386
|
+
import { mkdir as mkdir8, readFile as readFile6, rm, writeFile as writeFile6 } from "fs/promises";
|
|
1387
|
+
import { homedir as homedir9 } from "os";
|
|
1388
|
+
import { join as join10 } from "path";
|
|
1198
1389
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1199
1390
|
|
|
1200
1391
|
// src/managers/claude-manager.ts
|
|
1201
1392
|
import {
|
|
1202
1393
|
query
|
|
1203
1394
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
1204
|
-
import { join as
|
|
1205
|
-
import { mkdir as
|
|
1206
|
-
import { homedir as
|
|
1395
|
+
import { join as join8 } from "path";
|
|
1396
|
+
import { mkdir as mkdir6, appendFile as appendFile4 } from "fs/promises";
|
|
1397
|
+
import { homedir as homedir7 } from "os";
|
|
1207
1398
|
|
|
1208
1399
|
// src/utils/jsonl-reader.ts
|
|
1209
|
-
import { readFile as
|
|
1400
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1210
1401
|
function isJsonlEvent(value) {
|
|
1211
1402
|
if (!isRecord(value)) {
|
|
1212
1403
|
return false;
|
|
@@ -1228,7 +1419,7 @@ function parseJsonlEvents(lines) {
|
|
|
1228
1419
|
}
|
|
1229
1420
|
async function readJSONL(filePath) {
|
|
1230
1421
|
try {
|
|
1231
|
-
const content = await
|
|
1422
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1232
1423
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
1233
1424
|
return parseJsonlEvents(lines);
|
|
1234
1425
|
} catch (error) {
|
|
@@ -1962,7 +2153,7 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
1962
2153
|
pendingInterrupt = false;
|
|
1963
2154
|
constructor(options) {
|
|
1964
2155
|
super(options);
|
|
1965
|
-
this.historyFile = options.historyFilePath ??
|
|
2156
|
+
this.historyFile = options.historyFilePath ?? join8(homedir7(), ".replicas", "claude", "history.jsonl");
|
|
1966
2157
|
this.initializeManager(this.processMessageInternal.bind(this));
|
|
1967
2158
|
}
|
|
1968
2159
|
async interruptActiveTurn() {
|
|
@@ -2100,8 +2291,8 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
2100
2291
|
};
|
|
2101
2292
|
}
|
|
2102
2293
|
async initialize() {
|
|
2103
|
-
const historyDir =
|
|
2104
|
-
await
|
|
2294
|
+
const historyDir = join8(homedir7(), ".replicas", "claude");
|
|
2295
|
+
await mkdir6(historyDir, { recursive: true });
|
|
2105
2296
|
if (this.initialSessionId) {
|
|
2106
2297
|
this.sessionId = this.initialSessionId;
|
|
2107
2298
|
console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
|
|
@@ -2130,13 +2321,13 @@ var ClaudeManager = class extends CodingAgentManager {
|
|
|
2130
2321
|
// src/managers/codex-manager.ts
|
|
2131
2322
|
import { Codex } from "@openai/codex-sdk";
|
|
2132
2323
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2133
|
-
import { readdir as readdir2, stat as stat2, writeFile as
|
|
2134
|
-
import { existsSync as
|
|
2135
|
-
import { join as
|
|
2136
|
-
import { homedir as
|
|
2324
|
+
import { readdir as readdir2, stat as stat2, writeFile as writeFile5, mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
|
|
2325
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2326
|
+
import { join as join9 } from "path";
|
|
2327
|
+
import { homedir as homedir8 } from "os";
|
|
2137
2328
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
2138
2329
|
var DEFAULT_MODEL = "gpt-5.4";
|
|
2139
|
-
var CODEX_CONFIG_PATH =
|
|
2330
|
+
var CODEX_CONFIG_PATH = join9(homedir8(), ".codex", "config.toml");
|
|
2140
2331
|
function isLinearThoughtEvent2(event) {
|
|
2141
2332
|
return event.content.type === "thought";
|
|
2142
2333
|
}
|
|
@@ -2158,7 +2349,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2158
2349
|
constructor(options) {
|
|
2159
2350
|
super(options);
|
|
2160
2351
|
this.codex = new Codex();
|
|
2161
|
-
this.tempImageDir =
|
|
2352
|
+
this.tempImageDir = join9(homedir8(), ".replicas", "codex", "temp-images");
|
|
2162
2353
|
this.initializeManager(this.processMessageInternal.bind(this));
|
|
2163
2354
|
}
|
|
2164
2355
|
async initialize() {
|
|
@@ -2178,12 +2369,12 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2178
2369
|
*/
|
|
2179
2370
|
async updateCodexConfig(developerInstructions) {
|
|
2180
2371
|
try {
|
|
2181
|
-
const codexDir =
|
|
2182
|
-
await
|
|
2372
|
+
const codexDir = join9(homedir8(), ".codex");
|
|
2373
|
+
await mkdir7(codexDir, { recursive: true });
|
|
2183
2374
|
let config = {};
|
|
2184
|
-
if (
|
|
2375
|
+
if (existsSync5(CODEX_CONFIG_PATH)) {
|
|
2185
2376
|
try {
|
|
2186
|
-
const existingContent = await
|
|
2377
|
+
const existingContent = await readFile5(CODEX_CONFIG_PATH, "utf-8");
|
|
2187
2378
|
const parsed = parseToml(existingContent);
|
|
2188
2379
|
if (isRecord(parsed)) {
|
|
2189
2380
|
config = parsed;
|
|
@@ -2198,7 +2389,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2198
2389
|
delete config.developer_instructions;
|
|
2199
2390
|
}
|
|
2200
2391
|
const tomlContent = stringifyToml(config);
|
|
2201
|
-
await
|
|
2392
|
+
await writeFile5(CODEX_CONFIG_PATH, tomlContent, "utf-8");
|
|
2202
2393
|
console.log("[CodexManager] Updated config.toml with developer_instructions");
|
|
2203
2394
|
} catch (error) {
|
|
2204
2395
|
console.error("[CodexManager] Failed to update config.toml:", error);
|
|
@@ -2209,14 +2400,14 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2209
2400
|
* @returns Array of temp file paths
|
|
2210
2401
|
*/
|
|
2211
2402
|
async saveImagesToTempFiles(images) {
|
|
2212
|
-
await
|
|
2403
|
+
await mkdir7(this.tempImageDir, { recursive: true });
|
|
2213
2404
|
const tempPaths = [];
|
|
2214
2405
|
for (const image of images) {
|
|
2215
2406
|
const ext = image.source.media_type.split("/")[1] || "png";
|
|
2216
2407
|
const filename = `img_${randomUUID2()}.${ext}`;
|
|
2217
|
-
const filepath =
|
|
2408
|
+
const filepath = join9(this.tempImageDir, filename);
|
|
2218
2409
|
const buffer = Buffer.from(image.source.data, "base64");
|
|
2219
|
-
await
|
|
2410
|
+
await writeFile5(filepath, buffer);
|
|
2220
2411
|
tempPaths.push(filepath);
|
|
2221
2412
|
}
|
|
2222
2413
|
return tempPaths;
|
|
@@ -2346,13 +2537,13 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2346
2537
|
}
|
|
2347
2538
|
// Helper methods for finding session files
|
|
2348
2539
|
async findSessionFile(threadId) {
|
|
2349
|
-
const sessionsDir =
|
|
2540
|
+
const sessionsDir = join9(homedir8(), ".codex", "sessions");
|
|
2350
2541
|
try {
|
|
2351
2542
|
const now = /* @__PURE__ */ new Date();
|
|
2352
2543
|
const year = now.getFullYear();
|
|
2353
2544
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2354
2545
|
const day = String(now.getDate()).padStart(2, "0");
|
|
2355
|
-
const todayDir =
|
|
2546
|
+
const todayDir = join9(sessionsDir, String(year), month, day);
|
|
2356
2547
|
const file = await this.findFileInDirectory(todayDir, threadId);
|
|
2357
2548
|
if (file) return file;
|
|
2358
2549
|
for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
|
|
@@ -2361,7 +2552,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2361
2552
|
const searchYear = date.getFullYear();
|
|
2362
2553
|
const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
|
|
2363
2554
|
const searchDay = String(date.getDate()).padStart(2, "0");
|
|
2364
|
-
const searchDir =
|
|
2555
|
+
const searchDir = join9(sessionsDir, String(searchYear), searchMonth, searchDay);
|
|
2365
2556
|
const file2 = await this.findFileInDirectory(searchDir, threadId);
|
|
2366
2557
|
if (file2) return file2;
|
|
2367
2558
|
}
|
|
@@ -2375,7 +2566,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2375
2566
|
const files = await readdir2(directory);
|
|
2376
2567
|
for (const file of files) {
|
|
2377
2568
|
if (file.endsWith(".jsonl") && file.includes(threadId)) {
|
|
2378
|
-
const fullPath =
|
|
2569
|
+
const fullPath = join9(directory, file);
|
|
2379
2570
|
const stats = await stat2(fullPath);
|
|
2380
2571
|
if (stats.isFile()) {
|
|
2381
2572
|
return fullPath;
|
|
@@ -2408,7 +2599,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2408
2599
|
const seenLines = /* @__PURE__ */ new Set();
|
|
2409
2600
|
const seedSeenLines = async () => {
|
|
2410
2601
|
try {
|
|
2411
|
-
const content = await
|
|
2602
|
+
const content = await readFile5(sessionFile, "utf-8");
|
|
2412
2603
|
const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2413
2604
|
for (const line of lines) {
|
|
2414
2605
|
seenLines.add(line);
|
|
@@ -2420,7 +2611,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
2420
2611
|
const pump = async () => {
|
|
2421
2612
|
let emitted = 0;
|
|
2422
2613
|
try {
|
|
2423
|
-
const content = await
|
|
2614
|
+
const content = await readFile5(sessionFile, "utf-8");
|
|
2424
2615
|
const lines = content.split("\n");
|
|
2425
2616
|
const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
|
|
2426
2617
|
for (const line of completeLines) {
|
|
@@ -2491,9 +2682,9 @@ var DuplicateDefaultChatError = class extends Error {
|
|
|
2491
2682
|
};
|
|
2492
2683
|
|
|
2493
2684
|
// src/services/chat/chat-service.ts
|
|
2494
|
-
var ENGINE_DIR2 =
|
|
2495
|
-
var CHATS_FILE =
|
|
2496
|
-
var CLAUDE_HISTORY_DIR =
|
|
2685
|
+
var ENGINE_DIR2 = join10(homedir9(), ".replicas", "engine");
|
|
2686
|
+
var CHATS_FILE = join10(ENGINE_DIR2, "chats.json");
|
|
2687
|
+
var CLAUDE_HISTORY_DIR = join10(ENGINE_DIR2, "claude-histories");
|
|
2497
2688
|
function isPersistedChat(value) {
|
|
2498
2689
|
if (!isRecord(value)) {
|
|
2499
2690
|
return false;
|
|
@@ -2508,8 +2699,8 @@ var ChatService = class {
|
|
|
2508
2699
|
chats = /* @__PURE__ */ new Map();
|
|
2509
2700
|
writeChain = Promise.resolve();
|
|
2510
2701
|
async initialize() {
|
|
2511
|
-
await
|
|
2512
|
-
await
|
|
2702
|
+
await mkdir8(ENGINE_DIR2, { recursive: true });
|
|
2703
|
+
await mkdir8(CLAUDE_HISTORY_DIR, { recursive: true });
|
|
2513
2704
|
const persisted = await this.loadChats();
|
|
2514
2705
|
for (const chat of persisted) {
|
|
2515
2706
|
const runtime = this.createRuntimeChat(chat);
|
|
@@ -2593,7 +2784,7 @@ var ChatService = class {
|
|
|
2593
2784
|
this.chats.delete(chatId);
|
|
2594
2785
|
await this.persistAllChats();
|
|
2595
2786
|
if (chat.persisted.provider === "claude") {
|
|
2596
|
-
const historyFilePath =
|
|
2787
|
+
const historyFilePath = join10(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
|
|
2597
2788
|
await rm(historyFilePath, { force: true });
|
|
2598
2789
|
}
|
|
2599
2790
|
await this.publish({
|
|
@@ -2636,7 +2827,7 @@ var ChatService = class {
|
|
|
2636
2827
|
};
|
|
2637
2828
|
const provider = persisted.provider === "claude" ? new ClaudeManager({
|
|
2638
2829
|
workingDirectory: this.workingDirectory,
|
|
2639
|
-
historyFilePath:
|
|
2830
|
+
historyFilePath: join10(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
|
|
2640
2831
|
initialSessionId: persisted.providerSessionId,
|
|
2641
2832
|
onSaveSessionId: saveSession,
|
|
2642
2833
|
onTurnComplete: onProviderTurnComplete,
|
|
@@ -2732,7 +2923,7 @@ var ChatService = class {
|
|
|
2732
2923
|
}
|
|
2733
2924
|
async loadChats() {
|
|
2734
2925
|
try {
|
|
2735
|
-
const content = await
|
|
2926
|
+
const content = await readFile6(CHATS_FILE, "utf-8");
|
|
2736
2927
|
const parsed = JSON.parse(content);
|
|
2737
2928
|
if (!Array.isArray(parsed)) {
|
|
2738
2929
|
return [];
|
|
@@ -2749,7 +2940,7 @@ var ChatService = class {
|
|
|
2749
2940
|
this.writeChain = this.writeChain.catch(() => {
|
|
2750
2941
|
}).then(async () => {
|
|
2751
2942
|
const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
|
|
2752
|
-
await
|
|
2943
|
+
await writeFile6(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
|
|
2753
2944
|
});
|
|
2754
2945
|
await this.writeChain;
|
|
2755
2946
|
}
|
|
@@ -2774,12 +2965,12 @@ import { Hono } from "hono";
|
|
|
2774
2965
|
import { z } from "zod";
|
|
2775
2966
|
|
|
2776
2967
|
// src/services/plan-service.ts
|
|
2777
|
-
import { readdir as readdir3, readFile as
|
|
2778
|
-
import { homedir as
|
|
2779
|
-
import { basename, join as
|
|
2968
|
+
import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
|
|
2969
|
+
import { homedir as homedir10 } from "os";
|
|
2970
|
+
import { basename, join as join11 } from "path";
|
|
2780
2971
|
var PLAN_DIRECTORIES = [
|
|
2781
|
-
|
|
2782
|
-
|
|
2972
|
+
join11(homedir10(), ".claude", "plans"),
|
|
2973
|
+
join11(homedir10(), ".replicas", "plans")
|
|
2783
2974
|
];
|
|
2784
2975
|
function isMarkdownFile(filename) {
|
|
2785
2976
|
return filename.toLowerCase().endsWith(".md");
|
|
@@ -2813,9 +3004,9 @@ var PlanService = class {
|
|
|
2813
3004
|
return null;
|
|
2814
3005
|
}
|
|
2815
3006
|
for (const directory of PLAN_DIRECTORIES) {
|
|
2816
|
-
const filePath =
|
|
3007
|
+
const filePath = join11(directory, safeFilename);
|
|
2817
3008
|
try {
|
|
2818
|
-
const content = await
|
|
3009
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2819
3010
|
return { filename: safeFilename, content };
|
|
2820
3011
|
} catch {
|
|
2821
3012
|
}
|
|
@@ -2828,8 +3019,8 @@ var planService = new PlanService();
|
|
|
2828
3019
|
// src/services/warm-hooks-service.ts
|
|
2829
3020
|
import { execFile } from "child_process";
|
|
2830
3021
|
import { promisify as promisify2 } from "util";
|
|
2831
|
-
import { readFile as
|
|
2832
|
-
import { join as
|
|
3022
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3023
|
+
import { join as join12 } from "path";
|
|
2833
3024
|
var execFileAsync = promisify2(execFile);
|
|
2834
3025
|
async function installSkill(params) {
|
|
2835
3026
|
const timeout = clampWarmHookTimeoutMs(params.timeoutMs);
|
|
@@ -2895,9 +3086,9 @@ async function executeHookScript(params) {
|
|
|
2895
3086
|
}
|
|
2896
3087
|
}
|
|
2897
3088
|
async function readRepoWarmHook(repoPath) {
|
|
2898
|
-
const configPath =
|
|
3089
|
+
const configPath = join12(repoPath, "replicas.json");
|
|
2899
3090
|
try {
|
|
2900
|
-
const raw = await
|
|
3091
|
+
const raw = await readFile8(configPath, "utf-8");
|
|
2901
3092
|
const parsed = JSON.parse(raw);
|
|
2902
3093
|
if (!isRecord(parsed) || !("warmHook" in parsed)) {
|
|
2903
3094
|
return null;
|
|
@@ -2940,7 +3131,11 @@ async function runWarmHooks(params) {
|
|
|
2940
3131
|
timeoutMs: params.timeoutMs
|
|
2941
3132
|
});
|
|
2942
3133
|
outputBlocks.push(installResult.output);
|
|
3134
|
+
if (installResult.exitCode === 0) {
|
|
3135
|
+
await environmentDetailsService.track({ skillsInstalled: [source] });
|
|
3136
|
+
}
|
|
2943
3137
|
if (installResult.exitCode !== 0) {
|
|
3138
|
+
await environmentDetailsService.setGlobalWarmHook("no", installResult.output);
|
|
2944
3139
|
return {
|
|
2945
3140
|
exitCode: installResult.exitCode,
|
|
2946
3141
|
output: outputBlocks.join("\n\n"),
|
|
@@ -2956,6 +3151,7 @@ async function runWarmHooks(params) {
|
|
|
2956
3151
|
timeoutMs: params.timeoutMs
|
|
2957
3152
|
});
|
|
2958
3153
|
outputBlocks.push(orgResult.output);
|
|
3154
|
+
await environmentDetailsService.setGlobalWarmHook(orgResult.exitCode === 0 ? "yes" : "no", orgResult.output);
|
|
2959
3155
|
if (orgResult.exitCode !== 0) {
|
|
2960
3156
|
return {
|
|
2961
3157
|
exitCode: orgResult.exitCode,
|
|
@@ -2963,9 +3159,18 @@ async function runWarmHooks(params) {
|
|
|
2963
3159
|
timedOut: orgResult.timedOut
|
|
2964
3160
|
};
|
|
2965
3161
|
}
|
|
3162
|
+
} else {
|
|
3163
|
+
await environmentDetailsService.setGlobalWarmHook("n/a");
|
|
2966
3164
|
}
|
|
2967
3165
|
if (params.includeRepoHooks !== false) {
|
|
2968
3166
|
const repoHooks = await collectRepoWarmHooks();
|
|
3167
|
+
const repoHookNames = new Set(repoHooks.map((repoHook) => repoHook.repoName));
|
|
3168
|
+
const discoveredRepos = await gitService.listRepositories();
|
|
3169
|
+
for (const repo of discoveredRepos) {
|
|
3170
|
+
if (!repoHookNames.has(repo.name)) {
|
|
3171
|
+
await environmentDetailsService.setRepositoryWarmHook(repo.name, "n/a");
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
2969
3174
|
for (const repoHook of repoHooks) {
|
|
2970
3175
|
for (const command of repoHook.commands) {
|
|
2971
3176
|
const repoResult = await executeHookScript({
|
|
@@ -2975,6 +3180,10 @@ async function runWarmHooks(params) {
|
|
|
2975
3180
|
timeoutMs: repoHook.timeoutMs
|
|
2976
3181
|
});
|
|
2977
3182
|
outputBlocks.push(repoResult.output);
|
|
3183
|
+
await environmentDetailsService.setRepositoryWarmHook(
|
|
3184
|
+
repoHook.repoName,
|
|
3185
|
+
repoResult.exitCode === 0 ? "yes" : "no"
|
|
3186
|
+
);
|
|
2978
3187
|
if (repoResult.exitCode !== 0) {
|
|
2979
3188
|
return {
|
|
2980
3189
|
exitCode: repoResult.exitCode,
|
|
@@ -2984,6 +3193,11 @@ async function runWarmHooks(params) {
|
|
|
2984
3193
|
}
|
|
2985
3194
|
}
|
|
2986
3195
|
}
|
|
3196
|
+
} else {
|
|
3197
|
+
const repos = await gitService.listRepositories();
|
|
3198
|
+
for (const repo of repos) {
|
|
3199
|
+
await environmentDetailsService.setRepositoryWarmHook(repo.name, "n/a");
|
|
3200
|
+
}
|
|
2987
3201
|
}
|
|
2988
3202
|
if (outputBlocks.length === 0) {
|
|
2989
3203
|
return {
|
|
@@ -3219,6 +3433,35 @@ function createV1Routes(deps) {
|
|
|
3219
3433
|
};
|
|
3220
3434
|
return c.json(response);
|
|
3221
3435
|
});
|
|
3436
|
+
app2.get("/version", (c) => {
|
|
3437
|
+
const response = {
|
|
3438
|
+
version: REPLICAS_ENGINE_VERSION
|
|
3439
|
+
};
|
|
3440
|
+
return c.json(response);
|
|
3441
|
+
});
|
|
3442
|
+
app2.get("/environment", async (c) => {
|
|
3443
|
+
try {
|
|
3444
|
+
const details = await environmentDetailsService.getDetails();
|
|
3445
|
+
return c.json(details);
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
return c.json(
|
|
3448
|
+
jsonError("Failed to load environment details", error instanceof Error ? error.message : "Unknown error"),
|
|
3449
|
+
500
|
|
3450
|
+
);
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
app2.post("/environment/track", async (c) => {
|
|
3454
|
+
try {
|
|
3455
|
+
const body = await c.req.json();
|
|
3456
|
+
const details = await environmentDetailsService.track(body);
|
|
3457
|
+
return c.json(details);
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
return c.json(
|
|
3460
|
+
jsonError("Failed to track environment details", error instanceof Error ? error.message : "Unknown error"),
|
|
3461
|
+
500
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
});
|
|
3222
3465
|
app2.post("/warm-hooks/run", async (c) => {
|
|
3223
3466
|
try {
|
|
3224
3467
|
const body = await c.req.json();
|
|
@@ -3246,6 +3489,7 @@ function createV1Routes(deps) {
|
|
|
3246
3489
|
// src/index.ts
|
|
3247
3490
|
await engineLogger.initialize();
|
|
3248
3491
|
await eventService.initialize();
|
|
3492
|
+
await environmentDetailsService.initialize();
|
|
3249
3493
|
var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
|
|
3250
3494
|
var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
|
|
3251
3495
|
function checkActiveSSHSessions() {
|
|
@@ -3280,7 +3524,7 @@ app.get("/health", async (c) => {
|
|
|
3280
3524
|
return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
|
|
3281
3525
|
}
|
|
3282
3526
|
try {
|
|
3283
|
-
const logContent = await
|
|
3527
|
+
const logContent = await readFile9("/var/log/cloud-init-output.log", "utf-8");
|
|
3284
3528
|
let status;
|
|
3285
3529
|
if (logContent.includes(COMPLETION_MESSAGE)) {
|
|
3286
3530
|
status = "active";
|
package/package.json
CHANGED
package/dist/tsup.config.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'tsup';
|
|
2
|
-
export default defineConfig({
|
|
3
|
-
entry: ['src/index.ts'],
|
|
4
|
-
format: ['esm'],
|
|
5
|
-
bundle: true,
|
|
6
|
-
// Bundle @replicas/shared inline, all other dependencies are automatically external
|
|
7
|
-
noExternal: ['@replicas/shared'],
|
|
8
|
-
dts: false, // We don't need type definitions for the published package
|
|
9
|
-
clean: true,
|
|
10
|
-
// Output to dist/src to match existing structure
|
|
11
|
-
outDir: 'dist/src',
|
|
12
|
-
// Add shebang for the bin script
|
|
13
|
-
banner: {
|
|
14
|
-
js: '#!/usr/bin/env node',
|
|
15
|
-
},
|
|
16
|
-
// Preserve the directory structure
|
|
17
|
-
outExtension() {
|
|
18
|
-
return {
|
|
19
|
-
js: '.js',
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
// Resolve path mappings from tsconfig
|
|
23
|
-
esbuildOptions(options) {
|
|
24
|
-
options.alias = {
|
|
25
|
-
'@replicas/shared': '../shared/src',
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
});
|