code-squad-cli 1.2.16 → 1.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // dist/index.js
4
4
  import * as path12 from "path";
5
- import * as fs10 from "fs";
6
- import * as os4 from "os";
5
+ import * as fs11 from "fs";
6
+ import * as os5 from "os";
7
7
  import * as crypto from "crypto";
8
8
  import chalk2 from "chalk";
9
9
 
@@ -533,18 +533,36 @@ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0, result = {
533
533
  }
534
534
  return result;
535
535
  }
536
+ function getCwdFromSession(req, res) {
537
+ const sessionId = req.query.session_id;
538
+ if (!sessionId) {
539
+ res.status(400).json({ error: "Missing session_id parameter" });
540
+ return null;
541
+ }
542
+ const sessionManager = req.app.locals.sessionManager;
543
+ const session = sessionManager.getSession(sessionId);
544
+ if (!session) {
545
+ res.status(404).json({ error: "Session not found" });
546
+ return null;
547
+ }
548
+ return session.cwd;
549
+ }
536
550
  router.get("/", (req, res) => {
537
- const state = req.app.locals.state;
538
- const tree = buildFileTree(state.cwd, state.cwd, 10);
551
+ const cwd = getCwdFromSession(req, res);
552
+ if (!cwd)
553
+ return;
554
+ const tree = buildFileTree(cwd, cwd, 10);
539
555
  const response = {
540
- root: state.cwd,
556
+ root: cwd,
541
557
  tree
542
558
  };
543
559
  res.json(response);
544
560
  });
