itwillsync 1.5.2 → 1.6.0

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.
@@ -3649,10 +3649,11 @@ var require_websocket_server = __commonJS({
3649
3649
  });
3650
3650
 
3651
3651
  // src/daemon.ts
3652
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, unlinkSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
3653
- import { homedir as homedir2 } from "os";
3654
- import { join as join3, dirname as dirname2 } from "path";
3652
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, existsSync as existsSync3, readdirSync, statSync } from "fs";
3653
+ import { homedir as homedir4 } from "os";
3654
+ import { join as join4, dirname as dirname3 } from "path";
3655
3655
  import { fileURLToPath } from "url";
3656
+ import { spawn } from "child_process";
3656
3657
 
3657
3658
  // src/auth.ts
3658
3659
  import { randomBytes, timingSafeEqual } from "crypto";
@@ -3840,6 +3841,25 @@ var SessionRegistry = class extends EventEmitter {
3840
3841
  this.healthCheckInterval = null;
3841
3842
  }
3842
3843
  }
3844
+ /** Get deduplicated recent working directories from current + persisted sessions. */
3845
+ getRecentDirectories() {
3846
+ const dirMap = /* @__PURE__ */ new Map();
3847
+ for (const s of this.sessions.values()) {
3848
+ const existing = dirMap.get(s.cwd) ?? 0;
3849
+ if (s.lastSeen > existing) {
3850
+ dirMap.set(s.cwd, s.lastSeen);
3851
+ }
3852
+ }
3853
+ if (this.store) {
3854
+ for (const s of this.store.getAllSessions()) {
3855
+ const existing = dirMap.get(s.cwd) ?? 0;
3856
+ if (s.lastSeen > existing) {
3857
+ dirMap.set(s.cwd, s.lastSeen);
3858
+ }
3859
+ }
3860
+ }
3861
+ return Array.from(dirMap.entries()).sort((a, b) => b[1] - a[1]).map(([cwd]) => cwd);
3862
+ }
3843
3863
  clear() {
3844
3864
  const ids = Array.from(this.sessions.keys());
3845
3865
  this.sessions.clear();
@@ -3891,6 +3911,10 @@ var SessionStore = class {
3891
3911
  this.writeToDisk();
3892
3912
  }, 500);
3893
3913
  }
3914
+ /** Get all persisted sessions (including ended). */
3915
+ getAllSessions() {
3916
+ return this.sessions;
3917
+ }
3894
3918
  /** Flush any pending save immediately. */
