mexus-cli 1.0.1 → 1.0.2
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/package.json +1 -1
- package/packages/server/dist/cli.mjs +632 -149
- package/packages/server/dist/cli.mjs.map +1 -1
- package/packages/web/dist/assets/{_basePickBy-DQhOWiJk.js → _basePickBy-Co7zHyMx.js} +1 -1
- package/packages/web/dist/assets/{_baseUniq-DXTx6DDJ.js → _baseUniq-Dxx1kUxk.js} +1 -1
- package/packages/web/dist/assets/{arc-D98xlvrt.js → arc-BvdbVAwY.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-2XIMDMQ5-BFC9xn2H.js → architectureDiagram-2XIMDMQ5-NJEtmrih.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-WCTKOSBZ-DJH8Soam.js → blockDiagram-WCTKOSBZ-NMdKrHRR.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-IC4MRINW-w36rnsiL.js → c4Diagram-IC4MRINW-Cid8liHK.js} +1 -1
- package/packages/web/dist/assets/channel-ggzcU6fx.js +1 -0
- package/packages/web/dist/assets/{chunk-4BX2VUAB-SF13Eulk.js → chunk-4BX2VUAB-C0Cpcxjt.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-wMI9Klww.js → chunk-55IACEB6-uCQWWVUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-WwcC59gR.js → chunk-FMBD7UC4-j69k_AXR.js} +1 -1
- package/packages/web/dist/assets/{chunk-JSJVCQXG-DvXmNU5-.js → chunk-JSJVCQXG-BRb3U-tF.js} +1 -1
- package/packages/web/dist/assets/{chunk-KX2RTZJC-BdEEznCp.js → chunk-KX2RTZJC-CMLyC_k7.js} +1 -1
- package/packages/web/dist/assets/{chunk-NQ4KR5QH-BwNAeHYs.js → chunk-NQ4KR5QH-DUQxdvVp.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-C2xATrOG.js → chunk-QZHKN3VN-I_0dQXDN.js} +1 -1
- package/packages/web/dist/assets/{chunk-WL4C6EOR-CEAL3q8O.js → chunk-WL4C6EOR-BR8lxAey.js} +1 -1
- package/packages/web/dist/assets/classDiagram-VBA2DB6C-qeSRjRBc.js +1 -0
- package/packages/web/dist/assets/classDiagram-v2-RAHNMMFH-qeSRjRBc.js +1 -0
- package/packages/web/dist/assets/clone-CGAMRvPs.js +1 -0
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DMB9iF_s.js → cose-bilkent-S5V4N54A-BF1cADHY.js} +1 -1
- package/packages/web/dist/assets/{dagre-KLK3FWXG-B3CBp1UR.js → dagre-KLK3FWXG-DDt0p4vG.js} +1 -1
- package/packages/web/dist/assets/{diagram-E7M64L7V-BP3gXbyt.js → diagram-E7M64L7V-CTkR6JTl.js} +1 -1
- package/packages/web/dist/assets/{diagram-IFDJBPK2-DiWKPyS1.js → diagram-IFDJBPK2-DKPHzwe0.js} +1 -1
- package/packages/web/dist/assets/{diagram-P4PSJMXO-BRwIWc-T.js → diagram-P4PSJMXO-k5sUOXg6.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-INFDFZHY-CwNQ0bqL.js → erDiagram-INFDFZHY-HQ6gUchT.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-PKNHOUZH-CGwKqUfG.js → flowDiagram-PKNHOUZH-zQ6z0FfA.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-A5KZAMGK-CwfwEk-O.js → ganttDiagram-A5KZAMGK-7OanXR1G.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-K3NZZRJ6-BU-TCNoX.js → gitGraphDiagram-K3NZZRJ6-CWHj7ZAf.js} +1 -1
- package/packages/web/dist/assets/{graph-BpGuAB34.js → graph-DGZqBZjC.js} +1 -1
- package/packages/web/dist/assets/index-DWMolj1f.css +32 -0
- package/packages/web/dist/assets/{index-BQZEjwBD.js → index-ORmXz3Zu.js} +256 -204
- package/packages/web/dist/assets/{infoDiagram-LFFYTUFH-OgqnEPLD.js → infoDiagram-LFFYTUFH-yRz9H7FV.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-PHBUUO56-C7q1YvZf.js → ishikawaDiagram-PHBUUO56-D9KVAEH1.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-4ABVD52K-BFTnwlYT.js → journeyDiagram-4ABVD52K-xYc2g_2E.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-K7BYSVSG-BPNSFbTq.js → kanban-definition-K7BYSVSG-C9ctSGeG.js} +1 -1
- package/packages/web/dist/assets/{layout-DiHSY-T6.js → layout-dR6Z9QBB.js} +1 -1
- package/packages/web/dist/assets/{linear-DcbEPsvj.js → linear-C41sKDUx.js} +1 -1
- package/packages/web/dist/assets/{mindmap-definition-YRQLILUH-CalG0npn.js → mindmap-definition-YRQLILUH-DnEvOt89.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-SKSYHLDU-D1PyVe3u.js → pieDiagram-SKSYHLDU-Dwygi596.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-337W2JSQ-bTKx0-2j.js → quadrantDiagram-337W2JSQ-CGsDpSZv.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-Z7DCOOCP-DHyIVcFu.js → requirementDiagram-Z7DCOOCP-DrFNwpYL.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-WA2Y5GQK-CifZqZcJ.js → sankeyDiagram-WA2Y5GQK-CKWlgXXF.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-2WXFIKYE-DK8qO73H.js → sequenceDiagram-2WXFIKYE-DpbgNxFF.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-RAJIS63D-BoK_s4xU.js → stateDiagram-RAJIS63D-UQV1zJr3.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-FVOUBMTO-CmZylZ6s.js +1 -0
- package/packages/web/dist/assets/{timeline-definition-YZTLITO2-GwB8m3Pz.js → timeline-definition-YZTLITO2-CBNnAMDu.js} +1 -1
- package/packages/web/dist/assets/{treemap-KZPCXAKY-RvN0JaqD.js → treemap-KZPCXAKY-DQg0JG5Z.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-LZ73GAT5-OnlpPkDW.js → vennDiagram-LZ73GAT5-6EhbiWUV.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-JWTSCODW-Dv-KEW3d.js → xychartDiagram-JWTSCODW-DrTSQ6rp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/channel-s9cXVaHr.js +0 -1
- package/packages/web/dist/assets/classDiagram-VBA2DB6C-quXqjdGh.js +0 -1
- package/packages/web/dist/assets/classDiagram-v2-RAHNMMFH-quXqjdGh.js +0 -1
- package/packages/web/dist/assets/clone-BwNC5Rz2.js +0 -1
- package/packages/web/dist/assets/index-SBtQaWyn.css +0 -32
- package/packages/web/dist/assets/stateDiagram-v2-FVOUBMTO-BKcMBcxU.js +0 -1
|
@@ -43,6 +43,7 @@ var init_ConfigManager = __esm({
|
|
|
43
43
|
resume_flag: "--resume",
|
|
44
44
|
yolo_flag: "--dangerously-skip-permissions",
|
|
45
45
|
statusline: true,
|
|
46
|
+
transport: "pty",
|
|
46
47
|
env: {}
|
|
47
48
|
},
|
|
48
49
|
codex: {
|
|
@@ -51,6 +52,7 @@ var init_ConfigManager = __esm({
|
|
|
51
52
|
resume_flag: "",
|
|
52
53
|
yolo_flag: "",
|
|
53
54
|
statusline: false,
|
|
55
|
+
transport: "pty",
|
|
54
56
|
env: {}
|
|
55
57
|
},
|
|
56
58
|
opencode: {
|
|
@@ -59,6 +61,7 @@ var init_ConfigManager = __esm({
|
|
|
59
61
|
resume_flag: "",
|
|
60
62
|
yolo_flag: "--yolo",
|
|
61
63
|
statusline: false,
|
|
64
|
+
transport: "acp",
|
|
62
65
|
env: {}
|
|
63
66
|
},
|
|
64
67
|
"kimi-cli": {
|
|
@@ -67,6 +70,7 @@ var init_ConfigManager = __esm({
|
|
|
67
70
|
resume_flag: "",
|
|
68
71
|
yolo_flag: "",
|
|
69
72
|
statusline: false,
|
|
73
|
+
transport: "pty",
|
|
70
74
|
env: {}
|
|
71
75
|
},
|
|
72
76
|
qodercli: {
|
|
@@ -75,6 +79,7 @@ var init_ConfigManager = __esm({
|
|
|
75
79
|
resume_flag: "-r",
|
|
76
80
|
yolo_flag: "--yolo",
|
|
77
81
|
statusline: false,
|
|
82
|
+
transport: "pty",
|
|
78
83
|
env: {}
|
|
79
84
|
}
|
|
80
85
|
}
|
|
@@ -168,11 +173,11 @@ var init_ConfigManager = __esm({
|
|
|
168
173
|
async detectAgentsAsync() {
|
|
169
174
|
const agents = {};
|
|
170
175
|
const agentBins = [
|
|
171
|
-
{ key: "claudecode", bin: "claude", flag: "--continue", statusline: true },
|
|
172
|
-
{ key: "codex", bin: "codex", flag: "", statusline: false },
|
|
173
|
-
{ key: "opencode", bin: "opencode", flag: "--continue", statusline: false },
|
|
174
|
-
{ key: "kimi-cli", bin: "kimi", flag: "--continue", statusline: false },
|
|
175
|
-
{ key: "qodercli", bin: "qodercli", flag: "-c", statusline: false }
|
|
176
|
+
{ key: "claudecode", bin: "claude", flag: "--continue", statusline: true, transport: "pty" },
|
|
177
|
+
{ key: "codex", bin: "codex", flag: "", statusline: false, transport: "pty" },
|
|
178
|
+
{ key: "opencode", bin: "opencode", flag: "--continue", statusline: false, transport: "acp" },
|
|
179
|
+
{ key: "kimi-cli", bin: "kimi", flag: "--continue", statusline: false, transport: "pty" },
|
|
180
|
+
{ key: "qodercli", bin: "qodercli", flag: "-c", statusline: false, transport: "pty" }
|
|
176
181
|
];
|
|
177
182
|
const results = await Promise.allSettled(
|
|
178
183
|
agentBins.map(async (agent) => {
|
|
@@ -187,6 +192,7 @@ var init_ConfigManager = __esm({
|
|
|
187
192
|
bin: agent.bin,
|
|
188
193
|
continue_flag: agent.flag,
|
|
189
194
|
statusline: agent.statusline,
|
|
195
|
+
transport: agent.transport,
|
|
190
196
|
env: {}
|
|
191
197
|
};
|
|
192
198
|
}
|
|
@@ -270,16 +276,16 @@ var init_ConfigManager = __esm({
|
|
|
270
276
|
});
|
|
271
277
|
|
|
272
278
|
// src/cli.ts
|
|
273
|
-
import
|
|
274
|
-
import
|
|
279
|
+
import path11 from "path";
|
|
280
|
+
import fs10 from "fs";
|
|
275
281
|
|
|
276
282
|
// src/index.ts
|
|
277
283
|
init_ConfigManager();
|
|
278
284
|
import Fastify from "fastify";
|
|
279
285
|
import fastifyWebsocket from "@fastify/websocket";
|
|
280
286
|
import fastifyStatic from "@fastify/static";
|
|
281
|
-
import
|
|
282
|
-
import
|
|
287
|
+
import path10 from "path";
|
|
288
|
+
import fs9 from "fs";
|
|
283
289
|
import yaml3 from "js-yaml";
|
|
284
290
|
import { fileURLToPath } from "url";
|
|
285
291
|
|
|
@@ -710,7 +716,8 @@ var OutputStateAnalyzer = class {
|
|
|
710
716
|
function stripAnsi2(str) {
|
|
711
717
|
return str.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b[()][AB012]/g, "").replace(/\x1b[>=<]/g, "").replace(/\x0f|\x0e/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
712
718
|
}
|
|
713
|
-
var
|
|
719
|
+
var MAX_BUFFER_SIZE2 = 64 * 1024;
|
|
720
|
+
var ActivityParser = class _ActivityParser {
|
|
714
721
|
buffer = "";
|
|
715
722
|
lastFile = "";
|
|
716
723
|
lastTime = 0;
|
|
@@ -719,6 +726,9 @@ var ActivityParser = class {
|
|
|
719
726
|
parse(data) {
|
|
720
727
|
if (!data.includes("\n")) {
|
|
721
728
|
this.buffer += data;
|
|
729
|
+
if (this.buffer.length > MAX_BUFFER_SIZE2) {
|
|
730
|
+
this.buffer = "";
|
|
731
|
+
}
|
|
722
732
|
return null;
|
|
723
733
|
}
|
|
724
734
|
const lines = (this.buffer + data).split("\n");
|
|
@@ -825,10 +835,15 @@ var ActivityParser = class {
|
|
|
825
835
|
cleanPath(raw) {
|
|
826
836
|
return raw.trim().replace(/^['"`]+|['"`]+$/g, "").replace(/^\.\//, "").replace(/\s+.*$/, "").replace(/[,;:)]+$/, "").replace(/^\(/, "");
|
|
827
837
|
}
|
|
838
|
+
// Paths under these prefixes are Nexus internals or noise — ignore them
|
|
839
|
+
static IGNORED_PREFIXES = [".nexus/", "node_modules/", ".git/"];
|
|
828
840
|
isValidPath(file) {
|
|
829
841
|
if (!file || file.length < 3) return false;
|
|
830
842
|
if (!/\.\w{1,10}$/.test(file)) return false;
|
|
831
843
|
if (file.startsWith("/") || file.includes("://")) return false;
|
|
844
|
+
for (const prefix of _ActivityParser.IGNORED_PREFIXES) {
|
|
845
|
+
if (file.startsWith(prefix)) return false;
|
|
846
|
+
}
|
|
832
847
|
if (/[<>|&$`\\{}[\]]/.test(file)) return false;
|
|
833
848
|
const parts = file.split("/");
|
|
834
849
|
if (parts.length > 15) return false;
|
|
@@ -876,7 +891,19 @@ var PtyManager = class {
|
|
|
876
891
|
}
|
|
877
892
|
}
|
|
878
893
|
if (agentDef?.env) {
|
|
894
|
+
const BLOCKED_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
895
|
+
"PATH",
|
|
896
|
+
"LD_PRELOAD",
|
|
897
|
+
"LD_LIBRARY_PATH",
|
|
898
|
+
"DYLD_INSERT_LIBRARIES",
|
|
899
|
+
"DYLD_LIBRARY_PATH",
|
|
900
|
+
"DYLD_FRAMEWORK_PATH"
|
|
901
|
+
]);
|
|
879
902
|
for (const [key, value] of Object.entries(agentDef.env)) {
|
|
903
|
+
if (BLOCKED_ENV_KEYS.has(key)) {
|
|
904
|
+
console.warn(`[PTY] Ignoring blocked env var from agent config: ${key}`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
880
907
|
const resolved = value.replace(/\$\{(\w+)\}/g, (_, varName) => {
|
|
881
908
|
return process.env[varName] || "";
|
|
882
909
|
});
|
|
@@ -1049,6 +1076,10 @@ var PtyManager = class {
|
|
|
1049
1076
|
entry.shellDetector?.dispose();
|
|
1050
1077
|
entry.agentDetector?.dispose();
|
|
1051
1078
|
entry.parser.reset();
|
|
1079
|
+
entry.onDataCallbacks.length = 0;
|
|
1080
|
+
entry.onStatusCallbacks.length = 0;
|
|
1081
|
+
entry.onMetaCallbacks.length = 0;
|
|
1082
|
+
entry.onActivityCallbacks.length = 0;
|
|
1052
1083
|
try {
|
|
1053
1084
|
entry.pty.kill();
|
|
1054
1085
|
} catch {
|
|
@@ -1104,9 +1135,368 @@ var PtyManager = class {
|
|
|
1104
1135
|
}
|
|
1105
1136
|
};
|
|
1106
1137
|
|
|
1107
|
-
// src/
|
|
1108
|
-
import
|
|
1138
|
+
// src/runtime/AcpRuntime.ts
|
|
1139
|
+
import { spawn as spawn2 } from "child_process";
|
|
1109
1140
|
import fs3 from "fs";
|
|
1141
|
+
import os3 from "os";
|
|
1142
|
+
import path3 from "path";
|
|
1143
|
+
var MAX_SCROLLBACK_BYTES2 = 512 * 1024;
|
|
1144
|
+
function resolveAgentEnv(agentDef) {
|
|
1145
|
+
const env = {};
|
|
1146
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1147
|
+
if (value !== void 0 && !key.startsWith("CLAUDE") && key !== "CLAUDECODE") {
|
|
1148
|
+
env[key] = value;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (agentDef?.env) {
|
|
1152
|
+
const blocked = /* @__PURE__ */ new Set([
|
|
1153
|
+
"PATH",
|
|
1154
|
+
"LD_PRELOAD",
|
|
1155
|
+
"LD_LIBRARY_PATH",
|
|
1156
|
+
"DYLD_INSERT_LIBRARIES",
|
|
1157
|
+
"DYLD_LIBRARY_PATH",
|
|
1158
|
+
"DYLD_FRAMEWORK_PATH"
|
|
1159
|
+
]);
|
|
1160
|
+
for (const [key, value] of Object.entries(agentDef.env)) {
|
|
1161
|
+
if (blocked.has(key)) continue;
|
|
1162
|
+
env[key] = value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || "");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!env.PATH) {
|
|
1166
|
+
env.PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
1167
|
+
if (process.platform === "darwin") {
|
|
1168
|
+
env.PATH = `/opt/homebrew/bin:/opt/homebrew/sbin:${env.PATH}`;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return env;
|
|
1172
|
+
}
|
|
1173
|
+
var AcpRuntime = class {
|
|
1174
|
+
entries = /* @__PURE__ */ new Map();
|
|
1175
|
+
configManager;
|
|
1176
|
+
constructor(configManager) {
|
|
1177
|
+
this.configManager = configManager;
|
|
1178
|
+
}
|
|
1179
|
+
spawn(paneId, config) {
|
|
1180
|
+
if (this.entries.has(paneId)) {
|
|
1181
|
+
this.kill(paneId);
|
|
1182
|
+
}
|
|
1183
|
+
const projectDir = this.configManager.getProjectDir();
|
|
1184
|
+
const basePath = config.isolation === "worktree" && config.worktreePath ? config.worktreePath : projectDir;
|
|
1185
|
+
let cwd = config.workdir ? path3.resolve(basePath, config.workdir) : basePath;
|
|
1186
|
+
if (!fs3.existsSync(cwd)) {
|
|
1187
|
+
cwd = fs3.existsSync(projectDir) ? projectDir : os3.homedir();
|
|
1188
|
+
}
|
|
1189
|
+
const agentDef = this.configManager.getAgentDefinition(config.agent);
|
|
1190
|
+
if (!agentDef) {
|
|
1191
|
+
throw new Error(`Missing agent definition for ${config.agent}`);
|
|
1192
|
+
}
|
|
1193
|
+
const env = resolveAgentEnv(agentDef);
|
|
1194
|
+
const proc = spawn2(agentDef.bin, ["acp"], {
|
|
1195
|
+
cwd,
|
|
1196
|
+
env,
|
|
1197
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1198
|
+
});
|
|
1199
|
+
const entry = {
|
|
1200
|
+
proc,
|
|
1201
|
+
config,
|
|
1202
|
+
status: "running",
|
|
1203
|
+
meta: { cwd },
|
|
1204
|
+
nextRequestId: 1,
|
|
1205
|
+
nextMessageId: 1,
|
|
1206
|
+
nextToolId: 1,
|
|
1207
|
+
pending: /* @__PURE__ */ new Map(),
|
|
1208
|
+
stdoutBuffer: "",
|
|
1209
|
+
scrollback: [],
|
|
1210
|
+
scrollbackBytes: 0,
|
|
1211
|
+
onDataCallbacks: [],
|
|
1212
|
+
onStatusCallbacks: [],
|
|
1213
|
+
onMetaCallbacks: [],
|
|
1214
|
+
onConversationCallbacks: [],
|
|
1215
|
+
onActivityCallbacks: []
|
|
1216
|
+
};
|
|
1217
|
+
this.entries.set(paneId, entry);
|
|
1218
|
+
proc.stdout.setEncoding("utf8");
|
|
1219
|
+
proc.stderr.setEncoding("utf8");
|
|
1220
|
+
proc.stdout.on("data", (chunk) => this.handleStdout(paneId, chunk));
|
|
1221
|
+
proc.stderr.on("data", (chunk) => {
|
|
1222
|
+
this.emitTerminal(paneId, `[acp stderr] ${chunk}`);
|
|
1223
|
+
});
|
|
1224
|
+
proc.on("exit", (code, signal) => {
|
|
1225
|
+
const e = this.entries.get(paneId);
|
|
1226
|
+
if (!e) return;
|
|
1227
|
+
for (const pending of e.pending.values()) {
|
|
1228
|
+
pending.reject(new Error(`ACP process exited (${code ?? "null"}${signal ? `, ${signal}` : ""})`));
|
|
1229
|
+
}
|
|
1230
|
+
e.pending.clear();
|
|
1231
|
+
this.setStatus(paneId, code === 0 ? "stopped" : "error");
|
|
1232
|
+
});
|
|
1233
|
+
this.bootstrap(paneId, cwd, config).catch((err) => {
|
|
1234
|
+
this.emitTerminal(paneId, `[acp error] ${err.message}
|
|
1235
|
+
`);
|
|
1236
|
+
this.setStatus(paneId, "error");
|
|
1237
|
+
});
|
|
1238
|
+
return proc.pid;
|
|
1239
|
+
}
|
|
1240
|
+
async bootstrap(paneId, cwd, config) {
|
|
1241
|
+
const initResult = await this.request(paneId, "initialize", {
|
|
1242
|
+
protocolVersion: 1,
|
|
1243
|
+
clientInfo: { name: "nexus", version: "0.1.0" },
|
|
1244
|
+
clientCapabilities: {}
|
|
1245
|
+
});
|
|
1246
|
+
const loadedSessionId = config.restore === "resume" && config.sessionId ? await this.tryLoadSession(paneId, config.sessionId) : null;
|
|
1247
|
+
const sessionResult = loadedSessionId ? { sessionId: loadedSessionId } : await this.request(paneId, "session/new", {
|
|
1248
|
+
cwd
|
|
1249
|
+
});
|
|
1250
|
+
const sessionId = this.extractSessionId(sessionResult) || this.extractSessionId(initResult) || config.sessionId;
|
|
1251
|
+
if (sessionId) {
|
|
1252
|
+
this.updateMeta(paneId, { sessionId, cwd });
|
|
1253
|
+
} else {
|
|
1254
|
+
this.updateMeta(paneId, { cwd });
|
|
1255
|
+
}
|
|
1256
|
+
this.emitConversation(paneId, { type: "status", status: "idle" });
|
|
1257
|
+
this.setStatus(paneId, "idle");
|
|
1258
|
+
if (config.task && config.restore !== "manual") {
|
|
1259
|
+
await this.sendPrompt(paneId, config.task);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async tryLoadSession(paneId, sessionId) {
|
|
1263
|
+
try {
|
|
1264
|
+
const result = await this.request(paneId, "session/load", { sessionId });
|
|
1265
|
+
return this.extractSessionId(result) || sessionId;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async sendPrompt(paneId, text) {
|
|
1271
|
+
const entry = this.entries.get(paneId);
|
|
1272
|
+
if (!entry) return;
|
|
1273
|
+
const sessionId = entry.meta.sessionId || entry.config.sessionId;
|
|
1274
|
+
const messageId = `user-${entry.nextMessageId++}`;
|
|
1275
|
+
this.emitConversation(paneId, { type: "message", messageId, role: "user", text });
|
|
1276
|
+
this.emitTerminal(paneId, `
|
|
1277
|
+
> ${text}
|
|
1278
|
+
|
|
1279
|
+
`);
|
|
1280
|
+
this.setStatus(paneId, "running");
|
|
1281
|
+
this.emitConversation(paneId, { type: "status", status: "running" });
|
|
1282
|
+
const params = {
|
|
1283
|
+
prompt: [{ type: "text", text }]
|
|
1284
|
+
};
|
|
1285
|
+
if (sessionId) params.sessionId = sessionId;
|
|
1286
|
+
await this.request(paneId, "session/prompt", params);
|
|
1287
|
+
}
|
|
1288
|
+
onData(paneId, cb) {
|
|
1289
|
+
const entry = this.entries.get(paneId);
|
|
1290
|
+
if (entry) entry.onDataCallbacks.push(cb);
|
|
1291
|
+
}
|
|
1292
|
+
onStatus(paneId, cb) {
|
|
1293
|
+
const entry = this.entries.get(paneId);
|
|
1294
|
+
if (entry) entry.onStatusCallbacks.push(cb);
|
|
1295
|
+
}
|
|
1296
|
+
onMeta(paneId, cb) {
|
|
1297
|
+
const entry = this.entries.get(paneId);
|
|
1298
|
+
if (entry) entry.onMetaCallbacks.push(cb);
|
|
1299
|
+
}
|
|
1300
|
+
onConversation(paneId, cb) {
|
|
1301
|
+
const entry = this.entries.get(paneId);
|
|
1302
|
+
if (entry) entry.onConversationCallbacks.push(cb);
|
|
1303
|
+
}
|
|
1304
|
+
onActivity(paneId, cb) {
|
|
1305
|
+
const entry = this.entries.get(paneId);
|
|
1306
|
+
if (entry) entry.onActivityCallbacks.push(cb);
|
|
1307
|
+
}
|
|
1308
|
+
write(_paneId, _data) {
|
|
1309
|
+
}
|
|
1310
|
+
resize(_paneId, _cols, _rows) {
|
|
1311
|
+
}
|
|
1312
|
+
getScrollback(paneId) {
|
|
1313
|
+
const entry = this.entries.get(paneId);
|
|
1314
|
+
return entry ? entry.scrollback.join("") : "";
|
|
1315
|
+
}
|
|
1316
|
+
kill(paneId) {
|
|
1317
|
+
const entry = this.entries.get(paneId);
|
|
1318
|
+
if (!entry) return;
|
|
1319
|
+
entry.proc.kill();
|
|
1320
|
+
this.entries.delete(paneId);
|
|
1321
|
+
}
|
|
1322
|
+
killAll() {
|
|
1323
|
+
for (const paneId of this.entries.keys()) {
|
|
1324
|
+
this.kill(paneId);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
handleStdout(paneId, chunk) {
|
|
1328
|
+
const entry = this.entries.get(paneId);
|
|
1329
|
+
if (!entry) return;
|
|
1330
|
+
entry.stdoutBuffer += chunk;
|
|
1331
|
+
while (true) {
|
|
1332
|
+
const newline = entry.stdoutBuffer.indexOf("\n");
|
|
1333
|
+
if (newline === -1) break;
|
|
1334
|
+
const line = entry.stdoutBuffer.slice(0, newline).trim();
|
|
1335
|
+
entry.stdoutBuffer = entry.stdoutBuffer.slice(newline + 1);
|
|
1336
|
+
if (!line) continue;
|
|
1337
|
+
try {
|
|
1338
|
+
const message = JSON.parse(line);
|
|
1339
|
+
this.handleMessage(paneId, message);
|
|
1340
|
+
} catch {
|
|
1341
|
+
this.emitTerminal(paneId, line + "\n");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
handleMessage(paneId, message) {
|
|
1346
|
+
const entry = this.entries.get(paneId);
|
|
1347
|
+
if (!entry) return;
|
|
1348
|
+
if (typeof message.id === "number") {
|
|
1349
|
+
const pending = entry.pending.get(message.id);
|
|
1350
|
+
if (pending) {
|
|
1351
|
+
entry.pending.delete(message.id);
|
|
1352
|
+
if ("error" in message && message.error) {
|
|
1353
|
+
const err = message.error;
|
|
1354
|
+
pending.reject(new Error(err?.message || "ACP request failed"));
|
|
1355
|
+
} else {
|
|
1356
|
+
pending.resolve(message.result);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (message.method === "session/update") {
|
|
1362
|
+
const params = message.params || {};
|
|
1363
|
+
this.handleSessionUpdate(paneId, params);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
handleSessionUpdate(paneId, params) {
|
|
1368
|
+
const sessionId = this.extractSessionId(params);
|
|
1369
|
+
if (sessionId) {
|
|
1370
|
+
this.updateMeta(paneId, { sessionId });
|
|
1371
|
+
}
|
|
1372
|
+
const rawUpdate = params.update || params.delta || params.event || params;
|
|
1373
|
+
const updates = Array.isArray(rawUpdate) ? rawUpdate : [rawUpdate];
|
|
1374
|
+
for (const update of updates) {
|
|
1375
|
+
if (!update || typeof update !== "object") continue;
|
|
1376
|
+
const record = update;
|
|
1377
|
+
const kind = String(record.type || record.kind || "");
|
|
1378
|
+
const content = this.extractText(record);
|
|
1379
|
+
if (kind.includes("agent_message")) {
|
|
1380
|
+
const messageId = `assistant-${this.entries.get(paneId)?.nextMessageId ?? 1}`;
|
|
1381
|
+
this.emitConversation(paneId, {
|
|
1382
|
+
type: "message",
|
|
1383
|
+
messageId,
|
|
1384
|
+
role: "assistant",
|
|
1385
|
+
text: content,
|
|
1386
|
+
append: true
|
|
1387
|
+
});
|
|
1388
|
+
if (content) {
|
|
1389
|
+
this.emitTerminal(paneId, content);
|
|
1390
|
+
}
|
|
1391
|
+
this.setStatus(paneId, "running");
|
|
1392
|
+
} else if (kind.includes("tool_call")) {
|
|
1393
|
+
const toolCallId = `tool-${this.entries.get(paneId)?.nextToolId ?? 1}`;
|
|
1394
|
+
this.emitConversation(paneId, {
|
|
1395
|
+
type: "tool",
|
|
1396
|
+
toolCallId,
|
|
1397
|
+
title: String(record.title || record.name || "tool"),
|
|
1398
|
+
status: kind.includes("update") ? "in_progress" : "pending",
|
|
1399
|
+
text: content || void 0
|
|
1400
|
+
});
|
|
1401
|
+
if (content) {
|
|
1402
|
+
this.emitTerminal(paneId, `
|
|
1403
|
+
[tool] ${content}
|
|
1404
|
+
`);
|
|
1405
|
+
}
|
|
1406
|
+
} else if (kind.includes("turn") || kind.includes("done") || kind.includes("completed")) {
|
|
1407
|
+
this.setStatus(paneId, "idle");
|
|
1408
|
+
this.emitConversation(paneId, { type: "status", status: "idle" });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
extractText(record) {
|
|
1413
|
+
const direct = record.text || record.delta || record.content;
|
|
1414
|
+
if (typeof direct === "string") return direct;
|
|
1415
|
+
if (Array.isArray(direct)) {
|
|
1416
|
+
return direct.map((item) => {
|
|
1417
|
+
if (typeof item === "string") return item;
|
|
1418
|
+
if (item && typeof item === "object" && typeof item.text === "string") {
|
|
1419
|
+
return String(item.text);
|
|
1420
|
+
}
|
|
1421
|
+
return "";
|
|
1422
|
+
}).join("");
|
|
1423
|
+
}
|
|
1424
|
+
if (direct && typeof direct === "object" && typeof direct.text === "string") {
|
|
1425
|
+
return String(direct.text);
|
|
1426
|
+
}
|
|
1427
|
+
return "";
|
|
1428
|
+
}
|
|
1429
|
+
request(paneId, method, params) {
|
|
1430
|
+
const entry = this.entries.get(paneId);
|
|
1431
|
+
if (!entry) {
|
|
1432
|
+
return Promise.reject(new Error(`Missing ACP entry for ${paneId}`));
|
|
1433
|
+
}
|
|
1434
|
+
const id = entry.nextRequestId++;
|
|
1435
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
1436
|
+
entry.proc.stdin.write(payload);
|
|
1437
|
+
return new Promise((resolve, reject) => {
|
|
1438
|
+
entry.pending.set(id, { resolve, reject });
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
extractSessionId(result) {
|
|
1442
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1443
|
+
const record = result;
|
|
1444
|
+
if (typeof record.sessionId === "string") return record.sessionId;
|
|
1445
|
+
if (typeof record.session_id === "string") return record.session_id;
|
|
1446
|
+
if (record.session && typeof record.session === "object") {
|
|
1447
|
+
const nested = record.session;
|
|
1448
|
+
if (typeof nested.id === "string") return nested.id;
|
|
1449
|
+
if (typeof nested.sessionId === "string") return nested.sessionId;
|
|
1450
|
+
}
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
emitTerminal(paneId, data) {
|
|
1454
|
+
const entry = this.entries.get(paneId);
|
|
1455
|
+
if (!entry || !data) return;
|
|
1456
|
+
entry.scrollback.push(data);
|
|
1457
|
+
entry.scrollbackBytes += data.length;
|
|
1458
|
+
if (entry.scrollbackBytes > MAX_SCROLLBACK_BYTES2) {
|
|
1459
|
+
let bytesToRemove = entry.scrollbackBytes - MAX_SCROLLBACK_BYTES2;
|
|
1460
|
+
let removeCount = 0;
|
|
1461
|
+
while (removeCount < entry.scrollback.length - 1 && bytesToRemove > 0) {
|
|
1462
|
+
bytesToRemove -= entry.scrollback[removeCount].length;
|
|
1463
|
+
entry.scrollbackBytes -= entry.scrollback[removeCount].length;
|
|
1464
|
+
removeCount++;
|
|
1465
|
+
}
|
|
1466
|
+
if (removeCount > 0) entry.scrollback.splice(0, removeCount);
|
|
1467
|
+
}
|
|
1468
|
+
for (const cb of entry.onDataCallbacks) {
|
|
1469
|
+
cb(data);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
setStatus(paneId, status) {
|
|
1473
|
+
const entry = this.entries.get(paneId);
|
|
1474
|
+
if (!entry || entry.status === status) return;
|
|
1475
|
+
entry.status = status;
|
|
1476
|
+
for (const cb of entry.onStatusCallbacks) {
|
|
1477
|
+
cb(status);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
updateMeta(paneId, meta) {
|
|
1481
|
+
const entry = this.entries.get(paneId);
|
|
1482
|
+
if (!entry) return;
|
|
1483
|
+
entry.meta = { ...entry.meta, ...meta };
|
|
1484
|
+
for (const cb of entry.onMetaCallbacks) {
|
|
1485
|
+
cb(entry.meta);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
emitConversation(paneId, event) {
|
|
1489
|
+
const entry = this.entries.get(paneId);
|
|
1490
|
+
if (!entry) return;
|
|
1491
|
+
for (const cb of entry.onConversationCallbacks) {
|
|
1492
|
+
cb(event);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/git/WorktreeManager.ts
|
|
1498
|
+
import path4 from "path";
|
|
1499
|
+
import fs4 from "fs";
|
|
1110
1500
|
import { simpleGit } from "simple-git";
|
|
1111
1501
|
var WorktreeManager = class {
|
|
1112
1502
|
projectDir;
|
|
@@ -1124,15 +1514,15 @@ var WorktreeManager = class {
|
|
|
1124
1514
|
const baseBranch = await this.getCurrentBranch();
|
|
1125
1515
|
const slug = this.slugify(paneName);
|
|
1126
1516
|
const branch = `nexus/${paneId}-${slug}`;
|
|
1127
|
-
const worktreePath =
|
|
1128
|
-
if (
|
|
1517
|
+
const worktreePath = path4.join(this.projectDir, ".nexus", "worktrees", paneId);
|
|
1518
|
+
if (fs4.existsSync(worktreePath)) {
|
|
1129
1519
|
await this.forceRemoveWorktree(worktreePath);
|
|
1130
1520
|
}
|
|
1131
1521
|
try {
|
|
1132
1522
|
await this.git.raw(["branch", "-D", branch]);
|
|
1133
1523
|
} catch {
|
|
1134
1524
|
}
|
|
1135
|
-
|
|
1525
|
+
fs4.mkdirSync(path4.dirname(worktreePath), { recursive: true });
|
|
1136
1526
|
await this.git.raw(["worktree", "add", "-b", branch, worktreePath, baseBranch]);
|
|
1137
1527
|
const entry = { path: worktreePath, branch, baseBranch };
|
|
1138
1528
|
this.worktrees.set(paneId, entry);
|
|
@@ -1151,14 +1541,14 @@ var WorktreeManager = class {
|
|
|
1151
1541
|
} catch {
|
|
1152
1542
|
return false;
|
|
1153
1543
|
}
|
|
1154
|
-
if (
|
|
1544
|
+
if (fs4.existsSync(worktreePath)) {
|
|
1155
1545
|
this.worktrees.set(paneId, { path: worktreePath, branch, baseBranch });
|
|
1156
1546
|
return true;
|
|
1157
1547
|
}
|
|
1158
1548
|
try {
|
|
1159
1549
|
await this.git.raw(["worktree", "prune"]).catch(() => {
|
|
1160
1550
|
});
|
|
1161
|
-
|
|
1551
|
+
fs4.mkdirSync(path4.dirname(worktreePath), { recursive: true });
|
|
1162
1552
|
await this.git.raw(["worktree", "add", worktreePath, branch]);
|
|
1163
1553
|
this.worktrees.set(paneId, { path: worktreePath, branch, baseBranch });
|
|
1164
1554
|
return true;
|
|
@@ -1337,7 +1727,7 @@ var WorktreeManager = class {
|
|
|
1337
1727
|
await this.git.raw(["worktree", "remove", "--force", wtPath]);
|
|
1338
1728
|
} catch {
|
|
1339
1729
|
try {
|
|
1340
|
-
|
|
1730
|
+
fs4.rmSync(wtPath, { recursive: true, force: true });
|
|
1341
1731
|
await this.git.raw(["worktree", "prune"]);
|
|
1342
1732
|
} catch {
|
|
1343
1733
|
}
|
|
@@ -1357,7 +1747,7 @@ var WorktreeManager = class {
|
|
|
1357
1747
|
};
|
|
1358
1748
|
|
|
1359
1749
|
// src/git/GitService.ts
|
|
1360
|
-
import
|
|
1750
|
+
import path5 from "path";
|
|
1361
1751
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
1362
1752
|
import { watch } from "chokidar";
|
|
1363
1753
|
var GitService = class {
|
|
@@ -1383,11 +1773,11 @@ var GitService = class {
|
|
|
1383
1773
|
this.refresh();
|
|
1384
1774
|
}, 1e3);
|
|
1385
1775
|
};
|
|
1386
|
-
const gitDir =
|
|
1776
|
+
const gitDir = path5.join(this.projectDir, ".git");
|
|
1387
1777
|
this.gitWatcher = watch([
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1778
|
+
path5.join(gitDir, "index"),
|
|
1779
|
+
path5.join(gitDir, "HEAD"),
|
|
1780
|
+
path5.join(gitDir, "refs")
|
|
1391
1781
|
], {
|
|
1392
1782
|
persistent: true,
|
|
1393
1783
|
ignoreInitial: true
|
|
@@ -1401,7 +1791,7 @@ var GitService = class {
|
|
|
1401
1791
|
};
|
|
1402
1792
|
this.workWatcher = watch(this.projectDir, {
|
|
1403
1793
|
ignored: (filePath) => {
|
|
1404
|
-
const basename =
|
|
1794
|
+
const basename = path5.basename(filePath);
|
|
1405
1795
|
return basename === ".git" || basename === "node_modules" || basename === ".nexus" || basename === "dist";
|
|
1406
1796
|
},
|
|
1407
1797
|
persistent: true,
|
|
@@ -1412,10 +1802,18 @@ var GitService = class {
|
|
|
1412
1802
|
}
|
|
1413
1803
|
async refresh() {
|
|
1414
1804
|
try {
|
|
1415
|
-
const result = await
|
|
1805
|
+
const result = await Promise.race([
|
|
1806
|
+
this.getDiffs(),
|
|
1807
|
+
new Promise(
|
|
1808
|
+
(_, reject) => setTimeout(() => reject(new Error("git diff timeout")), 15e3)
|
|
1809
|
+
)
|
|
1810
|
+
]);
|
|
1416
1811
|
this.currentResult = result;
|
|
1417
1812
|
this.notifyListeners();
|
|
1418
|
-
} catch {
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
if (err.message === "git diff timeout") {
|
|
1815
|
+
console.warn("[GitService] git diff timed out (15s), using cached result");
|
|
1816
|
+
}
|
|
1419
1817
|
}
|
|
1420
1818
|
}
|
|
1421
1819
|
getCurrentDiffs() {
|
|
@@ -1447,10 +1845,10 @@ var GitService = class {
|
|
|
1447
1845
|
const status = await this.git.status();
|
|
1448
1846
|
const isUntracked = status.not_added.includes(file) || status.created.includes(file);
|
|
1449
1847
|
if (isUntracked) {
|
|
1450
|
-
const fullPath =
|
|
1451
|
-
const
|
|
1452
|
-
if (
|
|
1453
|
-
|
|
1848
|
+
const fullPath = path5.join(this.projectDir, file);
|
|
1849
|
+
const fs11 = await import("fs");
|
|
1850
|
+
if (fs11.existsSync(fullPath)) {
|
|
1851
|
+
fs11.unlinkSync(fullPath);
|
|
1454
1852
|
}
|
|
1455
1853
|
} else {
|
|
1456
1854
|
await this.git.checkout(["--", file]);
|
|
@@ -1591,15 +1989,19 @@ function nextPaneId() {
|
|
|
1591
1989
|
var WorkspaceManager = class {
|
|
1592
1990
|
panes = /* @__PURE__ */ new Map();
|
|
1593
1991
|
ptyManager;
|
|
1992
|
+
acpRuntime;
|
|
1594
1993
|
configManager;
|
|
1595
1994
|
worktreeManager;
|
|
1596
1995
|
perPaneGitServices = /* @__PURE__ */ new Map();
|
|
1597
1996
|
wsName = "";
|
|
1598
1997
|
wsDescription = "";
|
|
1998
|
+
// Serialize config writes to prevent race conditions when closing multiple panes
|
|
1999
|
+
configWriteLock = Promise.resolve();
|
|
1599
2000
|
// Multi-client event listener sets
|
|
1600
2001
|
listeners = {
|
|
1601
2002
|
onPaneAdded: /* @__PURE__ */ new Set(),
|
|
1602
2003
|
onPaneRemoved: /* @__PURE__ */ new Set(),
|
|
2004
|
+
onConversationEvent: /* @__PURE__ */ new Set(),
|
|
1603
2005
|
onPaneStatus: /* @__PURE__ */ new Set(),
|
|
1604
2006
|
onPaneMeta: /* @__PURE__ */ new Set(),
|
|
1605
2007
|
onTerminalData: /* @__PURE__ */ new Set(),
|
|
@@ -1612,6 +2014,7 @@ var WorkspaceManager = class {
|
|
|
1612
2014
|
constructor(configManager) {
|
|
1613
2015
|
this.configManager = configManager;
|
|
1614
2016
|
this.ptyManager = new PtyManager(configManager);
|
|
2017
|
+
this.acpRuntime = new AcpRuntime(configManager);
|
|
1615
2018
|
this.worktreeManager = new WorktreeManager(configManager.getProjectDir());
|
|
1616
2019
|
}
|
|
1617
2020
|
async init() {
|
|
@@ -1712,7 +2115,11 @@ var WorkspaceManager = class {
|
|
|
1712
2115
|
}
|
|
1713
2116
|
async closePane(paneId) {
|
|
1714
2117
|
const pane = this.panes.get(paneId);
|
|
1715
|
-
|
|
2118
|
+
if (pane?.runtime === "acp") {
|
|
2119
|
+
this.acpRuntime.kill(paneId);
|
|
2120
|
+
} else {
|
|
2121
|
+
this.ptyManager.kill(paneId);
|
|
2122
|
+
}
|
|
1716
2123
|
if (pane?.isolation === "worktree") {
|
|
1717
2124
|
this.stopPaneGitService(paneId);
|
|
1718
2125
|
await this.worktreeManager.remove(paneId);
|
|
@@ -1724,7 +2131,11 @@ var WorkspaceManager = class {
|
|
|
1724
2131
|
restartPane(paneId, mode, sessionId) {
|
|
1725
2132
|
const existingState = this.panes.get(paneId);
|
|
1726
2133
|
if (!existingState) return;
|
|
1727
|
-
|
|
2134
|
+
if (existingState.runtime === "acp") {
|
|
2135
|
+
this.acpRuntime.kill(paneId);
|
|
2136
|
+
} else {
|
|
2137
|
+
this.ptyManager.kill(paneId);
|
|
2138
|
+
}
|
|
1728
2139
|
const resolvedSessionId = mode === "resume" ? sessionId || existingState.sessionId || existingState.meta.sessionId : void 0;
|
|
1729
2140
|
const config = {
|
|
1730
2141
|
id: paneId,
|
|
@@ -1765,13 +2176,36 @@ var WorkspaceManager = class {
|
|
|
1765
2176
|
return result;
|
|
1766
2177
|
}
|
|
1767
2178
|
writeToPane(paneId, data) {
|
|
2179
|
+
const pane = this.panes.get(paneId);
|
|
2180
|
+
if (!pane) return;
|
|
2181
|
+
if (pane.runtime === "acp") {
|
|
2182
|
+
this.acpRuntime.write(paneId, data);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
1768
2185
|
this.ptyManager.write(paneId, data);
|
|
1769
2186
|
}
|
|
2187
|
+
sendConversationToPane(paneId, text) {
|
|
2188
|
+
const pane = this.panes.get(paneId);
|
|
2189
|
+
if (!pane) return Promise.resolve();
|
|
2190
|
+
if (pane.runtime === "acp") {
|
|
2191
|
+
return this.acpRuntime.sendPrompt(paneId, text);
|
|
2192
|
+
}
|
|
2193
|
+
this.ptyManager.write(paneId, text + "\r");
|
|
2194
|
+
return Promise.resolve();
|
|
2195
|
+
}
|
|
1770
2196
|
resizePane(paneId, cols, rows) {
|
|
2197
|
+
const pane = this.panes.get(paneId);
|
|
2198
|
+
if (!pane) return;
|
|
2199
|
+
if (pane.runtime === "acp") {
|
|
2200
|
+
this.acpRuntime.resize(paneId, cols, rows);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
1771
2203
|
this.ptyManager.resize(paneId, cols, rows);
|
|
1772
2204
|
}
|
|
1773
2205
|
getScrollback(paneId) {
|
|
1774
|
-
|
|
2206
|
+
const pane = this.panes.get(paneId);
|
|
2207
|
+
if (!pane) return "";
|
|
2208
|
+
return pane.runtime === "acp" ? this.acpRuntime.getScrollback(paneId) : this.ptyManager.getScrollback(paneId);
|
|
1775
2209
|
}
|
|
1776
2210
|
// ─── Event Registration (multi-client safe) ────────────────
|
|
1777
2211
|
/**
|
|
@@ -1828,7 +2262,8 @@ var WorkspaceManager = class {
|
|
|
1828
2262
|
}
|
|
1829
2263
|
}
|
|
1830
2264
|
spawnPane(config, cols, rows) {
|
|
1831
|
-
const
|
|
2265
|
+
const runtime = this.resolveRuntime(config.agent);
|
|
2266
|
+
const pid = runtime === "acp" ? this.acpRuntime.spawn(config.id, config) : this.ptyManager.spawn(config.id, config, cols || 80, rows || 24);
|
|
1832
2267
|
const pane = {
|
|
1833
2268
|
id: config.id,
|
|
1834
2269
|
name: config.name,
|
|
@@ -1841,6 +2276,7 @@ var WorkspaceManager = class {
|
|
|
1841
2276
|
branch: config.branch,
|
|
1842
2277
|
worktreePath: config.worktreePath,
|
|
1843
2278
|
sessionId: config.sessionId,
|
|
2279
|
+
runtime,
|
|
1844
2280
|
status: "running",
|
|
1845
2281
|
pid,
|
|
1846
2282
|
meta: {},
|
|
@@ -1848,17 +2284,18 @@ var WorkspaceManager = class {
|
|
|
1848
2284
|
};
|
|
1849
2285
|
this.panes.set(config.id, pane);
|
|
1850
2286
|
this.emit("onPaneAdded", pane);
|
|
1851
|
-
this.
|
|
2287
|
+
const runtimeAdapter = runtime === "acp" ? this.acpRuntime : this.ptyManager;
|
|
2288
|
+
runtimeAdapter.onData(config.id, (data) => {
|
|
1852
2289
|
this.emit("onTerminalData", config.id, data);
|
|
1853
2290
|
});
|
|
1854
|
-
|
|
2291
|
+
runtimeAdapter.onStatus(config.id, (status) => {
|
|
1855
2292
|
const p = this.panes.get(config.id);
|
|
1856
2293
|
if (p) {
|
|
1857
2294
|
p.status = status;
|
|
1858
2295
|
this.emit("onPaneStatus", config.id, status);
|
|
1859
2296
|
}
|
|
1860
2297
|
});
|
|
1861
|
-
|
|
2298
|
+
runtimeAdapter.onMeta(config.id, (meta) => {
|
|
1862
2299
|
const p = this.panes.get(config.id);
|
|
1863
2300
|
if (p) {
|
|
1864
2301
|
p.meta = meta;
|
|
@@ -1869,11 +2306,21 @@ var WorkspaceManager = class {
|
|
|
1869
2306
|
this.emit("onPaneMeta", config.id, meta);
|
|
1870
2307
|
}
|
|
1871
2308
|
});
|
|
1872
|
-
|
|
2309
|
+
runtimeAdapter.onActivity(config.id, (activity) => {
|
|
1873
2310
|
this.emit("onPaneActivity", config.id, activity);
|
|
1874
2311
|
});
|
|
2312
|
+
if (runtime === "acp") {
|
|
2313
|
+
this.acpRuntime.onConversation(config.id, (event) => {
|
|
2314
|
+
this.emit("onConversationEvent", config.id, event);
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
1875
2317
|
return pane;
|
|
1876
2318
|
}
|
|
2319
|
+
resolveRuntime(agentType) {
|
|
2320
|
+
if (agentType === "__shell__") return "pty";
|
|
2321
|
+
const def = this.configManager.getAgentDefinition(agentType);
|
|
2322
|
+
return def?.transport || "pty";
|
|
2323
|
+
}
|
|
1877
2324
|
async startPaneGitService(paneId, worktreePath) {
|
|
1878
2325
|
const gitService = new GitService(worktreePath);
|
|
1879
2326
|
gitService.onDiffChange((result) => {
|
|
@@ -1891,21 +2338,25 @@ var WorkspaceManager = class {
|
|
|
1891
2338
|
}
|
|
1892
2339
|
}
|
|
1893
2340
|
persistPaneConfig(config) {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
wsConfig
|
|
1897
|
-
|
|
1898
|
-
|
|
2341
|
+
this.serializedConfigWrite(() => {
|
|
2342
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2343
|
+
if (wsConfig) {
|
|
2344
|
+
wsConfig.panes.push(config);
|
|
2345
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
1899
2348
|
}
|
|
1900
2349
|
updatePaneConfigSessionId(paneId, sessionId) {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
paneConfig
|
|
1906
|
-
|
|
2350
|
+
this.serializedConfigWrite(() => {
|
|
2351
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2352
|
+
if (wsConfig) {
|
|
2353
|
+
const paneConfig = wsConfig.panes.find((p) => p.id === paneId);
|
|
2354
|
+
if (paneConfig) {
|
|
2355
|
+
paneConfig.sessionId = sessionId;
|
|
2356
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2357
|
+
}
|
|
1907
2358
|
}
|
|
1908
|
-
}
|
|
2359
|
+
});
|
|
1909
2360
|
}
|
|
1910
2361
|
getSessionList(paneId) {
|
|
1911
2362
|
const sessions = [];
|
|
@@ -1943,14 +2394,30 @@ var WorkspaceManager = class {
|
|
|
1943
2394
|
return sessions;
|
|
1944
2395
|
}
|
|
1945
2396
|
removePaneFromConfig(paneId) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2397
|
+
this.serializedConfigWrite(() => {
|
|
2398
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2399
|
+
if (wsConfig) {
|
|
2400
|
+
wsConfig.panes = wsConfig.panes.filter((p) => p.id !== paneId);
|
|
2401
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Serialize config file writes to prevent race conditions
|
|
2407
|
+
* when multiple panes are created/closed simultaneously.
|
|
2408
|
+
*/
|
|
2409
|
+
serializedConfigWrite(fn) {
|
|
2410
|
+
this.configWriteLock = this.configWriteLock.then(() => {
|
|
2411
|
+
try {
|
|
2412
|
+
fn();
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
console.error("[WorkspaceManager] Config write failed:", err);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
1951
2417
|
}
|
|
1952
2418
|
async shutdown() {
|
|
1953
2419
|
this.ptyManager.killAll();
|
|
2420
|
+
this.acpRuntime.killAll();
|
|
1954
2421
|
for (const [paneId] of this.perPaneGitServices) {
|
|
1955
2422
|
this.stopPaneGitService(paneId);
|
|
1956
2423
|
}
|
|
@@ -1958,8 +2425,8 @@ var WorkspaceManager = class {
|
|
|
1958
2425
|
};
|
|
1959
2426
|
|
|
1960
2427
|
// src/workspace/AgentsYamlWriter.ts
|
|
1961
|
-
import
|
|
1962
|
-
import
|
|
2428
|
+
import fs5 from "fs";
|
|
2429
|
+
import path6 from "path";
|
|
1963
2430
|
import yaml2 from "js-yaml";
|
|
1964
2431
|
var AgentsYamlWriter = class {
|
|
1965
2432
|
projectDir;
|
|
@@ -1984,8 +2451,8 @@ var AgentsYamlWriter = class {
|
|
|
1984
2451
|
this.writeFile(panes);
|
|
1985
2452
|
}
|
|
1986
2453
|
writeFile(panes) {
|
|
1987
|
-
const nexusDir =
|
|
1988
|
-
|
|
2454
|
+
const nexusDir = path6.join(this.projectDir, ".nexus");
|
|
2455
|
+
fs5.mkdirSync(nexusDir, { recursive: true });
|
|
1989
2456
|
const visible = panes.filter((p) => p.agent !== "__shell__");
|
|
1990
2457
|
const data = {
|
|
1991
2458
|
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1995,11 +2462,12 @@ var AgentsYamlWriter = class {
|
|
|
1995
2462
|
id: p.id,
|
|
1996
2463
|
name: p.name,
|
|
1997
2464
|
agent: p.agent,
|
|
2465
|
+
runtime: p.runtime,
|
|
1998
2466
|
pid: p.pid,
|
|
1999
2467
|
status: p.status,
|
|
2000
2468
|
isolation: p.isolation || "shared",
|
|
2001
2469
|
branch: p.branch || void 0,
|
|
2002
|
-
workdir: p.workdir ?
|
|
2470
|
+
workdir: p.workdir ? path6.resolve(basePath, p.workdir) : basePath,
|
|
2003
2471
|
task: p.task || void 0,
|
|
2004
2472
|
model: p.meta.model || void 0,
|
|
2005
2473
|
context_used_pct: p.meta.contextUsedPct ?? void 0,
|
|
@@ -2009,18 +2477,19 @@ var AgentsYamlWriter = class {
|
|
|
2009
2477
|
};
|
|
2010
2478
|
})
|
|
2011
2479
|
};
|
|
2012
|
-
const filePath =
|
|
2013
|
-
|
|
2480
|
+
const filePath = path6.join(nexusDir, "agents.yaml");
|
|
2481
|
+
fs5.writeFileSync(filePath, yaml2.dump(data, { lineWidth: -1 }));
|
|
2014
2482
|
}
|
|
2015
2483
|
};
|
|
2016
2484
|
|
|
2017
2485
|
// src/fs/FsWatcher.ts
|
|
2018
|
-
import
|
|
2019
|
-
import
|
|
2486
|
+
import fs6 from "fs";
|
|
2487
|
+
import path7 from "path";
|
|
2020
2488
|
import { watch as watch2 } from "chokidar";
|
|
2021
2489
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2022
2490
|
"node_modules",
|
|
2023
2491
|
".git",
|
|
2492
|
+
".nexus",
|
|
2024
2493
|
"dist",
|
|
2025
2494
|
".cache",
|
|
2026
2495
|
".turbo",
|
|
@@ -2047,7 +2516,7 @@ var FsWatcher = class {
|
|
|
2047
2516
|
this.notifyListeners();
|
|
2048
2517
|
this.watcher = watch2(this.projectDir, {
|
|
2049
2518
|
ignored: (filePath) => {
|
|
2050
|
-
const basename =
|
|
2519
|
+
const basename = path7.basename(filePath);
|
|
2051
2520
|
return IGNORED_DIRS.has(basename) || IGNORED_FILES.has(basename);
|
|
2052
2521
|
},
|
|
2053
2522
|
persistent: true,
|
|
@@ -2064,9 +2533,9 @@ var FsWatcher = class {
|
|
|
2064
2533
|
}, 300);
|
|
2065
2534
|
};
|
|
2066
2535
|
const emitFileChange = (eventType, filePath) => {
|
|
2067
|
-
const relativePath =
|
|
2536
|
+
const relativePath = path7.relative(this.projectDir, filePath);
|
|
2068
2537
|
if (!relativePath || relativePath.startsWith("..")) return;
|
|
2069
|
-
if (!/\.\w{1,10}$/.test(
|
|
2538
|
+
if (!/\.\w{1,10}$/.test(path7.basename(filePath))) return;
|
|
2070
2539
|
const now = Date.now();
|
|
2071
2540
|
const lastChange = this.recentChanges.get(relativePath);
|
|
2072
2541
|
if (lastChange && now - lastChange < 1e3) return;
|
|
@@ -2135,14 +2604,14 @@ var FsWatcher = class {
|
|
|
2135
2604
|
return parts.join("\n");
|
|
2136
2605
|
}
|
|
2137
2606
|
buildTree(dirPath, depth) {
|
|
2138
|
-
if (depth >
|
|
2607
|
+
if (depth > 5) return [];
|
|
2139
2608
|
try {
|
|
2140
|
-
const entries =
|
|
2609
|
+
const entries = fs6.readdirSync(dirPath, { withFileTypes: true });
|
|
2141
2610
|
const nodes = [];
|
|
2142
2611
|
for (const entry of entries) {
|
|
2143
2612
|
if (IGNORED_DIRS.has(entry.name) || IGNORED_FILES.has(entry.name)) continue;
|
|
2144
|
-
const fullPath =
|
|
2145
|
-
const relativePath =
|
|
2613
|
+
const fullPath = path7.join(dirPath, entry.name);
|
|
2614
|
+
const relativePath = path7.relative(this.projectDir, fullPath);
|
|
2146
2615
|
if (entry.isDirectory()) {
|
|
2147
2616
|
nodes.push({
|
|
2148
2617
|
name: entry.name,
|
|
@@ -2181,7 +2650,7 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2181
2650
|
type: "workspace.state",
|
|
2182
2651
|
state
|
|
2183
2652
|
});
|
|
2184
|
-
const SCROLLBACK_CHUNK_SIZE =
|
|
2653
|
+
const SCROLLBACK_CHUNK_SIZE = 512 * 1024;
|
|
2185
2654
|
const replayScrollback = async () => {
|
|
2186
2655
|
for (const pane of state.panes) {
|
|
2187
2656
|
const scrollback = workspaceManager.getScrollback(pane.id);
|
|
@@ -2211,6 +2680,9 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2211
2680
|
onTerminalData: (paneId, data) => {
|
|
2212
2681
|
send({ type: "terminal.output", paneId, data });
|
|
2213
2682
|
},
|
|
2683
|
+
onConversationEvent: (paneId, event) => {
|
|
2684
|
+
send({ type: "conversation.event", paneId, event });
|
|
2685
|
+
},
|
|
2214
2686
|
onPaneStatus: (paneId, status) => {
|
|
2215
2687
|
send({ type: "pane.status", paneId, status });
|
|
2216
2688
|
},
|
|
@@ -2248,10 +2720,21 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2248
2720
|
}
|
|
2249
2721
|
switch (event.type) {
|
|
2250
2722
|
case "terminal.input":
|
|
2251
|
-
|
|
2723
|
+
try {
|
|
2724
|
+
workspaceManager.writeToPane(event.paneId, event.data);
|
|
2725
|
+
} catch {
|
|
2726
|
+
}
|
|
2252
2727
|
break;
|
|
2253
2728
|
case "terminal.resize":
|
|
2254
|
-
|
|
2729
|
+
try {
|
|
2730
|
+
workspaceManager.resizePane(event.paneId, event.cols, event.rows);
|
|
2731
|
+
} catch {
|
|
2732
|
+
}
|
|
2733
|
+
break;
|
|
2734
|
+
case "conversation.send":
|
|
2735
|
+
workspaceManager.sendConversationToPane(event.paneId, event.text).catch((err) => {
|
|
2736
|
+
console.error("conversation.send failed:", err);
|
|
2737
|
+
});
|
|
2255
2738
|
break;
|
|
2256
2739
|
case "pane.create":
|
|
2257
2740
|
workspaceManager.createPane(event.config).catch((err) => {
|
|
@@ -2372,8 +2855,8 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2372
2855
|
}
|
|
2373
2856
|
|
|
2374
2857
|
// src/deps/DependencyAnalyzer.ts
|
|
2375
|
-
import
|
|
2376
|
-
import
|
|
2858
|
+
import fs7 from "fs";
|
|
2859
|
+
import path8 from "path";
|
|
2377
2860
|
var IMPORT_FROM_RE = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
2378
2861
|
var EXPORT_FROM_RE = /export\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
2379
2862
|
var REQUIRE_RE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2394,7 +2877,7 @@ var DependencyAnalyzer = class {
|
|
|
2394
2877
|
const files = this.collectFiles(this.projectDir);
|
|
2395
2878
|
const nodes = [];
|
|
2396
2879
|
for (const absPath of files) {
|
|
2397
|
-
const relPath =
|
|
2880
|
+
const relPath = path8.relative(this.projectDir, absPath);
|
|
2398
2881
|
const imports = this.extractImports(absPath, relPath);
|
|
2399
2882
|
nodes.push({ id: relPath, imports });
|
|
2400
2883
|
}
|
|
@@ -2405,18 +2888,18 @@ var DependencyAnalyzer = class {
|
|
|
2405
2888
|
const files = [];
|
|
2406
2889
|
let entries;
|
|
2407
2890
|
try {
|
|
2408
|
-
entries =
|
|
2891
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2409
2892
|
} catch {
|
|
2410
2893
|
return files;
|
|
2411
2894
|
}
|
|
2412
2895
|
for (const entry of entries) {
|
|
2413
2896
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
2414
|
-
const fullPath =
|
|
2897
|
+
const fullPath = path8.join(dir, entry.name);
|
|
2415
2898
|
if (entry.isDirectory()) {
|
|
2416
2899
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2417
2900
|
files.push(...this.collectFiles(fullPath, depth + 1));
|
|
2418
2901
|
} else if (entry.isFile()) {
|
|
2419
|
-
const ext =
|
|
2902
|
+
const ext = path8.extname(entry.name);
|
|
2420
2903
|
if (JS_TS_EXTENSIONS.has(ext)) {
|
|
2421
2904
|
files.push(fullPath);
|
|
2422
2905
|
}
|
|
@@ -2427,7 +2910,7 @@ var DependencyAnalyzer = class {
|
|
|
2427
2910
|
extractImports(absPath, relPath) {
|
|
2428
2911
|
let content;
|
|
2429
2912
|
try {
|
|
2430
|
-
content =
|
|
2913
|
+
content = fs7.readFileSync(absPath, "utf-8");
|
|
2431
2914
|
} catch {
|
|
2432
2915
|
return [];
|
|
2433
2916
|
}
|
|
@@ -2444,22 +2927,22 @@ var DependencyAnalyzer = class {
|
|
|
2444
2927
|
}
|
|
2445
2928
|
}
|
|
2446
2929
|
const imports = [];
|
|
2447
|
-
const fileDir =
|
|
2930
|
+
const fileDir = path8.dirname(absPath);
|
|
2448
2931
|
for (const spec of specifiers) {
|
|
2449
2932
|
const resolved = this.resolveSpecifier(fileDir, spec);
|
|
2450
2933
|
if (resolved) {
|
|
2451
|
-
const resolvedRel =
|
|
2934
|
+
const resolvedRel = path8.relative(this.projectDir, resolved);
|
|
2452
2935
|
imports.push(resolvedRel);
|
|
2453
2936
|
}
|
|
2454
2937
|
}
|
|
2455
2938
|
return imports;
|
|
2456
2939
|
}
|
|
2457
2940
|
resolveSpecifier(fromDir, specifier) {
|
|
2458
|
-
const base =
|
|
2941
|
+
const base = path8.resolve(fromDir, specifier);
|
|
2459
2942
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
2460
2943
|
const candidate = base + ext;
|
|
2461
2944
|
try {
|
|
2462
|
-
if (
|
|
2945
|
+
if (fs7.statSync(candidate).isFile()) {
|
|
2463
2946
|
return candidate;
|
|
2464
2947
|
}
|
|
2465
2948
|
} catch {
|
|
@@ -2470,8 +2953,8 @@ var DependencyAnalyzer = class {
|
|
|
2470
2953
|
};
|
|
2471
2954
|
|
|
2472
2955
|
// src/history/SessionRecorder.ts
|
|
2473
|
-
import
|
|
2474
|
-
import
|
|
2956
|
+
import fs8 from "fs";
|
|
2957
|
+
import path9 from "path";
|
|
2475
2958
|
import { execFile as execFile2 } from "child_process";
|
|
2476
2959
|
var HISTORY_DIR = ".nexus/history";
|
|
2477
2960
|
var SESSIONS_INDEX = "sessions.json";
|
|
@@ -2489,8 +2972,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2489
2972
|
constructor(projectDir, projectName, retentionDays = 30) {
|
|
2490
2973
|
this.projectDir = projectDir;
|
|
2491
2974
|
const sessionId = `s-${Date.now()}`;
|
|
2492
|
-
this.sessionDir =
|
|
2493
|
-
|
|
2975
|
+
this.sessionDir = path9.join(projectDir, HISTORY_DIR, sessionId);
|
|
2976
|
+
fs8.mkdirSync(this.sessionDir, { recursive: true });
|
|
2494
2977
|
this.retentionDays = retentionDays;
|
|
2495
2978
|
this.session = {
|
|
2496
2979
|
id: sessionId,
|
|
@@ -2630,47 +3113,47 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2630
3113
|
}
|
|
2631
3114
|
// ─── Query API ────────────────────────────────────────────
|
|
2632
3115
|
static listSessions(projectDir) {
|
|
2633
|
-
const indexPath =
|
|
2634
|
-
if (!
|
|
3116
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3117
|
+
if (!fs8.existsSync(indexPath)) return [];
|
|
2635
3118
|
try {
|
|
2636
|
-
const data = JSON.parse(
|
|
3119
|
+
const data = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2637
3120
|
return Array.isArray(data) ? data : [];
|
|
2638
3121
|
} catch {
|
|
2639
3122
|
return [];
|
|
2640
3123
|
}
|
|
2641
3124
|
}
|
|
2642
3125
|
static getSession(projectDir, sessionId) {
|
|
2643
|
-
const sessionPath =
|
|
2644
|
-
if (!
|
|
3126
|
+
const sessionPath = path9.join(projectDir, HISTORY_DIR, sessionId, "session.json");
|
|
3127
|
+
if (!fs8.existsSync(sessionPath)) return null;
|
|
2645
3128
|
try {
|
|
2646
|
-
return JSON.parse(
|
|
3129
|
+
return JSON.parse(fs8.readFileSync(sessionPath, "utf-8"));
|
|
2647
3130
|
} catch {
|
|
2648
3131
|
return null;
|
|
2649
3132
|
}
|
|
2650
3133
|
}
|
|
2651
3134
|
static getTurn(projectDir, sessionId, turnId) {
|
|
2652
|
-
const turnPath =
|
|
2653
|
-
if (!
|
|
3135
|
+
const turnPath = path9.join(projectDir, HISTORY_DIR, sessionId, `${turnId}.json`);
|
|
3136
|
+
if (!fs8.existsSync(turnPath)) return null;
|
|
2654
3137
|
try {
|
|
2655
|
-
return JSON.parse(
|
|
3138
|
+
return JSON.parse(fs8.readFileSync(turnPath, "utf-8"));
|
|
2656
3139
|
} catch {
|
|
2657
3140
|
return null;
|
|
2658
3141
|
}
|
|
2659
3142
|
}
|
|
2660
3143
|
/** Delete a single session and its directory. Returns true if deleted. */
|
|
2661
3144
|
static deleteSession(projectDir, sessionId) {
|
|
2662
|
-
const sessionDir =
|
|
2663
|
-
if (
|
|
2664
|
-
|
|
3145
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, sessionId);
|
|
3146
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3147
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2665
3148
|
}
|
|
2666
|
-
const indexPath =
|
|
2667
|
-
if (
|
|
3149
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3150
|
+
if (fs8.existsSync(indexPath)) {
|
|
2668
3151
|
try {
|
|
2669
|
-
let sessions = JSON.parse(
|
|
3152
|
+
let sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2670
3153
|
const before = sessions.length;
|
|
2671
3154
|
sessions = sessions.filter((s) => s.id !== sessionId);
|
|
2672
3155
|
if (sessions.length < before) {
|
|
2673
|
-
|
|
3156
|
+
fs8.writeFileSync(indexPath, JSON.stringify(sessions, null, 2));
|
|
2674
3157
|
return true;
|
|
2675
3158
|
}
|
|
2676
3159
|
} catch {
|
|
@@ -2683,14 +3166,14 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2683
3166
|
const sessions = _SessionRecorder.listSessions(projectDir);
|
|
2684
3167
|
let count = 0;
|
|
2685
3168
|
for (const session of sessions) {
|
|
2686
|
-
const sessionDir =
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
3169
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, session.id);
|
|
3170
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3171
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2689
3172
|
count++;
|
|
2690
3173
|
}
|
|
2691
3174
|
}
|
|
2692
|
-
const indexPath =
|
|
2693
|
-
|
|
3175
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3176
|
+
fs8.writeFileSync(indexPath, JSON.stringify([], null, 2));
|
|
2694
3177
|
return count;
|
|
2695
3178
|
}
|
|
2696
3179
|
/**
|
|
@@ -2698,11 +3181,11 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2698
3181
|
* Called automatically when saving a new session.
|
|
2699
3182
|
*/
|
|
2700
3183
|
static pruneOldSessions(projectDir, retentionDays) {
|
|
2701
|
-
const indexPath =
|
|
2702
|
-
if (!
|
|
3184
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3185
|
+
if (!fs8.existsSync(indexPath)) return 0;
|
|
2703
3186
|
let sessions;
|
|
2704
3187
|
try {
|
|
2705
|
-
sessions = JSON.parse(
|
|
3188
|
+
sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2706
3189
|
} catch {
|
|
2707
3190
|
return 0;
|
|
2708
3191
|
}
|
|
@@ -2718,12 +3201,12 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2718
3201
|
}
|
|
2719
3202
|
if (remove.length === 0) return 0;
|
|
2720
3203
|
for (const s of remove) {
|
|
2721
|
-
const sessionDir =
|
|
2722
|
-
if (
|
|
2723
|
-
|
|
3204
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, s.id);
|
|
3205
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3206
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2724
3207
|
}
|
|
2725
3208
|
}
|
|
2726
|
-
|
|
3209
|
+
fs8.writeFileSync(indexPath, JSON.stringify(keep, null, 2));
|
|
2727
3210
|
return remove.length;
|
|
2728
3211
|
}
|
|
2729
3212
|
// ─── Internal ─────────────────────────────────────────────
|
|
@@ -2830,19 +3313,19 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2830
3313
|
});
|
|
2831
3314
|
}
|
|
2832
3315
|
writeTurnFile(turn) {
|
|
2833
|
-
const turnPath =
|
|
2834
|
-
|
|
3316
|
+
const turnPath = path9.join(this.sessionDir, `${turn.id}.json`);
|
|
3317
|
+
fs8.writeFileSync(turnPath, JSON.stringify(turn));
|
|
2835
3318
|
}
|
|
2836
3319
|
writeSessionFile() {
|
|
2837
|
-
const sessionPath =
|
|
2838
|
-
|
|
3320
|
+
const sessionPath = path9.join(this.sessionDir, "session.json");
|
|
3321
|
+
fs8.writeFileSync(sessionPath, JSON.stringify(this.session, null, 2));
|
|
2839
3322
|
}
|
|
2840
3323
|
updateSessionsIndex() {
|
|
2841
|
-
const indexPath =
|
|
3324
|
+
const indexPath = path9.join(this.projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
2842
3325
|
let sessions = [];
|
|
2843
|
-
if (
|
|
3326
|
+
if (fs8.existsSync(indexPath)) {
|
|
2844
3327
|
try {
|
|
2845
|
-
sessions = JSON.parse(
|
|
3328
|
+
sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2846
3329
|
} catch {
|
|
2847
3330
|
}
|
|
2848
3331
|
}
|
|
@@ -2856,7 +3339,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2856
3339
|
paneCount: this.session.panes.length,
|
|
2857
3340
|
totalDurationMs
|
|
2858
3341
|
});
|
|
2859
|
-
|
|
3342
|
+
fs8.writeFileSync(indexPath, JSON.stringify(sessions, null, 2));
|
|
2860
3343
|
const pruned = _SessionRecorder.pruneOldSessions(this.projectDir, this.retentionDays);
|
|
2861
3344
|
if (pruned > 0) {
|
|
2862
3345
|
console.log(`[SessionRecorder] Pruned ${pruned} old session(s)`);
|
|
@@ -2915,7 +3398,7 @@ var SessionDiscovery = class {
|
|
|
2915
3398
|
};
|
|
2916
3399
|
|
|
2917
3400
|
// src/index.ts
|
|
2918
|
-
var __dirname =
|
|
3401
|
+
var __dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
2919
3402
|
async function startServer(port, projectDir) {
|
|
2920
3403
|
const fastify = Fastify({ logger: false });
|
|
2921
3404
|
const configManager = new ConfigManager(projectDir);
|
|
@@ -3079,17 +3562,17 @@ async function startServer(port, projectDir) {
|
|
|
3079
3562
|
reply.code(400);
|
|
3080
3563
|
return { error: "Missing path parameter" };
|
|
3081
3564
|
}
|
|
3082
|
-
if (filePath.includes("..") ||
|
|
3565
|
+
if (filePath.includes("..") || path10.isAbsolute(filePath)) {
|
|
3083
3566
|
reply.code(403);
|
|
3084
3567
|
return { error: "Invalid path" };
|
|
3085
3568
|
}
|
|
3086
|
-
const fullPath =
|
|
3569
|
+
const fullPath = path10.resolve(projectDir, filePath);
|
|
3087
3570
|
if (!fullPath.startsWith(projectDir)) {
|
|
3088
3571
|
reply.code(403);
|
|
3089
3572
|
return { error: "Path traversal not allowed" };
|
|
3090
3573
|
}
|
|
3091
3574
|
try {
|
|
3092
|
-
const content =
|
|
3575
|
+
const content = fs9.readFileSync(fullPath, "utf-8");
|
|
3093
3576
|
return { content, path: filePath };
|
|
3094
3577
|
} catch {
|
|
3095
3578
|
reply.code(404);
|
|
@@ -3102,20 +3585,20 @@ async function startServer(port, projectDir) {
|
|
|
3102
3585
|
reply.code(400);
|
|
3103
3586
|
return { error: "Missing path parameter" };
|
|
3104
3587
|
}
|
|
3105
|
-
if (filePath.includes("..") ||
|
|
3588
|
+
if (filePath.includes("..") || path10.isAbsolute(filePath)) {
|
|
3106
3589
|
reply.code(403);
|
|
3107
3590
|
return { error: "Invalid path" };
|
|
3108
3591
|
}
|
|
3109
|
-
const fullPath =
|
|
3592
|
+
const fullPath = path10.resolve(projectDir, filePath);
|
|
3110
3593
|
if (!fullPath.startsWith(projectDir)) {
|
|
3111
3594
|
reply.code(403);
|
|
3112
3595
|
return { error: "Path traversal not allowed" };
|
|
3113
3596
|
}
|
|
3114
|
-
if (!
|
|
3597
|
+
if (!fs9.existsSync(fullPath)) {
|
|
3115
3598
|
reply.code(404);
|
|
3116
3599
|
return { error: "File not found" };
|
|
3117
3600
|
}
|
|
3118
|
-
const ext =
|
|
3601
|
+
const ext = path10.extname(fullPath).toLowerCase();
|
|
3119
3602
|
const mimeMap = {
|
|
3120
3603
|
".png": "image/png",
|
|
3121
3604
|
".jpg": "image/jpeg",
|
|
@@ -3129,14 +3612,14 @@ async function startServer(port, projectDir) {
|
|
|
3129
3612
|
".svg": "image/svg+xml"
|
|
3130
3613
|
};
|
|
3131
3614
|
const mime = mimeMap[ext] || "application/octet-stream";
|
|
3132
|
-
const stream =
|
|
3615
|
+
const stream = fs9.createReadStream(fullPath);
|
|
3133
3616
|
reply.type(mime);
|
|
3134
3617
|
return reply.send(stream);
|
|
3135
3618
|
});
|
|
3136
|
-
const notesPath =
|
|
3619
|
+
const notesPath = path10.join(projectDir, ".nexus", "notes.yaml");
|
|
3137
3620
|
fastify.get("/api/notes", async () => {
|
|
3138
3621
|
try {
|
|
3139
|
-
const raw =
|
|
3622
|
+
const raw = fs9.readFileSync(notesPath, "utf-8");
|
|
3140
3623
|
const data = yaml3.load(raw);
|
|
3141
3624
|
return { notes: data?.notes || [] };
|
|
3142
3625
|
} catch {
|
|
@@ -3146,20 +3629,20 @@ async function startServer(port, projectDir) {
|
|
|
3146
3629
|
fastify.put("/api/notes", async (request, reply) => {
|
|
3147
3630
|
try {
|
|
3148
3631
|
const { notes } = request.body;
|
|
3149
|
-
|
|
3150
|
-
|
|
3632
|
+
fs9.mkdirSync(path10.dirname(notesPath), { recursive: true });
|
|
3633
|
+
fs9.writeFileSync(notesPath, yaml3.dump({ notes }, { lineWidth: -1 }), "utf-8");
|
|
3151
3634
|
return { success: true };
|
|
3152
3635
|
} catch (err) {
|
|
3153
3636
|
reply.code(400);
|
|
3154
3637
|
return { error: "Failed to save notes" };
|
|
3155
3638
|
}
|
|
3156
3639
|
});
|
|
3157
|
-
const webDistPath =
|
|
3158
|
-
if (!
|
|
3640
|
+
const webDistPath = path10.resolve(__dirname, "../../web/dist");
|
|
3641
|
+
if (!fs9.existsSync(webDistPath)) {
|
|
3159
3642
|
console.warn(` [Warning] Frontend not found at ${webDistPath}`);
|
|
3160
3643
|
console.warn(` Run 'pnpm run build:web' to build the frontend, or use dev mode.`);
|
|
3161
3644
|
}
|
|
3162
|
-
if (
|
|
3645
|
+
if (fs9.existsSync(webDistPath)) {
|
|
3163
3646
|
await fastify.register(fastifyStatic, {
|
|
3164
3647
|
root: webDistPath,
|
|
3165
3648
|
prefix: "/"
|
|
@@ -3237,14 +3720,14 @@ function findProjectRoot(startDir) {
|
|
|
3237
3720
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3238
3721
|
let dir = startDir;
|
|
3239
3722
|
let bestMatch = startDir;
|
|
3240
|
-
while (dir !==
|
|
3241
|
-
if (
|
|
3723
|
+
while (dir !== path11.dirname(dir)) {
|
|
3724
|
+
if (fs10.existsSync(path11.join(dir, "pnpm-workspace.yaml"))) {
|
|
3242
3725
|
return dir;
|
|
3243
3726
|
}
|
|
3244
|
-
if (
|
|
3727
|
+
if (fs10.existsSync(path11.join(dir, ".git")) || fs10.existsSync(path11.join(dir, ".nexus"))) {
|
|
3245
3728
|
bestMatch = dir;
|
|
3246
3729
|
}
|
|
3247
|
-
const parent =
|
|
3730
|
+
const parent = path11.dirname(dir);
|
|
3248
3731
|
if (home && parent === home && dir !== startDir) break;
|
|
3249
3732
|
dir = parent;
|
|
3250
3733
|
}
|
|
@@ -3256,15 +3739,15 @@ function findProjectRoot(startDir) {
|
|
|
3256
3739
|
}
|
|
3257
3740
|
function resolveProjectDir(dirArg) {
|
|
3258
3741
|
if (process.env.NEXUS_PROJECT_DIR) {
|
|
3259
|
-
return
|
|
3742
|
+
return path11.resolve(process.env.NEXUS_PROJECT_DIR);
|
|
3260
3743
|
}
|
|
3261
3744
|
if (dirArg) {
|
|
3262
|
-
const resolved =
|
|
3263
|
-
if (!
|
|
3745
|
+
const resolved = path11.resolve(dirArg);
|
|
3746
|
+
if (!fs10.existsSync(resolved)) {
|
|
3264
3747
|
console.error(`Error: directory does not exist: ${resolved}`);
|
|
3265
3748
|
process.exit(1);
|
|
3266
3749
|
}
|
|
3267
|
-
if (!
|
|
3750
|
+
if (!fs10.statSync(resolved).isDirectory()) {
|
|
3268
3751
|
console.error(`Error: not a directory: ${resolved}`);
|
|
3269
3752
|
process.exit(1);
|
|
3270
3753
|
}
|