replicas-engine 0.1.309 → 0.1.313

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 +322 -169
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -14,21 +14,6 @@ function isRecord(value) {
14
14
  return typeof value === "object" && value !== null && !Array.isArray(value);
15
15
  }
16
16
 
17
- // ../shared/src/result.ts
18
- function createSuccessResult(data) {
19
- return { ok: true, data };
20
- }
21
- function createErrorResult(error) {
22
- return {
23
- ok: false,
24
- error: {
25
- message: error.message,
26
- code: error.code,
27
- details: error.details
28
- }
29
- };
30
- }
31
-
32
17
  // ../shared/src/agent.ts
33
18
  var CODEX_REASONING_EFFORT_BY_THINKING_LEVEL = {
34
19
  low: "low",
@@ -132,9 +117,9 @@ var EXT_TO_LANGUAGE = {
132
117
  function detectLanguageByPath(filePath) {
133
118
  const dot = filePath.lastIndexOf(".");
134
119
  if (dot === -1) {
135
- const basename3 = filePath.split("/").pop() ?? "";
136
- if (basename3 === "Dockerfile") return "dockerfile";
137
- if (basename3 === "Makefile") return "makefile";
120
+ const basename2 = filePath.split("/").pop() ?? "";
121
+ if (basename2 === "Dockerfile") return "dockerfile";
122
+ if (basename2 === "Makefile") return "makefile";
138
123
  return null;
139
124
  }
140
125
  const ext = filePath.slice(dot).toLowerCase();
@@ -302,7 +287,7 @@ var WORKSPACE_SIZES = ["small", "large"];
302
287
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
303
288
 
304
289
  // ../shared/src/e2b.ts
305
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-13-v5";
290
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-13-v9";
306
291
 
307
292
  // ../shared/src/runtime-env.ts
308
293
  function parsePosixEnvFile(content) {
@@ -2091,6 +2076,35 @@ var MODEL_LABELS = {
2091
2076
  "gpt-5.2": "GPT-5.2"
2092
2077
  };
2093
2078
  var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
2079
+ var MAX_CANVAS_FILE_BYTES = 5 * 1024 * 1024;
2080
+ function isCanvasTextKind(kind) {
2081
+ return kind === "markdown" || kind === "html";
2082
+ }
2083
+ function sanitizeCanvasFilename(filename) {
2084
+ const safe = filename.split(/[\\/]/).pop() ?? "";
2085
+ if (!safe || safe !== filename || safe.startsWith(".")) return null;
2086
+ return safe;
2087
+ }
2088
+ function bytesToBase64(bytes) {
2089
+ let binary = "";
2090
+ const chunkSize = 32768;
2091
+ for (let i = 0; i < bytes.length; i += chunkSize) {
2092
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
2093
+ }
2094
+ return btoa(binary);
2095
+ }
2096
+ function serializeCanvasContentResponse(input) {
2097
+ const base = {
2098
+ filename: input.filename,
2099
+ kind: input.kind,
2100
+ sizeBytes: input.sizeBytes,
2101
+ mimeType: input.mimeType
2102
+ };
2103
+ if (isCanvasTextKind(input.kind)) {
2104
+ return { ...base, content: new TextDecoder().decode(input.bytes) };
2105
+ }
2106
+ return { ...base, base64: bytesToBase64(input.bytes) };
2107
+ }
2094
2108
  var CANVAS_KIND_BY_EXTENSION = {
2095
2109
  ".md": { kind: "markdown", mimeType: "text/markdown; charset=utf-8" },
2096
2110
  ".markdown": { kind: "markdown", mimeType: "text/markdown; charset=utf-8" },
@@ -2925,6 +2939,32 @@ function upsertCredentialFileLines(filePath, hosts, lines) {
2925
2939
  credentialFileQueue = task.catch(() => void 0);
2926
2940
  return task;
2927
2941
  }
2942
+ function removeCredentialFileLines(filePath, shouldRemove) {
2943
+ const task = credentialFileQueue.then(async () => {
2944
+ let existing = [];
2945
+ try {
2946
+ existing = (await readFile(filePath, "utf-8")).split("\n").filter(Boolean);
2947
+ } catch {
2948
+ return;
2949
+ }
2950
+ const kept = existing.filter((line) => !shouldRemove(line));
2951
+ await writeSecureCredentialFile(filePath, [...kept, ""].join("\n"));
2952
+ });
2953
+ credentialFileQueue = task.catch(() => void 0);
2954
+ return task;
2955
+ }
2956
+
2957
+ // src/utils/git-identity.ts
2958
+ async function updateGitIdentity(identity, tag) {
2959
+ try {
2960
+ const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity);
2961
+ await execFileAsync("git", nameArgs);
2962
+ await execFileAsync("git", emailArgs);
2963
+ console.log(`[${tag}] Updated git identity to ${identity.name} <${identity.email}>`);
2964
+ } catch (error) {
2965
+ console.error(`[${tag}] Failed to update git identity:`, error);
2966
+ }
2967
+ }
2928
2968
 
2929
2969
  // src/managers/github-token-manager.ts
2930
2970
  var GitHubTokenManager = class extends BaseRefreshManager {
@@ -2935,6 +2975,9 @@ var GitHubTokenManager = class extends BaseRefreshManager {
2935
2975
  console.log("[GitHubTokenManager] Refreshing GitHub token...");
2936
2976
  const response = await monolithRequest("/v1/engine/github/refresh-token");
2937
2977
  if (!response.ok) {
2978
+ if (response.status === 403) {
2979
+ await this.clearGitHubCredentials();
2980
+ }
2938
2981
  const errorText = await response.text();
2939
2982
  throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
2940
2983
  }
@@ -2944,7 +2987,7 @@ var GitHubTokenManager = class extends BaseRefreshManager {
2944
2987
  await this.updateGitCredentials(ghToken);
2945
2988
  await this.updateGhHostsFile(ghToken, ghUsername);
2946
2989
  if (data.gitIdentity) {
2947
- await this.updateGitIdentity(data.gitIdentity);
2990
+ await updateGitIdentity(data.gitIdentity, "GitHubTokenManager");
2948
2991
  }
2949
2992
  if (data.userToken) {
2950
2993
  console.log(`[GitHubTokenManager] Token refreshed with user token for PR attribution, installation token expires at ${data.expiresAt}, user token expires at ${data.userToken.expiresAt}`);
@@ -2977,15 +3020,12 @@ var GitHubTokenManager = class extends BaseRefreshManager {
2977
3020
  console.error("[GitHubTokenManager] Failed to update gh hosts file:", error);
2978
3021
  }
2979
3022
  }
2980
- async updateGitIdentity(identity) {
2981
- try {
2982
- const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity);
2983
- await execFileAsync("git", nameArgs);
2984
- await execFileAsync("git", emailArgs);
2985
- console.log(`[GitHubTokenManager] Updated git identity to ${identity.name} <${identity.email}>`);
2986
- } catch (error) {
2987
- console.error("[GitHubTokenManager] Failed to update git identity:", error);
2988
- }
3023
+ async clearGitHubCredentials() {
3024
+ const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
3025
+ await removeCredentialFileLines(
3026
+ credentialsPath,
3027
+ (line) => line.endsWith("@github.com")
3028
+ );
2989
3029
  }
2990
3030
  };
2991
3031
  var githubTokenManager = new GitHubTokenManager();
@@ -2997,12 +3037,16 @@ var GitLabTokenManager = class extends BaseRefreshManager {
2997
3037
  super("GitLabTokenManager");
2998
3038
  }
2999
3039
  async doRefresh(_config) {
3000
- const result = await this.fetchRefresh();
3001
- if (!result.ok) {
3002
- throw new Error(`Token refresh failed: ${result.error.message}`);
3040
+ const response = await monolithRequest("/v1/engine/gitlab/refresh-token");
3041
+ if (!response.ok) {
3042
+ if (response.status === 403) {
3043
+ await this.clearGitLabCredentials();
3044
+ }
3045
+ throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
3003
3046
  }
3004
- const data = result.data;
3047
+ const data = await response.json();
3005
3048
  if (!data.token) {
3049
+ await this.clearGitLabCredentials(data.hosts);
3006
3050
  console.log(`[GitLabTokenManager] No GitLab token to install: ${data.reason}`);
3007
3051
  return;
3008
3052
  }
@@ -3015,25 +3059,15 @@ var GitLabTokenManager = class extends BaseRefreshManager {
3015
3059
  );
3016
3060
  console.log(`[GitLabTokenManager] Updated ${credentialsPath} for ${hosts.join(", ")}`);
3017
3061
  if (data.gitIdentity) {
3018
- await this.updateGitIdentity(data.gitIdentity);
3019
- }
3020
- }
3021
- async fetchRefresh() {
3022
- const response = await monolithRequest("/v1/engine/gitlab/refresh-token");
3023
- if (!response.ok) {
3024
- return createErrorResult({ message: `${response.status} ${await response.text()}` });
3062
+ await updateGitIdentity(data.gitIdentity, "GitLabTokenManager");
3025
3063
  }
3026
- return createSuccessResult(await response.json());
3027
3064
  }
3028
- async updateGitIdentity(identity) {
3029
- try {
3030
- const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity);
3031
- await execFileAsync("git", nameArgs);
3032
- await execFileAsync("git", emailArgs);
3033
- console.log(`[GitLabTokenManager] Updated git identity to ${identity.name} <${identity.email}>`);
3034
- } catch (error) {
3035
- console.error("[GitLabTokenManager] Failed to update git identity:", error);
3036
- }
3065
+ async clearGitLabCredentials(hosts = []) {
3066
+ const credentialsPath = path2.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
3067
+ await removeCredentialFileLines(
3068
+ credentialsPath,
3069
+ (line) => line.startsWith("https://oauth2:") && (hosts.length === 0 || hosts.some((host) => line.endsWith(`@${host}`)))
3070
+ );
3037
3071
  }
3038
3072
  };
3039
3073
  var gitlabTokenManager = new GitLabTokenManager();
@@ -4899,9 +4933,9 @@ async function registerDesktopPreview() {
4899
4933
 
4900
4934
  // src/services/chat/chat-service.ts
4901
4935
  import { existsSync as existsSync7 } from "fs";
4902
- import { appendFile as appendFile4, copyFile, mkdir as mkdir11, readFile as readFile11, rename as rename2, rm } from "fs/promises";
4903
- import { homedir as homedir13 } from "os";
4904
- import { join as join15 } from "path";
4936
+ import { appendFile as appendFile4, copyFile, mkdir as mkdir11, readFile as readFile12, rename as rename2, rm } from "fs/promises";
4937
+ import { homedir as homedir14 } from "os";
4938
+ import { join as join16 } from "path";
4905
4939
  import { randomUUID as randomUUID5 } from "crypto";
4906
4940
 
4907
4941
  // src/managers/claude-manager.ts
@@ -6846,7 +6880,7 @@ var AspClient = class {
6846
6880
  // src/managers/codex-asp/app-server-process.ts
6847
6881
  var DEFAULT_CODEX_BINARY = "codex";
6848
6882
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
6849
- var ENGINE_PACKAGE_VERSION = "0.1.309";
6883
+ var ENGINE_PACKAGE_VERSION = "0.1.313";
6850
6884
  var INITIALIZE_METHOD = "initialize";
6851
6885
  var INITIALIZED_NOTIFICATION = "initialized";
6852
6886
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -9149,15 +9183,190 @@ var KeepAliveService = class _KeepAliveService {
9149
9183
  };
9150
9184
  var keepAliveService = new KeepAliveService();
9151
9185
 
9152
- // src/services/upload-chat-transcripts.ts
9153
- import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
9154
- import { basename, join as join14 } from "path";
9186
+ // src/services/canvas-service.ts
9187
+ import { readdir as readdir3, readFile as readFile10, stat as stat2 } from "fs/promises";
9155
9188
  import { homedir as homedir12 } from "os";
9156
- var ENGINE_DIR2 = join14(homedir12(), ".replicas", "engine");
9189
+ import { join as join14 } from "path";
9190
+ var CANVAS_DIRECTORIES = [
9191
+ join14(homedir12(), ".claude", "plans"),
9192
+ join14(homedir12(), ".replicas", "canvas")
9193
+ ];
9194
+ var CanvasService = class {
9195
+ async listItems() {
9196
+ const items = /* @__PURE__ */ new Map();
9197
+ for (const directory of CANVAS_DIRECTORIES) {
9198
+ let entries;
9199
+ try {
9200
+ entries = await readdir3(directory, { withFileTypes: true });
9201
+ } catch {
9202
+ continue;
9203
+ }
9204
+ for (const entry of entries) {
9205
+ if (!entry.isFile()) continue;
9206
+ if (entry.name.startsWith(".")) continue;
9207
+ if (items.has(entry.name)) continue;
9208
+ const { kind } = classifyCanvasFilename(entry.name);
9209
+ let sizeBytes = 0;
9210
+ try {
9211
+ const s = await stat2(join14(directory, entry.name));
9212
+ sizeBytes = s.size;
9213
+ } catch {
9214
+ continue;
9215
+ }
9216
+ items.set(entry.name, { filename: entry.name, kind, sizeBytes });
9217
+ }
9218
+ }
9219
+ return Array.from(items.values()).sort((a, b) => a.filename.localeCompare(b.filename));
9220
+ }
9221
+ async getItemFile(filename) {
9222
+ const safe = sanitizeCanvasFilename(filename);
9223
+ if (!safe) return null;
9224
+ const { kind, mimeType } = classifyCanvasFilename(safe);
9225
+ for (const directory of CANVAS_DIRECTORIES) {
9226
+ const filePath = join14(directory, safe);
9227
+ let sizeBytes = 0;
9228
+ let updatedAt = "";
9229
+ try {
9230
+ const s = await stat2(filePath);
9231
+ sizeBytes = s.size;
9232
+ updatedAt = s.mtime.toISOString();
9233
+ } catch {
9234
+ continue;
9235
+ }
9236
+ if (sizeBytes > MAX_CANVAS_FILE_BYTES) {
9237
+ return {
9238
+ filename: safe,
9239
+ kind,
9240
+ sizeBytes,
9241
+ mimeType,
9242
+ updatedAt,
9243
+ tooLarge: true
9244
+ };
9245
+ }
9246
+ try {
9247
+ const bytes = await readFile10(filePath);
9248
+ return { filename: safe, kind, sizeBytes, mimeType, updatedAt, bytes };
9249
+ } catch {
9250
+ continue;
9251
+ }
9252
+ }
9253
+ return null;
9254
+ }
9255
+ async getItem(filename) {
9256
+ const item = await this.getItemFile(filename);
9257
+ if (!item) return null;
9258
+ if (item.tooLarge || !item.bytes) {
9259
+ return {
9260
+ filename: item.filename,
9261
+ kind: item.kind,
9262
+ sizeBytes: item.sizeBytes,
9263
+ mimeType: item.mimeType,
9264
+ tooLarge: true
9265
+ };
9266
+ }
9267
+ return serializeCanvasContentResponse({
9268
+ filename: item.filename,
9269
+ kind: item.kind,
9270
+ sizeBytes: item.sizeBytes,
9271
+ mimeType: item.mimeType,
9272
+ bytes: item.bytes
9273
+ });
9274
+ }
9275
+ };
9276
+ var canvasService = new CanvasService();
9277
+
9278
+ // src/services/upload-canvas-items.ts
9279
+ var uploadedCanvasItems = /* @__PURE__ */ new Map();
9280
+ function isReconcileCanvasItemsResponse(value) {
9281
+ return typeof value === "object" && value !== null && "deleted" in value && typeof value.deleted === "number" && (!("skipped" in value) || typeof value.skipped === "string");
9282
+ }
9283
+ async function flushAllCanvasItems() {
9284
+ let flushed = 0;
9285
+ let skipped = 0;
9286
+ let failed = 0;
9287
+ let deleted = 0;
9288
+ const filenames = [];
9289
+ const items = await canvasService.listItems();
9290
+ await Promise.all(items.map(async (item) => {
9291
+ if (item.sizeBytes > MAX_CANVAS_FILE_BYTES) {
9292
+ skipped++;
9293
+ return;
9294
+ }
9295
+ try {
9296
+ const file = await canvasService.getItemFile(item.filename);
9297
+ if (!file?.bytes || file.tooLarge) {
9298
+ skipped++;
9299
+ return;
9300
+ }
9301
+ const uploaded = uploadedCanvasItems.get(file.filename);
9302
+ if (uploaded?.sizeBytes === file.sizeBytes && uploaded.updatedAt === file.updatedAt) {
9303
+ filenames.push(file.filename);
9304
+ skipped++;
9305
+ return;
9306
+ }
9307
+ await uploadCanvasItem(file);
9308
+ uploadedCanvasItems.set(file.filename, { sizeBytes: file.sizeBytes, updatedAt: file.updatedAt });
9309
+ filenames.push(file.filename);
9310
+ flushed++;
9311
+ } catch (err) {
9312
+ failed++;
9313
+ console.error("[CanvasUploader] upload failed:", { filename: item.filename, err });
9314
+ }
9315
+ }));
9316
+ if (failed === 0) {
9317
+ try {
9318
+ deleted = await reconcileCanvasItems(filenames);
9319
+ const currentFilenames = new Set(filenames);
9320
+ for (const filename of uploadedCanvasItems.keys()) {
9321
+ if (!currentFilenames.has(filename)) uploadedCanvasItems.delete(filename);
9322
+ }
9323
+ } catch (err) {
9324
+ failed++;
9325
+ console.error("[CanvasUploader] reconcile failed:", err);
9326
+ }
9327
+ }
9328
+ return { flushed, skipped, failed, deleted };
9329
+ }
9330
+ async function uploadCanvasItem(item) {
9331
+ if (!item.bytes || item.tooLarge) return;
9332
+ const form = new FormData();
9333
+ form.append("filename", item.filename);
9334
+ form.append("updated_at", item.updatedAt);
9335
+ form.append(
9336
+ "file",
9337
+ new Blob([item.bytes], { type: item.mimeType }),
9338
+ item.filename
9339
+ );
9340
+ const response = await monolithRequest("/v1/engine/canvas", { body: form });
9341
+ if (!response.ok) {
9342
+ const errorText = await response.text();
9343
+ throw new Error(`upload failed: ${response.status} ${errorText}`);
9344
+ }
9345
+ }
9346
+ async function reconcileCanvasItems(filenames) {
9347
+ const response = await monolithRequest("/v1/engine/canvas/reconcile", {
9348
+ body: { filenames }
9349
+ });
9350
+ if (!response.ok) {
9351
+ const errorText = await response.text();
9352
+ throw new Error(`reconcile failed: ${response.status} ${errorText}`);
9353
+ }
9354
+ const data = await response.json();
9355
+ if (!isReconcileCanvasItemsResponse(data)) {
9356
+ throw new Error("Invalid response from reconcile endpoint");
9357
+ }
9358
+ return data.deleted;
9359
+ }
9360
+
9361
+ // src/services/upload-chat-transcripts.ts
9362
+ import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
9363
+ import { basename, join as join15 } from "path";
9364
+ import { homedir as homedir13 } from "os";
9365
+ var ENGINE_DIR2 = join15(homedir13(), ".replicas", "engine");
9157
9366
  var HISTORY_DIRS = [
9158
- join14(ENGINE_DIR2, "claude-histories"),
9159
- join14(ENGINE_DIR2, "relay-histories"),
9160
- join14(ENGINE_DIR2, "codex-histories")
9367
+ join15(ENGINE_DIR2, "claude-histories"),
9368
+ join15(ENGINE_DIR2, "relay-histories"),
9369
+ join15(ENGINE_DIR2, "codex-histories")
9161
9370
  ];
9162
9371
  async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
9163
9372
  let flushed = 0;
@@ -9166,7 +9375,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
9166
9375
  for (const dir of HISTORY_DIRS) {
9167
9376
  let entries;
9168
9377
  try {
9169
- entries = await readdir3(dir);
9378
+ entries = await readdir4(dir);
9170
9379
  } catch {
9171
9380
  continue;
9172
9381
  }
@@ -9174,7 +9383,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
9174
9383
  if (!entry.endsWith(".jsonl")) continue;
9175
9384
  const chatId = basename(entry, ".jsonl");
9176
9385
  tasks.push(
9177
- uploadChatTranscript(chatId, join14(dir, entry), chatsById.get(chatId)).then(() => {
9386
+ uploadChatTranscript(chatId, join15(dir, entry), chatsById.get(chatId)).then(() => {
9178
9387
  flushed++;
9179
9388
  }).catch((err) => {
9180
9389
  failed++;
@@ -9187,7 +9396,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map()) {
9187
9396
  return { flushed, failed };
9188
9397
  }
9189
9398
  async function uploadChatTranscript(chatId, filePath, chat) {
9190
- const bytes = await readFile10(filePath);
9399
+ const bytes = await readFile11(filePath);
9191
9400
  if (bytes.byteLength === 0) return;
9192
9401
  const form = new FormData();
9193
9402
  form.append("chat_id", chatId);
@@ -9244,18 +9453,18 @@ var DuplicateDefaultChatError = class extends Error {
9244
9453
  };
9245
9454
 
9246
9455
  // src/services/chat/chat-service.ts
9247
- var ENGINE_DIR3 = join15(homedir13(), ".replicas", "engine");
9248
- var CHATS_FILE = join15(ENGINE_DIR3, "chats.json");
9249
- var CLAUDE_HISTORY_DIR = join15(ENGINE_DIR3, "claude-histories");
9250
- var RELAY_HISTORY_DIR = join15(ENGINE_DIR3, "relay-histories");
9251
- var CODEX_HISTORY_DIR = join15(ENGINE_DIR3, "codex-histories");
9456
+ var ENGINE_DIR3 = join16(homedir14(), ".replicas", "engine");
9457
+ var CHATS_FILE = join16(ENGINE_DIR3, "chats.json");
9458
+ var CLAUDE_HISTORY_DIR = join16(ENGINE_DIR3, "claude-histories");
9459
+ var RELAY_HISTORY_DIR = join16(ENGINE_DIR3, "relay-histories");
9460
+ var CODEX_HISTORY_DIR = join16(ENGINE_DIR3, "codex-histories");
9252
9461
  var HISTORY_DIR_BY_PROVIDER = {
9253
9462
  claude: CLAUDE_HISTORY_DIR,
9254
9463
  relay: RELAY_HISTORY_DIR,
9255
9464
  codex: CODEX_HISTORY_DIR
9256
9465
  };
9257
- var CHAT_SENDERS_DIR = join15(ENGINE_DIR3, "chat-senders");
9258
- var CODEX_AUTH_PATH2 = join15(homedir13(), ".codex", "auth.json");
9466
+ var CHAT_SENDERS_DIR = join16(ENGINE_DIR3, "chat-senders");
9467
+ var CODEX_AUTH_PATH2 = join16(homedir14(), ".codex", "auth.json");
9259
9468
  var CHATS_BACKUP_FILE = `${CHATS_FILE}.bak`;
9260
9469
  function isChatMessageSender(value) {
9261
9470
  if (!isRecord4(value)) return false;
@@ -9453,7 +9662,7 @@ var ChatService = class {
9453
9662
  };
9454
9663
  }
9455
9664
  senderFilePath(chatId) {
9456
- return join15(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
9665
+ return join16(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
9457
9666
  }
9458
9667
  async appendSender(chatId, sender) {
9459
9668
  try {
@@ -9464,7 +9673,7 @@ var ChatService = class {
9464
9673
  }
9465
9674
  async readSenders(chatId) {
9466
9675
  try {
9467
- const content = await readFile11(this.senderFilePath(chatId), "utf-8");
9676
+ const content = await readFile12(this.senderFilePath(chatId), "utf-8");
9468
9677
  const lines = content.split("\n").filter((line) => line.trim().length > 0);
9469
9678
  const senders = [];
9470
9679
  for (const line of lines) {
@@ -9609,7 +9818,7 @@ var ChatService = class {
9609
9818
  return descendants;
9610
9819
  }
9611
9820
  async deleteHistoryFile(persisted) {
9612
- await rm(join15(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`), { force: true });
9821
+ await rm(join16(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`), { force: true });
9613
9822
  await rm(this.senderFilePath(persisted.id), { force: true });
9614
9823
  }
9615
9824
  async getChatHistory(chatId) {
@@ -9647,6 +9856,16 @@ var ChatService = class {
9647
9856
  [...this.chats.entries()].map(([chatId, chat]) => [chatId, this.toSummary(chat)])
9648
9857
  ));
9649
9858
  }
9859
+ async flushAllWorkspaceArtifacts() {
9860
+ const chatsById = new Map(
9861
+ [...this.chats.entries()].map(([chatId, chat]) => [chatId, this.toSummary(chat)])
9862
+ );
9863
+ const [chatTranscripts, canvas] = await Promise.all([
9864
+ flushAllChatTranscripts(chatsById),
9865
+ flushAllCanvasItems()
9866
+ ]);
9867
+ return { chatTranscripts, canvas };
9868
+ }
9650
9869
  createRuntimeChat(persisted) {
9651
9870
  const saveSession = async (sessionId) => {
9652
9871
  persisted.providerSessionId = sessionId;
@@ -9670,7 +9889,7 @@ var ChatService = class {
9670
9889
  if (persisted.provider === "claude") {
9671
9890
  provider = new ClaudeManager({
9672
9891
  workingDirectory: this.workingDirectory,
9673
- historyFilePath: join15(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
9892
+ historyFilePath: join16(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
9674
9893
  initialSessionId: persisted.providerSessionId,
9675
9894
  onSaveSessionId: saveSession,
9676
9895
  onTurnComplete: onProviderTurnComplete,
@@ -9679,7 +9898,7 @@ var ChatService = class {
9679
9898
  } else if (persisted.provider === "relay") {
9680
9899
  provider = new RelayManager({
9681
9900
  workingDirectory: this.workingDirectory,
9682
- historyFilePath: join15(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
9901
+ historyFilePath: join16(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
9683
9902
  initialSessionId: persisted.providerSessionId,
9684
9903
  onSaveSessionId: saveSession,
9685
9904
  onTurnComplete: onProviderTurnComplete,
@@ -9690,7 +9909,7 @@ var ChatService = class {
9690
9909
  } else {
9691
9910
  provider = new CodexAspManager({
9692
9911
  workingDirectory: this.workingDirectory,
9693
- historyFilePath: join15(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
9912
+ historyFilePath: join16(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
9694
9913
  initialSessionId: persisted.providerSessionId,
9695
9914
  onSaveSessionId: saveSession,
9696
9915
  onTurnComplete: onProviderTurnComplete,
@@ -9828,11 +10047,14 @@ var ChatService = class {
9828
10047
  });
9829
10048
  uploadChatTranscript(
9830
10049
  chatId,
9831
- join15(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
10050
+ join16(HISTORY_DIR_BY_PROVIDER[chat.persisted.provider], `${chatId}.jsonl`),
9832
10051
  this.toSummary(chat)
9833
10052
  ).catch((err) => {
9834
10053
  console.error("[ChatService] Failed to upload chat transcript:", { chatId, err });
9835
10054
  });
10055
+ flushAllCanvasItems().catch((err) => {
10056
+ console.error("[ChatService] Failed to upload canvas items:", { chatId, err });
10057
+ });
9836
10058
  await this.publishAgentTurnCompleteWebhook(chat);
9837
10059
  }
9838
10060
  getRuntimeChat(chatId) {
@@ -9840,7 +10062,7 @@ var ChatService = class {
9840
10062
  }
9841
10063
  async loadChats() {
9842
10064
  try {
9843
- const content = await readFile11(CHATS_FILE, "utf-8");
10065
+ const content = await readFile12(CHATS_FILE, "utf-8");
9844
10066
  return parsePersistedChatsContent(content);
9845
10067
  } catch (error) {
9846
10068
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -9855,7 +10077,7 @@ var ChatService = class {
9855
10077
  console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
9856
10078
  }
9857
10079
  try {
9858
- const backupContent = await readFile11(CHATS_BACKUP_FILE, "utf-8");
10080
+ const backupContent = await readFile12(CHATS_BACKUP_FILE, "utf-8");
9859
10081
  return parsePersistedChatsContent(backupContent);
9860
10082
  } catch (backupError) {
9861
10083
  if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
@@ -9940,8 +10162,8 @@ var ChatService = class {
9940
10162
 
9941
10163
  // src/services/repo-file-service.ts
9942
10164
  import { execFile as execFile2 } from "child_process";
9943
- import { readFile as readFile12, realpath, stat as stat2 } from "fs/promises";
9944
- import { join as join16, resolve, extname } from "path";
10165
+ import { readFile as readFile13, realpath, stat as stat3 } from "fs/promises";
10166
+ import { join as join17, resolve, extname } from "path";
9945
10167
  var CACHE_TTL_MS = 3e4;
9946
10168
  var SEARCH_TIMEOUT_MS = 15e3;
9947
10169
  var MAX_CONTENT_BYTES = 256 * 1024;
@@ -10001,11 +10223,11 @@ function scoreMatch(query2, filePath) {
10001
10223
  const lowerQuery = query2.toLowerCase();
10002
10224
  const lowerPath = filePath.toLowerCase();
10003
10225
  const segments = lowerPath.split("/");
10004
- const basename3 = segments[segments.length - 1] ?? "";
10005
- if (basename3 === lowerQuery) return 100;
10006
- if (basename3.startsWith(lowerQuery)) return 90;
10226
+ const basename2 = segments[segments.length - 1] ?? "";
10227
+ if (basename2 === lowerQuery) return 100;
10228
+ if (basename2.startsWith(lowerQuery)) return 90;
10007
10229
  if (segments.some((seg) => seg === lowerQuery)) return 80;
10008
- if (basename3.includes(lowerQuery)) return 70;
10230
+ if (basename2.includes(lowerQuery)) return 70;
10009
10231
  if (segments.some((seg) => seg.includes(lowerQuery))) return 60;
10010
10232
  if (lowerPath.includes(lowerQuery)) return 50;
10011
10233
  return 0;
@@ -10101,11 +10323,11 @@ var RepoFileService = class {
10101
10323
  const repo = repos.find((r) => r.name === repoName);
10102
10324
  if (!repo) return null;
10103
10325
  try {
10104
- const fullPath = await realpath(resolve(join16(repo.path, filePath)));
10326
+ const fullPath = await realpath(resolve(join17(repo.path, filePath)));
10105
10327
  const repoRoot = await realpath(repo.path);
10106
10328
  const repoPrefix = repoRoot.endsWith("/") ? repoRoot : repoRoot + "/";
10107
10329
  if (!fullPath.startsWith(repoPrefix) && fullPath !== repoRoot) return null;
10108
- const fileStat = await stat2(fullPath);
10330
+ const fileStat = await stat3(fullPath);
10109
10331
  if (!fileStat.isFile()) return null;
10110
10332
  const sizeBytes = fileStat.size;
10111
10333
  if (isBinaryExtension(filePath)) {
@@ -10130,7 +10352,7 @@ var RepoFileService = class {
10130
10352
  tooLarge: true
10131
10353
  };
10132
10354
  }
10133
- const content = await readFile12(fullPath, "utf-8");
10355
+ const content = await readFile13(fullPath, "utf-8");
10134
10356
  return {
10135
10357
  repoName,
10136
10358
  path: filePath,
@@ -10211,86 +10433,6 @@ import { z as z2 } from "zod";
10211
10433
  import { readdir as readdir6, stat as stat4, readFile as readFile16 } from "fs/promises";
10212
10434
  import { join as join20, resolve as resolve2 } from "path";
10213
10435
 
10214
- // src/services/canvas-service.ts
10215
- import { readdir as readdir4, readFile as readFile13, stat as stat3 } from "fs/promises";
10216
- import { homedir as homedir14 } from "os";
10217
- import { basename as basename2, join as join17 } from "path";
10218
- var CANVAS_DIRECTORIES = [
10219
- join17(homedir14(), ".claude", "plans"),
10220
- join17(homedir14(), ".replicas", "canvas")
10221
- ];
10222
- var MAX_CANVAS_FILE_BYTES = 5 * 1024 * 1024;
10223
- function isTextKind(kind) {
10224
- return kind === "markdown" || kind === "html";
10225
- }
10226
- function sanitizeFilename(filename) {
10227
- return basename2(filename);
10228
- }
10229
- var CanvasService = class {
10230
- async listItems() {
10231
- const items = /* @__PURE__ */ new Map();
10232
- for (const directory of CANVAS_DIRECTORIES) {
10233
- let entries;
10234
- try {
10235
- entries = await readdir4(directory, { withFileTypes: true });
10236
- } catch {
10237
- continue;
10238
- }
10239
- for (const entry of entries) {
10240
- if (!entry.isFile()) continue;
10241
- if (entry.name.startsWith(".")) continue;
10242
- if (items.has(entry.name)) continue;
10243
- const { kind } = classifyCanvasFilename(entry.name);
10244
- let sizeBytes = 0;
10245
- try {
10246
- const s = await stat3(join17(directory, entry.name));
10247
- sizeBytes = s.size;
10248
- } catch {
10249
- continue;
10250
- }
10251
- items.set(entry.name, { filename: entry.name, kind, sizeBytes });
10252
- }
10253
- }
10254
- return Array.from(items.values()).sort((a, b) => a.filename.localeCompare(b.filename));
10255
- }
10256
- async getItem(filename) {
10257
- const safe = sanitizeFilename(filename);
10258
- if (!safe || safe !== filename || safe.startsWith(".")) return null;
10259
- const { kind, mimeType } = classifyCanvasFilename(safe);
10260
- for (const directory of CANVAS_DIRECTORIES) {
10261
- const filePath = join17(directory, safe);
10262
- let sizeBytes = 0;
10263
- try {
10264
- const s = await stat3(filePath);
10265
- sizeBytes = s.size;
10266
- } catch {
10267
- continue;
10268
- }
10269
- if (sizeBytes > MAX_CANVAS_FILE_BYTES) {
10270
- return {
10271
- filename: safe,
10272
- kind,
10273
- sizeBytes,
10274
- mimeType,
10275
- tooLarge: true
10276
- };
10277
- }
10278
- try {
10279
- if (isTextKind(kind)) {
10280
- const content = await readFile13(filePath, "utf-8");
10281
- return { filename: safe, kind, sizeBytes, mimeType, content };
10282
- }
10283
- const buf = await readFile13(filePath);
10284
- return { filename: safe, kind, sizeBytes, mimeType, base64: buf.toString("base64") };
10285
- } catch {
10286
- continue;
10287
- }
10288
- }
10289
- return null;
10290
- }
10291
- };
10292
- var canvasService = new CanvasService();
10293
-
10294
10436
  // src/services/warm-hooks-service.ts
10295
10437
  import { spawn as spawn4 } from "child_process";
10296
10438
  import { readFile as readFile15 } from "fs/promises";
@@ -10907,6 +11049,17 @@ function createV1Routes(deps) {
10907
11049
  );
10908
11050
  }
10909
11051
  });
11052
+ app2.post("/workspace-artifacts/flush-all", async (c) => {
11053
+ try {
11054
+ const result = await deps.chatService.flushAllWorkspaceArtifacts();
11055
+ return c.json(result);
11056
+ } catch (error) {
11057
+ return c.json(
11058
+ jsonError("Failed to flush workspace artifacts", error instanceof Error ? error.message : "Unknown error"),
11059
+ 500
11060
+ );
11061
+ }
11062
+ });
10910
11063
  app2.get("/chats/:chatId/queue", (c) => {
10911
11064
  try {
10912
11065
  const result = deps.chatService.getChatQueue(c.req.param("chatId"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.309",
3
+ "version": "0.1.313",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",