3895
3919
  flush() {
3896
3920
  if (this.saveTimer) {
@@ -3923,6 +3947,58 @@ var SessionStore = class {
3923
3947
  }
3924
3948
  };
3925
3949
 
3950
+ // src/tool-history.ts
3951
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
3952
+ import { homedir as homedir2 } from "os";
3953
+ import { join as join2, dirname as dirname2 } from "path";
3954
+ var MAX_ENTRIES = 20;
3955
+ function getHistoryPath() {
3956
+ const dir = process.env.ITWILLSYNC_CONFIG_DIR || join2(homedir2(), ".itwillsync");
3957
+ return join2(dir, "tool-history.json");
3958
+ }
3959
+ var ToolHistory = class {
3960
+ tools = [];
3961
+ constructor() {
3962
+ this.load();
3963
+ }
3964
+ /** Get tool names sorted by most recently used. */
3965
+ getTools() {
3966
+ return this.tools.sort((a, b) => b.lastUsed - a.lastUsed).map((t) => t.name);
3967
+ }
3968
+ /** Record a tool usage (add or update lastUsed). */
3969
+ recordUsage(toolName) {
3970
+ const existing = this.tools.find((t) => t.name === toolName);
3971
+ if (existing) {
3972
+ existing.lastUsed = Date.now();
3973
+ } else {
3974
+ this.tools.push({ name: toolName, lastUsed: Date.now() });
3975
+ }
3976
+ if (this.tools.length > MAX_ENTRIES) {
3977
+ this.tools.sort((a, b) => b.lastUsed - a.lastUsed);
3978
+ this.tools = this.tools.slice(0, MAX_ENTRIES);
3979
+ }
3980
+ this.save();
3981
+ }
3982
+ load() {
3983
+ const path = getHistoryPath();
3984
+ if (!existsSync2(path)) return;
3985
+ try {
3986
+ const raw = readFileSync2(path, "utf-8");
3987
+ const data = JSON.parse(raw);
3988
+ if (Array.isArray(data.tools)) {
3989
+ this.tools = data.tools;
3990
+ }
3991
+ } catch {
3992
+ }
3993
+ }
3994
+ save() {
3995
+ const path = getHistoryPath();
3996
+ mkdirSync2(dirname2(path), { recursive: true });
3997
+ const data = { tools: this.tools };
3998
+ writeFileSync2(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
3999
+ }
4000
+ };
4001
+
3926
4002
  // src/internal-api.ts
3927
4003
  import { createServer } from "http";
3928
4004
  function createInternalApi(options) {
@@ -4100,8 +4176,9 @@ function readBody(req) {
4100
4176
 
4101
4177
  // src/server.ts
4102
4178
  import { createServer as createServer2 } from "http";
4103
- import { readFile } from "fs/promises";
4104
- import { join as join2, extname } from "path";
4179
+ import { readFile, readdir, stat, realpath } from "fs/promises";
4180
+ import { join as join3, extname } from "path";
4181
+ import { homedir as homedir3 } from "os";
4105
4182
  import { gzipSync } from "zlib";
4106
4183
 
4107
4184
  // ../../node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
@@ -4124,7 +4201,8 @@ var MIME_TYPES = {
4124
4201
  var COMPRESSIBLE = /* @__PURE__ */ new Set([".html", ".js", ".css", ".json", ".svg"]);
4125
4202
  var PING_INTERVAL_MS = 3e4;
4126
4203
  function createDashboardServer(options) {
4127
- const { registry, masterToken, dashboardPath, host, port, previewCollector } = options;
4204
+ const { registry, masterToken, dashboardPath, host, port, previewCollector, toolHistory, onCreateSession } = options;
4205
+ const homeDir = homedir3();
4128
4206
  const clients = /* @__PURE__ */ new Set();
4129
4207
  const aliveMap = /* @__PURE__ */ new WeakMap();
4130
4208
  const gzipCache = /* @__PURE__ */ new Map();
@@ -4171,10 +4249,49 @@ function createDashboardServer(options) {
4171
4249
  res.end(JSON.stringify({ sessions }));
4172
4250
  return;
4173
4251
  }
4252
+ if (pathname === "/api/tool-history") {
4253
+ const tools = toolHistory ? toolHistory.getTools() : [];
4254
+ res.writeHead(200, { "Content-Type": "application/json" });
4255
+ res.end(JSON.stringify({ tools }));
4256
+ return;
4257
+ }
4258
+ if (pathname === "/api/recent-dirs") {
4259
+ const dirs = registry.getRecentDirectories();
4260
+ res.writeHead(200, { "Content-Type": "application/json" });
4261
+ res.end(JSON.stringify({ dirs }));
4262
+ return;
4263
+ }
4264
+ if (pathname === "/api/browse") {
4265
+ const rawPath = url.searchParams.get("path") || "~";
4266
+ const browsePath = rawPath.replace(/^~/, homeDir);
4267
+ try {
4268
+ const resolved = await realpath(browsePath);
4269
+ if (!resolved.startsWith(homeDir)) {
4270
+ res.writeHead(403, { "Content-Type": "application/json" });
4271
+ res.end(JSON.stringify({ error: "Access denied" }));
4272
+ return;
4273
+ }
4274
+ const dirStat = await stat(resolved);
4275
+ if (!dirStat.isDirectory()) {
4276
+ res.writeHead(400, { "Content-Type": "application/json" });
4277
+ res.end(JSON.stringify({ error: "Not a directory" }));
4278
+ return;
4279
+ }
4280
+ const entries = await readdir(resolved, { withFileTypes: true });
4281
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
4282
+ const displayPath = resolved.replace(homeDir, "~");
4283
+ res.writeHead(200, { "Content-Type": "application/json" });
4284
+ res.end(JSON.stringify({ path: displayPath, resolvedPath: resolved, entries: dirs }));
4285
+ } catch {
4286
+ res.writeHead(400, { "Content-Type": "application/json" });
4287
+ res.end(JSON.stringify({ error: "Cannot read directory" }));
4288
+ }
4289
+ return;
4290
+ }
4174
4291
  await serveStaticFile(dashboardPath, pathname, req, res);
4175
4292
  });
4176
4293
  async function serveStaticFile(basePath, filePath, req, res) {
4177
- const fullPath = join2(basePath, filePath);
4294
+ const fullPath = join3(basePath, filePath);
4178
4295
  const ext = extname(fullPath);
4179
4296
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
4180
4297
  try {
@@ -4280,6 +4397,32 @@ function createDashboardServer(options) {
4280
4397
  }
4281
4398
  break;
4282
4399
  }
4400
+ case "create-session": {
4401
+ if (!onCreateSession) {
4402
+ ws.send(JSON.stringify({ type: "session-create-error", error: "Session creation not available" }));
4403
+ break;
4404
+ }
4405
+ const tool = (msg.tool || "").trim();
4406
+ const rawCwd = (msg.cwd || "").trim();
4407
+ if (!tool) {
4408
+ ws.send(JSON.stringify({ type: "session-create-error", error: "Tool name is required" }));
4409
+ break;
4410
+ }
4411
+ const cwd = rawCwd ? rawCwd.replace(/^~/, homeDir) : homeDir;
4412
+ try {
4413
+ const resolved = await realpath(cwd);
4414
+ const dirStat = await stat(resolved);
4415
+ if (!dirStat.isDirectory()) {
4416
+ ws.send(JSON.stringify({ type: "session-create-error", error: "Not a directory" }));
4417
+ break;
4418
+ }
4419
+ ws.send(JSON.stringify({ type: "session-creating", tool, cwd: rawCwd || "~" }));
4420
+ onCreateSession(tool, resolved);
4421
+ } catch (err) {
4422
+ ws.send(JSON.stringify({ type: "session-create-error", error: err.message }));
4423
+ }
4424
+ break;
4425
+ }
4283
4426
  case "get-metadata": {
4284
4427
  const session = registry.getById(msg.sessionId);
4285
4428
  if (!session) {
@@ -4549,20 +4692,33 @@ var HUB_EXTERNAL_PORT = 7962;
4549
4692
  var HUB_INTERNAL_PORT = 7963;
4550
4693
  var AUTO_SHUTDOWN_DELAY_MS = 3e4;
4551
4694
  function getHubDir() {
4552
- return process.env.ITWILLSYNC_CONFIG_DIR || join3(homedir2(), ".itwillsync");
4695
+ return process.env.ITWILLSYNC_CONFIG_DIR || join4(homedir4(), ".itwillsync");
4553
4696
  }
4554
4697
  function getPidPath() {
4555
- return join3(getHubDir(), "hub.pid");
4698
+ return join4(getHubDir(), "hub.pid");
4556
4699
  }
4557
4700
  function getHubConfigPath() {
4558
- return join3(getHubDir(), "hub.json");
4701
+ return join4(getHubDir(), "hub.json");
4702
+ }
4703
+ function isValidToolName(tool) {
4704
+ return /^[a-zA-Z0-9._-]+$/.test(tool) && tool.length > 0 && tool.length <= 100;
4705
+ }
4706
+ function spawnSession(tool, cwd, cliEntryPath) {
4707
+ const child = spawn(process.execPath, [cliEntryPath, "--headless", "--", tool], {
4708
+ cwd,
4709
+ stdio: "ignore",
4710
+ detached: true,
4711
+ env: { ...process.env }
4712
+ });
4713
+ child.unref();
4714
+ return child;
4559
4715
  }
4560
4716
  async function main() {
4561
4717
  const hubDir = getHubDir();
4562
- mkdirSync2(hubDir, { recursive: true });
4718
+ mkdirSync3(hubDir, { recursive: true });
4563
4719
  const masterToken = generateToken();
4564
4720
  const startedAt = Date.now();
4565
- writeFileSync2(getPidPath(), String(process.pid), "utf-8");
4721
+ writeFileSync3(getPidPath(), String(process.pid), "utf-8");
4566
4722
  const hubConfig = {
4567
4723
  masterToken,
4568
4724
  externalPort: HUB_EXTERNAL_PORT,
@@ -4570,28 +4726,30 @@ async function main() {
4570
4726
  pid: process.pid,
4571
4727
  startedAt
4572
4728
  };
4573
- writeFileSync2(getHubConfigPath(), JSON.stringify(hubConfig, null, 2) + "\n", "utf-8");
4729
+ writeFileSync3(getHubConfigPath(), JSON.stringify(hubConfig, null, 2) + "\n", "utf-8");
4574
4730
  const sessionStore = new SessionStore();
4575
4731
  const registry = new SessionRegistry({ store: sessionStore });
4576
4732
  registry.startHealthChecks();
4577
- const logsDir = join3(hubDir, "logs");
4578
- if (existsSync2(logsDir)) {
4733
+ const toolHistory = new ToolHistory();
4734
+ const logsDir = join4(hubDir, "logs");
4735
+ if (existsSync3(logsDir)) {
4579
4736
  const retentionMs = 30 * 864e5;
4580
4737
  const cutoff = Date.now() - retentionMs;
4581
4738
  try {
4582
4739
  for (const file of readdirSync(logsDir)) {
4583
- const filePath = join3(logsDir, file);
4740
+ const filePath = join4(logsDir, file);
4584
4741
  try {
4585
- const stat = statSync(filePath);
4586
- if (stat.mtimeMs < cutoff) unlinkSync(filePath);
4742
+ const stat2 = statSync(filePath);
4743
+ if (stat2.mtimeMs < cutoff) unlinkSync(filePath);
4587
4744
  } catch {
4588
4745
  }
4589
4746
  }
4590
4747
  } catch {
4591
4748
  }
4592
4749
  }
4593
- const __dirname = dirname2(fileURLToPath(import.meta.url));
4594
- const dashboardPath = join3(__dirname, "dashboard");
4750
+ const __dirname = dirname3(fileURLToPath(import.meta.url));
4751
+ const dashboardPath = join4(__dirname, "dashboard");
4752
+ const cliEntryPath = join4(__dirname, "..", "index.js");
4595
4753
  const internalApi = createInternalApi({
4596
4754
  registry,
4597
4755
  port: HUB_INTERNAL_PORT
@@ -4603,7 +4761,15 @@ async function main() {
4603
4761
  dashboardPath,
4604
4762
  host: "0.0.0.0",
4605
4763
  port: HUB_EXTERNAL_PORT,
4606
- previewCollector
4764
+ previewCollector,
4765
+ toolHistory,
4766
+ onCreateSession: (tool, cwd) => {
4767
+ if (!isValidToolName(tool)) {
4768
+ throw new Error(`Invalid tool name: ${tool}`);
4769
+ }
4770
+ toolHistory.recordUsage(tool);
4771
+ spawnSession(tool, cwd, cliEntryPath);
4772
+ }
4607
4773
  });
4608
4774
  await Promise.all([
4609
4775
  internalApi.listen(),