545
561
  router.get("/flat", (req, res) => {
546
- const state = req.app.locals.state;
547
- const result = collectFlatFiles(state.cwd, state.cwd, 10);
562
+ const cwd = getCwdFromSession(req, res);
563
+ if (!cwd)
564
+ return;
565
+ const result = collectFlatFiles(cwd, cwd, 10);
548
566
  result.files.sort((a, b) => a.path.localeCompare(b.path));
549
567
  result.filteredDirs.sort();
550
568
  let recentFile = null;
@@ -686,16 +704,32 @@ function detectLanguage(filePath) {
686
704
  }
687
705
  return "text";
688
706
  }
707
+ function getCwdFromSession2(req, res) {
708
+ const sessionId = req.query.session_id;
709
+ if (!sessionId) {
710
+ res.status(400).json({ error: "Missing session_id parameter" });
711
+ return null;
712
+ }
713
+ const sessionManager = req.app.locals.sessionManager;
714
+ const session = sessionManager.getSession(sessionId);
715
+ if (!session) {
716
+ res.status(404).json({ error: "Session not found" });
717
+ return null;
718
+ }
719
+ return session.cwd;
720
+ }
689
721
  router2.get("/", (req, res) => {
690
- const state = req.app.locals.state;
722
+ const cwd = getCwdFromSession2(req, res);
723
+ if (!cwd)
724
+ return;
691
725
  const relativePath = req.query.path;
692
726
  if (!relativePath) {
693
727
  res.status(400).json({ error: "Missing path parameter" });
694
728
  return;
695
729
  }
696
- const filePath = path3.join(state.cwd, relativePath);
730
+ const filePath = path3.join(cwd, relativePath);
697
731
  const resolvedPath = path3.resolve(filePath);
698
- const resolvedCwd = path3.resolve(state.cwd);
732
+ const resolvedCwd = path3.resolve(cwd);
699
733
  if (!resolvedPath.startsWith(resolvedCwd)) {
700
734
  res.status(403).json({ error: "Access denied" });
701
735
  return;
@@ -823,12 +857,28 @@ function createAddedFileDiff(content) {
823
857
  lines: diffLines
824
858
  }];
825
859
  }
860
+ function getCwdFromSession3(req, res) {
861
+ const sessionId = req.query.session_id;
862
+ if (!sessionId) {
863
+ res.status(400).json({ error: "Missing session_id parameter" });
864
+ return null;
865
+ }
866
+ const sessionManager = req.app.locals.sessionManager;
867
+ const session = sessionManager.getSession(sessionId);
868
+ if (!session) {
869
+ res.status(404).json({ error: "Session not found" });
870
+ return null;
871
+ }
872
+ return session.cwd;
873
+ }
826
874
  router3.get("/status", (req, res) => {
827
- const state = req.app.locals.state;
875
+ const cwd = getCwdFromSession3(req, res);
876
+ if (!cwd)
877
+ return;
828
878
  let isGitRepo = false;
829
879
  try {
830
880
  execSync("git rev-parse --git-dir", {
831
- cwd: state.cwd,
881
+ cwd,
832
882
  stdio: "pipe"
833
883
  });
834
884
  isGitRepo = true;
@@ -845,7 +895,7 @@ router3.get("/status", (req, res) => {
845
895
  let unstaged = [];
846
896
  try {
847
897
  const output = execSync("git status --porcelain", {
848
- cwd: state.cwd,
898
+ cwd,
849
899
  encoding: "utf-8"
850
900
  });
851
901
  unstaged = parseGitStatus(output);
@@ -858,17 +908,19 @@ router3.get("/status", (req, res) => {
858
908
  res.json(response);
859
909
  });
860
910
  router3.get("/diff", (req, res) => {
861
- const state = req.app.locals.state;
911
+ const cwd = getCwdFromSession3(req, res);
912
+ if (!cwd)
913
+ return;
862
914
  const relativePath = req.query.path;
863
915
  if (!relativePath) {
864
916
  res.status(400).json({ error: "Missing path parameter" });
865
917
  return;
866
918
  }
867
- const fullPath = path4.join(state.cwd, relativePath);
919
+ const fullPath = path4.join(cwd, relativePath);
868
920
  let fileStatus = "modified";
869
921
  try {
870
922
  const statusOutput = execSync(`git status --porcelain -- "${relativePath}"`, {
871
- cwd: state.cwd,
923
+ cwd,
872
924
  encoding: "utf-8"
873
925
  });
874
926
  if (statusOutput.startsWith("??")) {
@@ -896,7 +948,7 @@ router3.get("/diff", (req, res) => {
896
948
  }
897
949
  try {
898
950
  const diffOutput = execSync(`git diff --no-color -- "${relativePath}"`, {
899
- cwd: state.cwd,
951
+ cwd,
900
952
  encoding: "utf-8"
901
953
  });
902
954
  if (!diffOutput.trim()) {
@@ -1070,8 +1122,12 @@ async function pasteToOriginalSession(sessionId) {
1070
1122
  // dist/flip/routes/submit.js
1071
1123
  var router4 = Router4();
1072
1124
  router4.post("/", async (req, res) => {
1073
- const state = req.app.locals.state;
1125
+ const sessionManager = req.app.locals.sessionManager;
1074
1126
  const body = req.body;
1127
+ if (!body.session_id) {
1128
+ res.status(400).json({ error: "Missing session_id" });
1129
+ return;
1130
+ }
1075
1131
  if (!body.items || body.items.length === 0) {
1076
1132
  res.status(400).json({ error: "No items to submit" });
1077
1133
  return;
@@ -1101,20 +1157,21 @@ router4.post("/", async (req, res) => {
1101
1157
  }
1102
1158
  }
1103
1159
  res.json({ status: "ok" });
1104
- if (state.resolve) {
1105
- state.resolve(formatted);
1106
- }
1160
+ await sessionManager.unregisterSession(body.session_id);
1107
1161
  });
1108
1162
 
1109
1163
  // dist/flip/routes/cancel.js
1110
1164
  import { Router as Router5 } from "express";
1111
1165
  var router5 = Router5();
1112
- router5.post("/", (req, res) => {
1113
- const state = req.app.locals.state;
1114
- res.json({ status: "ok" });
1115
- if (state.resolve) {
1116
- state.resolve(null);
1166
+ router5.post("/", async (req, res) => {
1167
+ const sessionManager = req.app.locals.sessionManager;
1168
+ const body = req.body;
1169
+ if (!body.session_id) {
1170
+ res.status(400).json({ error: "Missing session_id" });
1171
+ return;
1117
1172
  }
1173
+ res.json({ status: "ok" });
1174
+ await sessionManager.unregisterSession(body.session_id);
1118
1175
  });
1119
1176
 
1120
1177
  // dist/flip/routes/static.js
@@ -1149,28 +1206,97 @@ function createStaticRouter() {
1149
1206
  return router6;
1150
1207
  }
1151
1208
 
1152
- // dist/flip/routes/events.js
1209
+ // dist/flip/routes/ping.js
1153
1210
  import { Router as Router7 } from "express";
1154
- function createEventsRouter(sseManager) {
1211
+ function createPingRouter() {
1155
1212
  const router6 = Router7();
1156
- router6.get("/", (req, res) => {
1157
- res.setHeader("Content-Type", "text/event-stream");
1158
- res.setHeader("Cache-Control", "no-cache");
1159
- res.setHeader("Connection", "keep-alive");
1160
- res.setHeader("X-Accel-Buffering", "no");
1161
- res.write('data: {"type":"connected"}\n\n');
1162
- sseManager.addClient(res);
1163
- const heartbeat = setInterval(() => {
1164
- res.write(":heartbeat\n\n");
1165
- }, 3e4);
1166
- req.on("close", () => {
1167
- clearInterval(heartbeat);
1168
- res.end();
1213
+ router6.get("/", (_req, res) => {
1214
+ const response = {
1215
+ id: SERVER_ID,
1216
+ version: "1.0.0"
1217
+ };
1218
+ res.json(response);
1219
+ });
1220
+ return router6;
1221
+ }
1222
+
1223
+ // dist/flip/routes/session.js
1224
+ import { Router as Router8 } from "express";
1225
+ function createSessionRouter(sessionManager) {
1226
+ const router6 = Router8();
1227
+ router6.post("/register", (req, res) => {
1228
+ const body = req.body;
1229
+ if (!body.session_id || !body.cwd) {
1230
+ res.status(400).json({ error: "Missing session_id or cwd" });
1231
+ return;
1232
+ }
1233
+ if (!sessionManager.hasSessions && req.app.locals.clearIdleTimer) {
1234
+ req.app.locals.clearIdleTimer();
1235
+ }
1236
+ sessionManager.registerSession(body.session_id, body.cwd);
1237
+ const response = {
1238
+ status: "ok",
1239
+ session_id: body.session_id
1240
+ };
1241
+ res.json(response);
1242
+ });
1243
+ router6.post("/unregister", async (req, res) => {
1244
+ const body = req.body;
1245
+ if (!body.session_id) {
1246
+ res.status(400).json({ error: "Missing session_id" });
1247
+ return;
1248
+ }
1249
+ await sessionManager.unregisterSession(body.session_id);
1250
+ const response = {
1251
+ status: "ok"
1252
+ };
1253
+ res.json(response);
1254
+ });
1255
+ router6.get("/info", (req, res) => {
1256
+ const sessionId = req.query.session_id;
1257
+ if (!sessionId) {
1258
+ res.status(400).json({ error: "Missing session_id" });
1259
+ return;
1260
+ }
1261
+ const session = sessionManager.getSession(sessionId);
1262
+ if (!session) {
1263
+ res.status(404).json({ error: "Session not found" });
1264
+ return;
1265
+ }
1266
+ res.json({
1267
+ session_id: session.id,
1268
+ cwd: session.cwd
1169
1269
  });
1170
1270
  });
1171
1271
  return router6;
1172
1272
  }
1173
1273
 
1274
+ // dist/flip/routes/changes.js
1275
+ import { Router as Router9 } from "express";
1276
+ function createChangesRouter(sessionManager) {
1277
+ const router6 = Router9();
1278
+ router6.get("/", (req, res) => {
1279
+ const sessionId = req.query.session_id;
1280
+ if (!sessionId) {
1281
+ res.status(400).json({ error: "Missing session_id" });
1282
+ return;
1283
+ }
1284
+ sessionManager.touchSession(sessionId);
1285
+ const changes = sessionManager.consumePendingChanges(sessionId);
1286
+ if (!changes) {
1287
+ res.status(404).json({ error: "Session not found" });
1288
+ return;
1289
+ }
1290
+ const response = {
1291
+ filesChanged: changes.filesChanged,
1292
+ gitChanged: changes.gitChanged,
1293
+ changedFiles: Array.from(changes.changedFiles)
1294
+ };
1295
+ res.json(response);
1296
+ });
1297
+ return router6;
1298
+ }
1299
+
1174
1300
  // dist/flip/watcher/FileWatcher.js
1175
1301
  import chokidar from "chokidar";
1176
1302
  import * as path8 from "path";
@@ -1294,87 +1420,266 @@ var FileWatcher = class {
1294
1420
  }
1295
1421
  };
1296
1422
 
1297
- // dist/flip/events/SSEManager.js
1298
- var SSEManager = class {
1299
- clients = /* @__PURE__ */ new Set();
1300
- addClient(res) {
1301
- this.clients.add(res);
1302
- res.on("close", () => {
1303
- this.clients.delete(res);
1304
- });
1423
+ // dist/flip/session/SessionManager.js
1424
+ var log = (...args) => console.error(...args);
1425
+ var SessionManager = class {
1426
+ sessions = /* @__PURE__ */ new Map();
1427
+ options;
1428
+ constructor(options) {
1429
+ this.options = options;
1305
1430
  }
1306
- removeClient(res) {
1307
- this.clients.delete(res);
1431
+ /**
1432
+ * Start the session manager (no-op, kept for API compatibility)
1433
+ */
1434
+ start() {
1308
1435
  }
1309
- broadcast(event) {
1310
- const data = JSON.stringify(event);
1311
- const message = `data: ${data}
1312
-
1313
- `;
1314
- this.clients.forEach((client) => {
1315
- client.write(message);
1436
+ /**
1437
+ * Stop the session manager and clean up all sessions
1438
+ */
1439
+ async stop() {
1440
+ const stopPromises = Array.from(this.sessions.values()).map((session) => {
1441
+ if (session.timeoutTimer) {
1442
+ clearTimeout(session.timeoutTimer);
1443
+ }
1444
+ return session.watcher.stop();
1316
1445
  });
1446
+ await Promise.all(stopPromises);
1447
+ this.sessions.clear();
1448
+ }
1449
+ /**
1450
+ * Register a new session or update existing one
1451
+ */
1452
+ registerSession(sessionId, cwd) {
1453
+ const existing = this.sessions.get(sessionId);
1454
+ if (existing) {
1455
+ this.resetSessionTimeout(sessionId);
1456
+ if (existing.cwd !== cwd) {
1457
+ existing.watcher.stop();
1458
+ existing.cwd = cwd;
1459
+ existing.watcher = this.createWatcher(sessionId, cwd);
1460
+ existing.pendingChanges = this.createEmptyPendingChanges();
1461
+ }
1462
+ return existing;
1463
+ }
1464
+ const watcher = this.createWatcher(sessionId, cwd);
1465
+ const session = {
1466
+ id: sessionId,
1467
+ cwd,
1468
+ watcher,
1469
+ pendingChanges: this.createEmptyPendingChanges(),
1470
+ timeoutTimer: null
1471
+ };
1472
+ this.sessions.set(sessionId, session);
1473
+ this.resetSessionTimeout(sessionId);
1474
+ log(`[SessionManager] Session registered: ${sessionId} (cwd: ${cwd})`);
1475
+ return session;
1476
+ }
1477
+ /**
1478
+ * Get a session by ID and reset timeout
1479
+ */
1480
+ getSession(sessionId) {
1481
+ const session = this.sessions.get(sessionId);
1482
+ if (session) {
1483
+ this.resetSessionTimeout(sessionId);
1484
+ }
1485
+ return session;
1486
+ }
1487
+ /**
1488
+ * Touch session to reset timeout (called on every API request)
1489
+ */
1490
+ touchSession(sessionId) {
1491
+ this.resetSessionTimeout(sessionId);
1492
+ }
1493
+ /**
1494
+ * Unregister a session (e.g., on cancel/submit/timeout)
1495
+ */
1496
+ async unregisterSession(sessionId) {
1497
+ const session = this.sessions.get(sessionId);
1498
+ if (session) {
1499
+ if (session.timeoutTimer) {
1500
+ clearTimeout(session.timeoutTimer);
1501
+ }
1502
+ await session.watcher.stop();
1503
+ this.sessions.delete(sessionId);
1504
+ log(`[SessionManager] Session unregistered: ${sessionId}`);
1505
+ if (this.sessions.size === 0 && this.options.onAllSessionsGone) {
1506
+ this.options.onAllSessionsGone();
1507
+ }
1508
+ }
1317
1509
  }
1318
- closeAll() {
1319
- this.clients.forEach((client) => {
1320
- client.end();
1510
+ /**
1511
+ * Get pending changes for a session and clear them
1512
+ */
1513
+ consumePendingChanges(sessionId) {
1514
+ const session = this.sessions.get(sessionId);
1515
+ if (!session)
1516
+ return null;
1517
+ const changes = { ...session.pendingChanges };
1518
+ changes.changedFiles = new Set(session.pendingChanges.changedFiles);
1519
+ session.pendingChanges = this.createEmptyPendingChanges();
1520
+ return changes;
1521
+ }
1522
+ /**
1523
+ * Get number of active sessions
1524
+ */
1525
+ get sessionCount() {
1526
+ return this.sessions.size;
1527
+ }
1528
+ /**
1529
+ * Check if any sessions exist
1530
+ */
1531
+ get hasSessions() {
1532
+ return this.sessions.size > 0;
1533
+ }
1534
+ createWatcher(sessionId, cwd) {
1535
+ const watcher = new FileWatcher({ cwd });
1536
+ watcher.onFilesChanged(() => {
1537
+ const session = this.sessions.get(sessionId);
1538
+ if (session) {
1539
+ session.pendingChanges.filesChanged = true;
1540
+ }
1541
+ });
1542
+ watcher.onFileChanged((filePath) => {
1543
+ const session = this.sessions.get(sessionId);
1544
+ if (session) {
1545
+ session.pendingChanges.changedFiles.add(filePath);
1546
+ }
1321
1547
  });
1322
- this.clients.clear();
1548
+ watcher.onGitChanged(() => {
1549
+ const session = this.sessions.get(sessionId);
1550
+ if (session) {
1551
+ session.pendingChanges.gitChanged = true;
1552
+ }
1553
+ });
1554
+ watcher.start();
1555
+ return watcher;
1556
+ }
1557
+ createEmptyPendingChanges() {
1558
+ return {
1559
+ filesChanged: false,
1560
+ gitChanged: false,
1561
+ changedFiles: /* @__PURE__ */ new Set()
1562
+ };
1323
1563
  }
1324
- get clientCount() {
1325
- return this.clients.size;
1564
+ /**
1565
+ * Reset the timeout timer for a session.
1566
+ * Called on every activity (register, poll, etc.)
1567
+ */
1568
+ resetSessionTimeout(sessionId) {
1569
+ const session = this.sessions.get(sessionId);
1570
+ if (!session)
1571
+ return;
1572
+ if (session.timeoutTimer) {
1573
+ clearTimeout(session.timeoutTimer);
1574
+ }
1575
+ session.timeoutTimer = setTimeout(() => {
1576
+ log(`[SessionManager] Session timed out: ${sessionId}`);
1577
+ this.unregisterSession(sessionId).catch((error) => {
1578
+ log(`[SessionManager] Error during session unregister for ${sessionId}:`, error);
1579
+ });
1580
+ }, this.options.sessionTimeoutMs);
1326
1581
  }
1327
1582
  };
1328
1583
 
1329
1584
  // dist/flip/server/Server.js
1585
+ var log2 = (...args) => console.error(...args);
1586
+ var SERVER_ID = "csq-flip";
1330
1587
  var Server = class {
1331
- cwd;
1332
1588
  port;
1333
- constructor(cwd, port) {
1334
- this.cwd = cwd;
1589
+ options;
1590
+ app = null;
1591
+ server = null;
1592
+ sessionManager = null;
1593
+ resolveShutdown = null;
1594
+ constructor(port, options = {}) {
1335
1595
  this.port = port;
1596
+ this.options = {
1597
+ sessionTimeoutMs: options.sessionTimeoutMs ?? 3e4,
1598
+ idleTimeout: options.idleTimeout ?? 0
1599
+ };
1336
1600
  }
1601
+ /**
1602
+ * Start the server and return a promise that resolves when server shuts down
1603
+ */
1337
1604
  async run() {
1338
1605
  return new Promise((resolve2) => {
1339
- const state = {
1340
- cwd: this.cwd,
1341
- resolve: null
1342
- };
1343
- const app = express2();
1344
- app.use(cors());
1345
- app.use(express2.json());
1346
- app.locals.state = state;
1347
- const sseManager = new SSEManager();
1348
- const fileWatcher = new FileWatcher({ cwd: this.cwd });
1349
- fileWatcher.onFilesChanged(() => {
1350
- sseManager.broadcast({ type: "files-changed" });
1351
- });
1352
- fileWatcher.onFileChanged((path13) => {
1353
- sseManager.broadcast({ type: "file-changed", path: path13 });
1354
- });
1355
- fileWatcher.onGitChanged(() => {
1356
- sseManager.broadcast({ type: "git-changed" });
1606
+ this.resolveShutdown = resolve2;
1607
+ this.app = express2();
1608
+ this.app.use(cors());
1609
+ this.app.use(express2.json());
1610
+ this.sessionManager = new SessionManager({
1611
+ sessionTimeoutMs: this.options.sessionTimeoutMs,
1612
+ onAllSessionsGone: () => {
1613
+ log2("[Server] All sessions gone, shutting down...");
1614
+ this.shutdown();
1615
+ }
1357
1616
  });
1358
- fileWatcher.start();
1359
- app.use("/api/files", router);
1360
- app.use("/api/file", router2);
1361
- app.use("/api/git", router3);
1362
- app.use("/api/submit", router4);
1363
- app.use("/api/cancel", router5);
1364
- app.use("/api/events", createEventsRouter(sseManager));
1365
- app.use(createStaticRouter());
1366
- const server = http.createServer(app);
1367
- state.resolve = async (output) => {
1368
- await fileWatcher.stop();
1369
- sseManager.closeAll();
1370
- await new Promise((res) => server.close(() => res()));
1371
- resolve2(output);
1617
+ this.sessionManager.start();
1618
+ this.app.locals.sessionManager = this.sessionManager;
1619
+ this.app.use("/api/ping", createPingRouter());
1620
+ this.app.use("/api/session", createSessionRouter(this.sessionManager));
1621
+ this.app.use("/api/changes", createChangesRouter(this.sessionManager));
1622
+ this.app.use("/api/files", router);
1623
+ this.app.use("/api/file", router2);
1624
+ this.app.use("/api/git", router3);
1625
+ this.app.use("/api/submit", router4);
1626
+ this.app.use("/api/cancel", router5);
1627
+ this.app.use(createStaticRouter());
1628
+ this.server = http.createServer(this.app);
1629
+ const signalHandler = () => {
1630
+ log2("\nShutting down...");
1631
+ this.shutdown();
1372
1632
  };
1373
- server.listen(this.port, "127.0.0.1", () => {
1374
- console.error(`Server running at http://localhost:${this.port}`);
1633
+ process.on("SIGINT", signalHandler);
1634
+ process.on("SIGTERM", signalHandler);
1635
+ let idleTimer = null;
1636
+ if (this.options.idleTimeout && this.options.idleTimeout > 0) {
1637
+ idleTimer = setTimeout(() => {
1638
+ if (!this.sessionManager?.hasSessions) {
1639
+ log2("\nNo session registered, shutting down...");
1640
+ this.shutdown();
1641
+ }
1642
+ }, this.options.idleTimeout);
1643
+ }
1644
+ this.app.locals.idleTimer = idleTimer;
1645
+ this.app.locals.clearIdleTimer = () => {
1646
+ if (idleTimer) {
1647
+ clearTimeout(idleTimer);
1648
+ idleTimer = null;
1649
+ this.app.locals.idleTimer = null;
1650
+ }
1651
+ };
1652
+ this.server.listen(this.port, "127.0.0.1", () => {
1653
+ log2(`[Server] Running at http://localhost:${this.port}`);
1375
1654
  });
1376
1655
  });
1377
1656
  }
1657
+ /**
1658
+ * Shutdown the server gracefully
1659
+ */
1660
+ async shutdown() {
1661
+ if (!this.server)
1662
+ return;
1663
+ if (this.app?.locals.idleTimer) {
1664
+ clearTimeout(this.app.locals.idleTimer);
1665
+ }
1666
+ if (this.sessionManager) {
1667
+ await this.sessionManager.stop();
1668
+ }
1669
+ await new Promise((res) => {
1670
+ this.server.close(() => res());
1671
+ });
1672
+ log2("[Server] Shutdown complete");
1673
+ if (this.resolveShutdown) {
1674
+ this.resolveShutdown();
1675
+ }
1676
+ }
1677
+ /**
1678
+ * Get the session manager
1679
+ */
1680
+ getSessionManager() {
1681
+ return this.sessionManager;
1682
+ }
1378
1683
  };
1379
1684
  async function findFreePort(preferred, maxPort = 65535) {
1380
1685
  return new Promise((resolve2, reject) => {
@@ -1395,13 +1700,53 @@ async function findFreePort(preferred, maxPort = 65535) {
1395
1700
  });
1396
1701
  });
1397
1702
  }
1703
+ async function isFlipServerRunning(port, timeoutMs = 1e3) {
1704
+ return new Promise((resolve2) => {
1705
+ const req = http.request({
1706
+ hostname: "127.0.0.1",
1707
+ port,
1708
+ path: "/api/ping",
1709
+ method: "GET",
1710
+ timeout: timeoutMs
1711
+ }, (res) => {
1712
+ let data = "";
1713
+ res.on("data", (chunk) => data += chunk);
1714
+ res.on("end", () => {
1715
+ try {
1716
+ const json = JSON.parse(data);
1717
+ resolve2(json.id === SERVER_ID);
1718
+ } catch {
1719
+ resolve2(false);
1720
+ }
1721
+ });
1722
+ });
1723
+ req.on("error", () => resolve2(false));
1724
+ req.on("timeout", () => {
1725
+ req.destroy();
1726
+ resolve2(false);
1727
+ });
1728
+ req.end();
1729
+ });
1730
+ }
1731
+ async function findExistingServer(startPort, endPort) {
1732
+ for (let port = startPort; port <= endPort; port++) {
1733
+ if (await isFlipServerRunning(port)) {
1734
+ return port;
1735
+ }
1736
+ }
1737
+ return null;
1738
+ }
1398
1739
 
1399
1740
  // dist/flip/index.js
1400
1741
  import open from "open";
1401
1742
  import path9 from "path";
1743
+ import fs8 from "fs";
1744
+ import os3 from "os";
1745
+ import http2 from "http";
1402
1746
  import { execSync as execSync2 } from "child_process";
1403
1747
  var DEFAULT_PORT = 51234;
1404
- var log = (...args) => console.error(...args);
1748
+ var MAX_PORT = 51240;
1749
+ var log3 = (...args) => console.error(...args);
1405
1750
  function formatTime() {
1406
1751
  const now = /* @__PURE__ */ new Date();
1407
1752
  const hours = String(now.getHours()).padStart(2, "0");
@@ -1409,6 +1754,43 @@ function formatTime() {
1409
1754
  const secs = String(now.getSeconds()).padStart(2, "0");
1410
1755
  return `${hours}:${mins}:${secs}`;
1411
1756
  }
1757
+ function getSessionId() {
1758
+ try {
1759
+ const tty = execSync2("tty", { encoding: "utf-8" }).trim();
1760
+ return tty;
1761
+ } catch {
1762
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1763
+ }
1764
+ }
1765
+ async function registerSession(port, sessionId, cwd) {
1766
+ return new Promise((resolve2) => {
1767
+ const data = JSON.stringify({ session_id: sessionId, cwd });
1768
+ const req = http2.request({
1769
+ hostname: "127.0.0.1",
1770
+ port,
1771
+ path: "/api/session/register",
1772
+ method: "POST",
1773
+ headers: {
1774
+ "Content-Type": "application/json",
1775
+ "Content-Length": Buffer.byteLength(data)
1776
+ },
1777
+ timeout: 5e3
1778
+ }, (res) => {
1779
+ let body = "";
1780
+ res.on("data", (chunk) => body += chunk);
1781
+ res.on("end", () => {
1782
+ resolve2(res.statusCode === 200);
1783
+ });
1784
+ });
1785
+ req.on("error", () => resolve2(false));
1786
+ req.on("timeout", () => {
1787
+ req.destroy();
1788
+ resolve2(false);
1789
+ });
1790
+ req.write(data);
1791
+ req.end();
1792
+ });
1793
+ }
1412
1794
  async function runFlip(args) {
1413
1795
  let sessionId;
1414
1796
  const sessionIdx = args.indexOf("--session");
@@ -1449,58 +1831,88 @@ async function runFlip(args) {
1449
1831
  command = "oneshot";
1450
1832
  }
1451
1833
  const cwd = pathArg ? path9.resolve(pathArg) : process.cwd();
1834
+ const finalSessionId = sessionId || getSessionId();
1452
1835
  switch (command) {
1453
1836
  case "setup": {
1454
1837
  await setupHotkey();
1455
1838
  return;
1456
1839
  }
1457
1840
  case "serve": {
1458
- const port = await findFreePort(DEFAULT_PORT);
1459
- log(`Server running at http://localhost:${port}`);
1460
- log("Press Ctrl+C to stop");
1461
- log("");
1462
- log("To open browser, run: csq flip open");
1463
- log(`Or use hotkey to open: open http://localhost:${port}`);
1464
- while (true) {
1465
- const server = new Server(cwd, port);
1466
- const result = await server.run();
1467
- if (result) {
1468
- log(`[${formatTime()}] Submitted ${result.length} characters`);
1469
- } else {
1470
- log(`[${formatTime()}] Cancelled`);
1471
- }
1472
- log(`[${formatTime()}] Ready for next session...`);
1841
+ let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1842
+ if (port) {
1843
+ log3(`[${formatTime()}] Found existing server at port ${port}`);
1844
+ } else {
1845
+ port = await findFreePort(DEFAULT_PORT, MAX_PORT);
1846
+ log3(`[${formatTime()}] Starting new server at port ${port}`);
1847
+ const server = new Server(port, { sessionTimeoutMs: 4e3 });
1848
+ server.run().catch((err) => {
1849
+ log3(`[${formatTime()}] Server error:`, err);
1850
+ });
1473
1851
  }
1852
+ log3(`[${formatTime()}] Server running at http://localhost:${port}`);
1853
+ log3("Press Ctrl+C to stop");
1854
+ log3("");
1855
+ log3("To open browser, run: csq flip open");
1856
+ log3(`Or use hotkey to open: open http://localhost:${port}`);
1857
+ await new Promise(() => {
1858
+ });
1474
1859
  }
1475
1860
  case "open": {
1476
- const url = `http://localhost:${DEFAULT_PORT}`;
1477
- log(`Opening ${url} in browser...`);
1861
+ const port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1862
+ if (!port) {
1863
+ log3("No csq flip server running.");
1864
+ log3("Start with: csq flip serve");
1865
+ log3("Or use: csq flip (one-shot mode)");
1866
+ return;
1867
+ }
1868
+ if (!await registerSession(port, finalSessionId, cwd)) {
1869
+ log3("Failed to register session with server");
1870
+ return;
1871
+ }
1872
+ const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
1873
+ log3(`Opening ${url} in browser...`);
1478
1874
  try {
1479
1875
  await open(url);
1480
1876
  } catch (e) {
1481
- log("Failed to open browser:", e);
1482
- log("Is the server running? Start with: csq flip serve");
1877
+ log3("Failed to open browser:", e);
1878
+ log3(`Please open ${url} manually`);
1483
1879
  }
1484
1880
  break;
1485
1881
  }
1486
1882
  case "oneshot":
1487
1883
  default: {
1488
- const port = await findFreePort(DEFAULT_PORT);
1489
- const url = sessionId ? `http://localhost:${port}?session=${sessionId}` : `http://localhost:${port}`;
1490
- log(`Opening ${url} in browser...`);
1884
+ let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1885
+ let serverPromise = null;
1886
+ if (!port) {
1887
+ port = await findFreePort(DEFAULT_PORT, MAX_PORT);
1888
+ log3(`Starting server at http://localhost:${port}...`);
1889
+ const server = new Server(port, {
1890
+ sessionTimeoutMs: 4e3,
1891
+ // 4s (polling is 2s, so 2 missed polls = dead)
1892
+ idleTimeout: 5e3
1893
+ });
1894
+ serverPromise = server.run();
1895
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
1896
+ } else {
1897
+ log3(`Using existing server at http://localhost:${port}`);
1898
+ }
1899
+ if (!await registerSession(port, finalSessionId, cwd)) {
1900
+ log3("Failed to register session with server");
1901
+ return;
1902
+ }
1903
+ const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
1904
+ log3(`Opening ${url} in browser...`);
1491
1905
  try {
1492
1906
  await open(url);
1493
1907
  } catch (e) {
1494
- log("Failed to open browser:", e);
1495
- log(`Please open ${url} manually`);
1908
+ log3("Failed to open browser:", e);
1909
+ log3(`Please open ${url} manually`);
1496
1910
  }
1497
- const server = new Server(cwd, port);
1498
- const result = await server.run();
1499
- if (result) {
1500
- log(`
1501
- Submitted ${result.length} characters`);
1502
- } else {
1503
- log("\nCancelled");
1911
+ if (serverPromise) {
1912
+ log3(`Session: ${finalSessionId}`);
1913
+ log3("Waiting for session to complete... (Ctrl+C to exit)");
1914
+ await serverPromise;
1915
+ log3(`[${formatTime()}] Done`);
1504
1916
  }
1505
1917
  break;
1506
1918
  }
@@ -1512,12 +1924,12 @@ function printUsage() {
1512
1924
  console.error("Commands:");
1513
1925
  console.error(" serve [path] Start server in daemon mode (keeps running)");
1514
1926
  console.error(" open Open browser to existing server");
1515
- console.error(" setup Setup Alt+; hotkey in shell config");
1927
+ console.error(" setup Setup hotkey in shell config");
1516
1928
  console.error(" (no command) Start server + open browser (one-shot mode)");
1517
1929
  console.error("");
1518
1930
  console.error("Options:");
1519
1931
  console.error(" path Directory to serve (default: current directory)");
1520
- console.error(" --session <uuid> Session ID for paste-back tracking");
1932
+ console.error(" --session <id> Session ID for paste-back tracking (auto-generated from tty)");
1521
1933
  }
1522
1934
  async function setupHotkey() {
1523
1935
  let nodePath;
@@ -1526,8 +1938,44 @@ async function setupHotkey() {
1526
1938
  } catch {
1527
1939
  nodePath = "/usr/local/bin/node";
1528
1940
  }
1529
- const csqPath = new URL(import.meta.url).pathname;
1530
- const command = `${nodePath} ${csqPath} flip`;
1941
+ let csqPath;
1942
+ try {
1943
+ csqPath = execSync2("which csq", { encoding: "utf-8" }).trim();
1944
+ } catch {
1945
+ csqPath = new URL(import.meta.url).pathname;
1946
+ }
1947
+ const nodeDir = path9.dirname(nodePath);
1948
+ const wrapperScript = `#!/bin/bash
1949
+ # Add node to PATH (coprocess doesn't inherit shell PATH)
1950
+ export PATH="${nodeDir}:$PATH"
1951
+
1952
+ # Get current directory from iTerm2
1953
+ CWD=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return variable named "session.path"' 2>/dev/null)
1954
+
1955
+ # Fallback: try to get from tty
1956
+ if [ -z "$CWD" ] || [ "$CWD" = "missing value" ]; then
1957
+ TTY=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return tty' 2>/dev/null)
1958
+ if [ -n "$TTY" ]; then
1959
+ PID=$(lsof -t "$TTY" 2>/dev/null | head -1)
1960
+ if [ -n "$PID" ]; then
1961
+ CWD=$(lsof -a -d cwd -p "$PID" -Fn 2>/dev/null | grep ^n | cut -c2-)
1962
+ fi
1963
+ fi
1964
+ fi
1965
+
1966
+ # Run csq flip with cwd
1967
+ if [ -n "$CWD" ] && [ "$CWD" != "missing value" ]; then
1968
+ exec ${csqPath} flip "$CWD"
1969
+ else
1970
+ exec ${csqPath} flip
1971
+ fi
1972
+ `;
1973
+ const scriptDir = path9.join(os3.homedir(), ".config", "csq");
1974
+ const scriptPath = path9.join(scriptDir, "flip-hotkey.sh");
1975
+ if (!fs8.existsSync(scriptDir)) {
1976
+ fs8.mkdirSync(scriptDir, { recursive: true });
1977
+ }
1978
+ fs8.writeFileSync(scriptPath, wrapperScript, { mode: 493 });
1531
1979
  console.log("");
1532
1980
  console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
1533
1981
  console.log("\u2502 Flip Hotkey Setup (iTerm2) \u2502");
@@ -1539,10 +1987,10 @@ async function setupHotkey() {
1539
1987
  console.log('4. Action: "Run Coprocess"');
1540
1988
  console.log("5. Command (\uC544\uB798 \uC790\uB3D9 \uBCF5\uC0AC\uB428):");
1541
1989
  console.log("");
1542
- console.log(` ${command}`);
1990
+ console.log(` ${scriptPath}`);
1543
1991
  console.log("");
1544
1992
  try {
1545
- await copyToClipboard(command);
1993
+ await copyToClipboard(scriptPath);
1546
1994
  console.log(" (Copied to clipboard!)");
1547
1995
  } catch {
1548
1996
  }
@@ -1550,13 +1998,13 @@ async function setupHotkey() {
1550
1998
  }
1551
1999
 
1552
2000
  // dist/config.js
1553
- import * as fs8 from "fs";
1554
- import * as os3 from "os";
2001
+ import * as fs9 from "fs";
2002
+ import * as os4 from "os";
1555
2003
  import * as path10 from "path";
1556
- var GLOBAL_CONFIG_PATH = path10.join(os3.homedir(), ".code-squad", "config.json");
2004
+ var GLOBAL_CONFIG_PATH = path10.join(os4.homedir(), ".code-squad", "config.json");
1557
2005
  async function loadGlobalConfig() {
1558
2006
  try {
1559
- const content = await fs8.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
2007
+ const content = await fs9.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
1560
2008
  return JSON.parse(content);
1561
2009
  } catch (error) {
1562
2010
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -1587,7 +2035,7 @@ function getWorktreeCopyPatterns(config) {
1587
2035
  }
1588
2036
 
1589
2037
  // dist/fileUtils.js
1590
- import * as fs9 from "fs";
2038
+ import * as fs10 from "fs";
1591
2039
  import * as path11 from "path";
1592
2040
  import fg from "fast-glob";
1593
2041
  async function copyFilesWithPatterns(sourceRoot, destRoot, patterns) {
@@ -1624,8 +2072,8 @@ async function copySingleFile(absolutePath, sourceRoot, destRoot) {
1624
2072
  const relativePath = path11.relative(sourceRoot, absolutePath);
1625
2073
  const destPath = path11.join(destRoot, relativePath);
1626
2074
  const destDir = path11.dirname(destPath);
1627
- await fs9.promises.mkdir(destDir, { recursive: true });
1628
- await fs9.promises.copyFile(absolutePath, destPath);
2075
+ await fs10.promises.mkdir(destDir, { recursive: true });
2076
+ await fs10.promises.copyFile(absolutePath, destPath);
1629
2077
  }
1630
2078
 
1631
2079
  // dist/index.js
@@ -1681,12 +2129,12 @@ function getProjectHash(workspaceRoot) {
1681
2129
  function getSessionsPath(workspaceRoot) {
1682
2130
  const projectHash = getProjectHash(workspaceRoot);
1683
2131
  const projectName = path12.basename(workspaceRoot);
1684
- return path12.join(os4.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
2132
+ return path12.join(os5.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
1685
2133
  }
1686
2134
  async function loadLocalThreads(workspaceRoot) {
1687
2135
  const sessionsPath = getSessionsPath(workspaceRoot);
1688
2136
  try {
1689
- const content = await fs10.promises.readFile(sessionsPath, "utf-8");
2137
+ const content = await fs11.promises.readFile(sessionsPath, "utf-8");
1690
2138
  const data = JSON.parse(content);
1691
2139
  return data.localThreads || [];
1692
2140
  } catch {
@@ -1696,8 +2144,8 @@ async function loadLocalThreads(workspaceRoot) {
1696
2144
  async function saveLocalThreads(workspaceRoot, threads) {
1697
2145
  const sessionsPath = getSessionsPath(workspaceRoot);
1698
2146
  const dir = path12.dirname(sessionsPath);
1699
- await fs10.promises.mkdir(dir, { recursive: true });
1700
- await fs10.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
2147
+ await fs11.promises.mkdir(dir, { recursive: true });
2148
+ await fs11.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
1701
2149
  }
1702
2150
  async function addLocalThread(workspaceRoot, name) {
1703
2151
  const threads = await loadLocalThreads(workspaceRoot);