hotsheet 0.15.4 → 0.16.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.
package/dist/cli.js CHANGED
@@ -15053,7 +15053,8 @@ function buildLogWhereClause(filters) {
15053
15053
  }
15054
15054
  if (filters?.search !== void 0 && filters.search !== "") {
15055
15055
  conditions.push(`(summary ILIKE $${paramIdx} OR detail ILIKE $${paramIdx})`);
15056
- params.push(`%${filters.search}%`);
15056
+ const escaped = filters.search.replace(/[%_\\]/g, "\\$&");
15057
+ params.push(`%${escaped}%`);
15057
15058
  paramIdx++;
15058
15059
  }
15059
15060
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -15337,6 +15338,9 @@ var init_tags = __esm({
15337
15338
  });
15338
15339
 
15339
15340
  // src/db/tickets.ts
15341
+ function escapeIlike(value) {
15342
+ return value.replace(/[%_\\]/g, "\\$&");
15343
+ }
15340
15344
  async function nextTicketNumber(prefix = "HS") {
15341
15345
  const db = await getDb();
15342
15346
  const result = await db.query("SELECT nextval('ticket_seq')");
@@ -15482,7 +15486,7 @@ function buildTicketWhereClause(filters) {
15482
15486
  }
15483
15487
  if (filters.search !== void 0 && filters.search !== "") {
15484
15488
  conditions.push(`(title ILIKE $${paramIdx} OR details ILIKE $${paramIdx} OR ticket_number ILIKE $${paramIdx} OR tags ILIKE $${paramIdx})`);
15485
- values.push(`%${filters.search}%`);
15489
+ values.push(`%${escapeIlike(filters.search)}%`);
15486
15490
  paramIdx++;
15487
15491
  }
15488
15492
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -15625,12 +15629,12 @@ async function queryTickets(logic, conditions, sortBy, sortDir, requiredTag, inc
15625
15629
  break;
15626
15630
  case "contains":
15627
15631
  userWhere.push(`${field} ILIKE $${paramIdx}`);
15628
- values.push(`%${cond.value}%`);
15632
+ values.push(`%${escapeIlike(cond.value)}%`);
15629
15633
  paramIdx++;
15630
15634
  break;
15631
15635
  case "not_contains":
15632
15636
  userWhere.push(`${field} NOT ILIKE $${paramIdx}`);
15633
- values.push(`%${cond.value}%`);
15637
+ values.push(`%${escapeIlike(cond.value)}%`);
15634
15638
  paramIdx++;
15635
15639
  break;
15636
15640
  }
@@ -16340,7 +16344,7 @@ var init_skills = __esm({
16340
16344
  "use strict";
16341
16345
  init_file_settings();
16342
16346
  init_types();
16343
- SKILL_VERSION = 7;
16347
+ SKILL_VERSION = 8;
16344
16348
  skillCategories = DEFAULT_CATEGORIES;
16345
16349
  HOTSHEET_ALLOW_PATTERNS = [
16346
16350
  "Bash(curl * http://localhost:417*/api/*)",
@@ -16534,13 +16538,15 @@ async function buildWorkflowInstructions(port, secretHeader) {
16534
16538
  sections.push("");
16535
16539
  sections.push("## Requesting User Feedback");
16536
16540
  sections.push("");
16537
- sections.push("When you need input from the user before continuing, add a note with a special prefix:");
16541
+ sections.push("When you need input from the user before continuing, add a note where the **entire note text begins** with one of these exact prefixes:");
16538
16542
  sections.push("");
16539
- sections.push("- **Standard feedback**: Add a note starting with `FEEDBACK NEEDED:` followed by your question");
16543
+ sections.push("- **Standard feedback**: `FEEDBACK NEEDED: Your question here`");
16540
16544
  sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"notes": "FEEDBACK NEEDED: Your question here"}'\``);
16541
- sections.push("- **Urgent feedback** (auto-selects the ticket in the UI): Use `IMMEDIATE FEEDBACK NEEDED:` instead");
16545
+ sections.push("- **Urgent feedback** (auto-selects the ticket in the UI): `IMMEDIATE FEEDBACK NEEDED: Your question here`");
16542
16546
  sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"notes": "IMMEDIATE FEEDBACK NEEDED: Your urgent question"}'\``);
16543
16547
  sections.push("");
16548
+ sections.push('**IMPORTANT:** The prefix must be the very first characters of the note \u2014 do not add any text before it. The note text sent in the `"notes"` field must start with `FEEDBACK NEEDED:` or `IMMEDIATE FEEDBACK NEEDED:` exactly.');
16549
+ sections.push("");
16544
16550
  sections.push("After adding a feedback note, signal done and wait to be re-triggered. The user will see a dialog prompting them to respond. When they submit feedback, you will be re-triggered with a message indicating the ticket was updated.");
16545
16551
  sections.push("");
16546
16552
  sections.push('Only the most recent note is checked for feedback prefixes. Once the user responds (or clicks "No Response Needed"), the feedback state clears automatically.');
@@ -18121,6 +18127,150 @@ var init_notify = __esm({
18121
18127
  }
18122
18128
  });
18123
18129
 
18130
+ // src/routes/validation.ts
18131
+ function parseBody(schema, data) {
18132
+ const result = schema.safeParse(data);
18133
+ if (!result.success) {
18134
+ const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).filter((m) => m !== ": ");
18135
+ return { success: false, error: messages.join("; ") || "Invalid request body" };
18136
+ }
18137
+ return { success: true, data: result.data };
18138
+ }
18139
+ var TicketPrioritySchema, TicketStatusSchema, SortBySchema, SortDirSchema, CreateTicketSchema, UpdateTicketSchema, BatchActionSchema, DuplicateSchema, NotesEditSchema, NotesBulkSchema, QueryTicketsSchema, UpdateSettingsSchema, BackupTierSchema, CreateBackupSchema, RestoreBackupSchema, ShellExecSchema, ShellKillSchema, ChannelTriggerSchema, PermissionRespondSchema, RegisterProjectSchema, ReorderProjectsSchema, CategoryDefSchema, UpdateCategoriesSchema, PrintSchema, GlobalConfigSchema, PluginActionSchema, PluginValidateSchema, PluginSyncScheduleSchema, PluginConflictResolveSchema, PluginInstallSchema, PluginGlobalConfigSchema, ChannelHeartbeatSchema;
18140
+ var init_validation = __esm({
18141
+ "src/routes/validation.ts"() {
18142
+ "use strict";
18143
+ init_zod();
18144
+ TicketPrioritySchema = external_exports.enum(["highest", "high", "default", "low", "lowest"]);
18145
+ TicketStatusSchema = external_exports.enum(["not_started", "started", "completed", "verified", "backlog", "archive", "deleted"]);
18146
+ SortBySchema = external_exports.enum(["created", "priority", "category", "status"]);
18147
+ SortDirSchema = external_exports.enum(["asc", "desc"]);
18148
+ CreateTicketSchema = external_exports.object({
18149
+ title: external_exports.string().optional().default(""),
18150
+ defaults: external_exports.object({
18151
+ category: external_exports.string().optional(),
18152
+ priority: TicketPrioritySchema.or(external_exports.literal("")).optional(),
18153
+ status: TicketStatusSchema.or(external_exports.literal("")).optional(),
18154
+ up_next: external_exports.boolean().optional(),
18155
+ details: external_exports.string().optional(),
18156
+ tags: external_exports.string().optional()
18157
+ }).optional()
18158
+ });
18159
+ UpdateTicketSchema = external_exports.object({
18160
+ title: external_exports.string().optional(),
18161
+ details: external_exports.string().optional(),
18162
+ notes: external_exports.string().optional(),
18163
+ tags: external_exports.string().optional(),
18164
+ category: external_exports.string().optional(),
18165
+ priority: TicketPrioritySchema.optional(),
18166
+ status: TicketStatusSchema.optional(),
18167
+ up_next: external_exports.boolean().optional(),
18168
+ last_read_at: external_exports.string().nullable().optional()
18169
+ });
18170
+ BatchActionSchema = external_exports.object({
18171
+ ids: external_exports.array(external_exports.number().int()),
18172
+ action: external_exports.enum(["delete", "restore", "category", "priority", "status", "up_next", "mark_read", "mark_unread"]),
18173
+ value: external_exports.union([external_exports.string(), external_exports.boolean()]).optional()
18174
+ });
18175
+ DuplicateSchema = external_exports.object({
18176
+ ids: external_exports.array(external_exports.number().int())
18177
+ });
18178
+ NotesEditSchema = external_exports.object({
18179
+ text: external_exports.string()
18180
+ });
18181
+ NotesBulkSchema = external_exports.object({
18182
+ notes: external_exports.string()
18183
+ });
18184
+ QueryTicketsSchema = external_exports.object({
18185
+ logic: external_exports.enum(["all", "any"]),
18186
+ conditions: external_exports.array(external_exports.object({
18187
+ field: external_exports.enum(["category", "priority", "status", "title", "details", "up_next", "tags"]),
18188
+ operator: external_exports.enum(["equals", "not_equals", "contains", "not_contains", "lt", "lte", "gt", "gte"]),
18189
+ value: external_exports.string()
18190
+ })),
18191
+ sort_by: external_exports.string().optional(),
18192
+ sort_dir: SortDirSchema.optional(),
18193
+ required_tag: external_exports.string().optional(),
18194
+ include_archived: external_exports.boolean().optional()
18195
+ });
18196
+ UpdateSettingsSchema = external_exports.record(external_exports.string(), external_exports.string());
18197
+ BackupTierSchema = external_exports.enum(["5min", "hourly", "daily"]);
18198
+ CreateBackupSchema = external_exports.object({
18199
+ tier: BackupTierSchema
18200
+ });
18201
+ RestoreBackupSchema = external_exports.object({
18202
+ tier: external_exports.string().min(1),
18203
+ filename: external_exports.string().min(1)
18204
+ });
18205
+ ShellExecSchema = external_exports.object({
18206
+ command: external_exports.string().min(1, "Command cannot be empty"),
18207
+ name: external_exports.string().optional()
18208
+ });
18209
+ ShellKillSchema = external_exports.object({
18210
+ id: external_exports.number().int()
18211
+ });
18212
+ ChannelTriggerSchema = external_exports.object({
18213
+ message: external_exports.string().optional()
18214
+ });
18215
+ PermissionRespondSchema = external_exports.object({
18216
+ request_id: external_exports.string(),
18217
+ behavior: external_exports.enum(["allow", "deny"]),
18218
+ tool_name: external_exports.string().optional()
18219
+ });
18220
+ RegisterProjectSchema = external_exports.object({
18221
+ dataDir: external_exports.string().min(1, "dataDir is required")
18222
+ });
18223
+ ReorderProjectsSchema = external_exports.object({
18224
+ secrets: external_exports.array(external_exports.string())
18225
+ });
18226
+ CategoryDefSchema = external_exports.object({
18227
+ id: external_exports.string().min(1),
18228
+ label: external_exports.string().min(1),
18229
+ shortLabel: external_exports.string().min(1),
18230
+ color: external_exports.string().min(1),
18231
+ shortcutKey: external_exports.string(),
18232
+ description: external_exports.string()
18233
+ }).loose();
18234
+ UpdateCategoriesSchema = external_exports.array(CategoryDefSchema).min(1);
18235
+ PrintSchema = external_exports.object({
18236
+ html: external_exports.string()
18237
+ });
18238
+ GlobalConfigSchema = external_exports.object({
18239
+ channelEnabled: external_exports.boolean().optional(),
18240
+ shareTotalSeconds: external_exports.number().optional(),
18241
+ shareLastPrompted: external_exports.string().optional(),
18242
+ shareAccepted: external_exports.boolean().optional()
18243
+ }).strict();
18244
+ PluginActionSchema = external_exports.object({
18245
+ actionId: external_exports.string(),
18246
+ ticketIds: external_exports.array(external_exports.number().int()).optional(),
18247
+ value: external_exports.unknown().optional()
18248
+ });
18249
+ PluginValidateSchema = external_exports.object({
18250
+ key: external_exports.string(),
18251
+ value: external_exports.string()
18252
+ });
18253
+ PluginSyncScheduleSchema = external_exports.object({
18254
+ interval_minutes: external_exports.number().nullable()
18255
+ });
18256
+ PluginConflictResolveSchema = external_exports.object({
18257
+ plugin_id: external_exports.string().min(1, "plugin_id is required"),
18258
+ resolution: external_exports.enum(["keep_local", "keep_remote"])
18259
+ });
18260
+ PluginInstallSchema = external_exports.object({
18261
+ path: external_exports.string().min(1, "path is required")
18262
+ });
18263
+ PluginGlobalConfigSchema = external_exports.object({
18264
+ key: external_exports.string().min(1, "key is required"),
18265
+ value: external_exports.string()
18266
+ });
18267
+ ChannelHeartbeatSchema = external_exports.object({
18268
+ projectDir: external_exports.string().optional(),
18269
+ state: external_exports.enum(["busy", "idle", "heartbeat"]).optional()
18270
+ });
18271
+ }
18272
+ });
18273
+
18124
18274
  // src/channel-config.ts
18125
18275
  var channel_config_exports = {};
18126
18276
  __export(channel_config_exports, {
@@ -18339,6 +18489,129 @@ var init_global_config = __esm({
18339
18489
  }
18340
18490
  });
18341
18491
 
18492
+ // src/claude-hooks.ts
18493
+ var claude_hooks_exports = {};
18494
+ __export(claude_hooks_exports, {
18495
+ installHeartbeatHook: () => installHeartbeatHook,
18496
+ isHeartbeatHookInstalled: () => isHeartbeatHookInstalled,
18497
+ removeHeartbeatHook: () => removeHeartbeatHook
18498
+ });
18499
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
18500
+ import { homedir as homedir4 } from "os";
18501
+ import { join as join13 } from "path";
18502
+ function getClaudeSettingsPath() {
18503
+ return join13(homedir4(), ".claude", "settings.json");
18504
+ }
18505
+ function readClaudeSettings() {
18506
+ const path = getClaudeSettingsPath();
18507
+ if (!existsSync11(path)) return {};
18508
+ try {
18509
+ return JSON.parse(readFileSync10(path, "utf-8"));
18510
+ } catch {
18511
+ return {};
18512
+ }
18513
+ }
18514
+ function writeClaudeSettings(settings) {
18515
+ const path = getClaudeSettingsPath();
18516
+ mkdirSync8(join13(homedir4(), ".claude"), { recursive: true });
18517
+ if (existsSync11(path)) {
18518
+ copyFileSync(path, path + ".bak");
18519
+ }
18520
+ writeFileSync10(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
18521
+ }
18522
+ function isHeartbeatHookInstalled() {
18523
+ const settings = readClaudeSettings();
18524
+ if (!settings.hooks) return false;
18525
+ for (const groups of Object.values(settings.hooks)) {
18526
+ if (!Array.isArray(groups)) continue;
18527
+ for (const group of groups) {
18528
+ if (group.hooks.some((h) => h.command.includes(HOOK_MARKER))) return true;
18529
+ }
18530
+ }
18531
+ return false;
18532
+ }
18533
+ function makeCommand(port, state) {
18534
+ return `curl -s -X POST http://localhost:${port}/api/channel/heartbeat -H "Content-Type: application/json" -d '{"projectDir":"'$CLAUDE_PROJECT_DIR'","state":"${state}"}' >/dev/null 2>&1 & # ${HOOK_MARKER}`;
18535
+ }
18536
+ function installHeartbeatHook(port) {
18537
+ if (isHeartbeatHookInstalled()) {
18538
+ updateHeartbeatHookPort(port);
18539
+ return;
18540
+ }
18541
+ const settings = readClaudeSettings();
18542
+ if (!settings.hooks) settings.hooks = {};
18543
+ const hookDefs = [
18544
+ { event: "PostToolUse", state: "heartbeat" },
18545
+ { event: "UserPromptSubmit", state: "busy" },
18546
+ { event: "Stop", state: "idle" }
18547
+ ];
18548
+ for (const def of hookDefs) {
18549
+ if (!Array.isArray(settings.hooks[def.event])) settings.hooks[def.event] = [];
18550
+ settings.hooks[def.event].push({
18551
+ hooks: [{ "//": "Hot Sheet", type: "command", command: makeCommand(port, def.state), timeout: 5 }]
18552
+ });
18553
+ }
18554
+ writeClaudeSettings(settings);
18555
+ console.log("[hooks] Installed Claude Code hooks (PostToolUse, UserPromptSubmit, Stop)");
18556
+ }
18557
+ function updateHeartbeatHookPort(port) {
18558
+ const settings = readClaudeSettings();
18559
+ if (!settings.hooks) return;
18560
+ let changed = false;
18561
+ for (const groups of Object.values(settings.hooks)) {
18562
+ if (!Array.isArray(groups)) continue;
18563
+ for (const group of groups) {
18564
+ for (const hook of group.hooks) {
18565
+ if (hook.command.includes(HOOK_MARKER)) {
18566
+ const updated = hook.command.replace(/localhost:\d+/, `localhost:${port}`);
18567
+ if (updated !== hook.command) {
18568
+ hook.command = updated;
18569
+ changed = true;
18570
+ }
18571
+ if (hook["//"] === void 0) {
18572
+ hook["//"] = "Hot Sheet";
18573
+ changed = true;
18574
+ }
18575
+ }
18576
+ }
18577
+ }
18578
+ }
18579
+ if (changed) {
18580
+ writeClaudeSettings(settings);
18581
+ console.log(`[hooks] Updated heartbeat hook port to ${port}`);
18582
+ }
18583
+ }
18584
+ function removeHeartbeatHook() {
18585
+ const settings = readClaudeSettings();
18586
+ if (!settings.hooks) return;
18587
+ let changed = false;
18588
+ for (const [event, groups] of Object.entries(settings.hooks)) {
18589
+ if (!Array.isArray(groups)) continue;
18590
+ const filtered = groups.filter(
18591
+ (group) => !group.hooks.some((h) => h.command.includes(HOOK_MARKER))
18592
+ );
18593
+ if (filtered.length !== groups.length) {
18594
+ changed = true;
18595
+ if (filtered.length === 0) {
18596
+ Reflect.deleteProperty(settings.hooks, event);
18597
+ } else {
18598
+ settings.hooks[event] = filtered;
18599
+ }
18600
+ }
18601
+ }
18602
+ if (!changed) return;
18603
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
18604
+ writeClaudeSettings(settings);
18605
+ console.log("[hooks] Removed Claude Code heartbeat hooks");
18606
+ }
18607
+ var HOOK_MARKER;
18608
+ var init_claude_hooks = __esm({
18609
+ "src/claude-hooks.ts"() {
18610
+ "use strict";
18611
+ HOOK_MARKER = "hotsheet-heartbeat";
18612
+ }
18613
+ });
18614
+
18342
18615
  // src/db/sync.ts
18343
18616
  async function getSyncRecord(ticketId, pluginId) {
18344
18617
  const db = await getDb();
@@ -18629,6 +18902,7 @@ var init_keychain = __esm({
18629
18902
  // src/plugins/loader.ts
18630
18903
  var loader_exports = {};
18631
18904
  __export(loader_exports, {
18905
+ compareSemver: () => compareSemver,
18632
18906
  disablePlugin: () => disablePlugin,
18633
18907
  discoverPlugins: () => discoverPlugins,
18634
18908
  dismissBundledPlugin: () => dismissBundledPlugin,
@@ -18651,10 +18925,19 @@ __export(loader_exports, {
18651
18925
  unloadAllPlugins: () => unloadAllPlugins,
18652
18926
  unregisterPlugin: () => unregisterPlugin
18653
18927
  });
18654
- import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync10, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync11 } from "fs";
18655
- import { homedir as homedir5 } from "os";
18656
- import { dirname as dirname3, join as join14 } from "path";
18928
+ import { cpSync, existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync11, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync12 } from "fs";
18929
+ import { homedir as homedir6 } from "os";
18930
+ import { dirname as dirname3, join as join15 } from "path";
18657
18931
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
18932
+ function compareSemver(a, b) {
18933
+ const pa = a.split(".").map(Number);
18934
+ const pb = b.split(".").map(Number);
18935
+ for (let i = 0; i < 3; i++) {
18936
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
18937
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
18938
+ }
18939
+ return 0;
18940
+ }
18658
18941
  function getConfigLabelOverride(pluginId, labelId) {
18659
18942
  return configLabelOverrides.get(`${pluginId}:${labelId}`);
18660
18943
  }
@@ -18686,15 +18969,15 @@ function getAllBackends() {
18686
18969
  return getLoadedPlugins().filter((p) => p.backend !== null && p.enabled).map((p) => p.backend);
18687
18970
  }
18688
18971
  function getPluginDir() {
18689
- return join14(homedir5(), ".hotsheet", "plugins");
18972
+ return join15(homedir6(), ".hotsheet", "plugins");
18690
18973
  }
18691
18974
  function discoverPlugins() {
18692
18975
  const pluginDir = getPluginDir();
18693
- if (!existsSync12(pluginDir)) return [];
18976
+ if (!existsSync13(pluginDir)) return [];
18694
18977
  const results = [];
18695
18978
  for (const entry of readdirSync3(pluginDir, { withFileTypes: true })) {
18696
18979
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
18697
- const pluginPath = join14(pluginDir, entry.name);
18980
+ const pluginPath = join15(pluginDir, entry.name);
18698
18981
  if (entry.isSymbolicLink()) {
18699
18982
  try {
18700
18983
  if (!statSync2(pluginPath).isDirectory()) continue;
@@ -18708,20 +18991,20 @@ function discoverPlugins() {
18708
18991
  return results;
18709
18992
  }
18710
18993
  function readManifest(pluginPath) {
18711
- const manifestPath = join14(pluginPath, "manifest.json");
18712
- if (existsSync12(manifestPath)) {
18994
+ const manifestPath = join15(pluginPath, "manifest.json");
18995
+ if (existsSync13(manifestPath)) {
18713
18996
  try {
18714
- const raw = JSON.parse(readFileSync10(manifestPath, "utf-8"));
18997
+ const raw = JSON.parse(readFileSync11(manifestPath, "utf-8"));
18715
18998
  return validateManifest(raw);
18716
18999
  } catch (e) {
18717
19000
  console.warn(`[plugins] Invalid manifest.json in ${pluginPath}: ${getErrorMessage(e)}`);
18718
19001
  return null;
18719
19002
  }
18720
19003
  }
18721
- const pkgPath = join14(pluginPath, "package.json");
18722
- if (existsSync12(pkgPath)) {
19004
+ const pkgPath = join15(pluginPath, "package.json");
19005
+ if (existsSync13(pkgPath)) {
18723
19006
  try {
18724
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
19007
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
18725
19008
  const hotsheet = pkg.hotsheet;
18726
19009
  if (hotsheet != null) {
18727
19010
  const author = pkg.author;
@@ -18747,13 +19030,13 @@ function validateManifest(raw) {
18747
19030
  return result.data;
18748
19031
  }
18749
19032
  function getDismissedPluginsPath() {
18750
- return join14(homedir5(), ".hotsheet", "dismissed-plugins.json");
19033
+ return join15(homedir6(), ".hotsheet", "dismissed-plugins.json");
18751
19034
  }
18752
19035
  function getDismissedPlugins() {
18753
19036
  const path = getDismissedPluginsPath();
18754
- if (!existsSync12(path)) return /* @__PURE__ */ new Set();
19037
+ if (!existsSync13(path)) return /* @__PURE__ */ new Set();
18755
19038
  try {
18756
- const result = external_exports.array(external_exports.string()).safeParse(JSON.parse(readFileSync10(path, "utf-8")));
19039
+ const result = external_exports.array(external_exports.string()).safeParse(JSON.parse(readFileSync11(path, "utf-8")));
18757
19040
  return result.success ? new Set(result.data) : /* @__PURE__ */ new Set();
18758
19041
  } catch {
18759
19042
  return /* @__PURE__ */ new Set();
@@ -18763,19 +19046,19 @@ function dismissBundledPlugin(pluginId) {
18763
19046
  const dismissed = getDismissedPlugins();
18764
19047
  dismissed.add(pluginId);
18765
19048
  const path = getDismissedPluginsPath();
18766
- mkdirSync8(dirname3(path), { recursive: true });
18767
- writeFileSync11(path, JSON.stringify([...dismissed]));
19049
+ mkdirSync9(dirname3(path), { recursive: true });
19050
+ writeFileSync12(path, JSON.stringify([...dismissed]));
18768
19051
  }
18769
19052
  function undismissBundledPlugin(pluginId) {
18770
19053
  const dismissed = getDismissedPlugins();
18771
19054
  dismissed.delete(pluginId);
18772
- writeFileSync11(getDismissedPluginsPath(), JSON.stringify([...dismissed]));
19055
+ writeFileSync12(getDismissedPluginsPath(), JSON.stringify([...dismissed]));
18773
19056
  }
18774
19057
  function getBundledDir() {
18775
19058
  const selfDir = dirname3(fileURLToPath2(import.meta.url));
18776
- let bundledDir = join14(selfDir, "plugins");
18777
- if (!existsSync12(bundledDir)) bundledDir = join14(process.cwd(), "dist", "plugins");
18778
- return existsSync12(bundledDir) ? bundledDir : null;
19059
+ let bundledDir = join15(selfDir, "plugins");
19060
+ if (!existsSync13(bundledDir)) bundledDir = join15(process.cwd(), "dist", "plugins");
19061
+ return existsSync13(bundledDir) ? bundledDir : null;
18779
19062
  }
18780
19063
  function listBundledPlugins() {
18781
19064
  const bundledDir = getBundledDir();
@@ -18784,7 +19067,7 @@ function listBundledPlugins() {
18784
19067
  const results = [];
18785
19068
  for (const entry of readdirSync3(bundledDir, { withFileTypes: true })) {
18786
19069
  if (!entry.isDirectory()) continue;
18787
- const manifest = readManifest(join14(bundledDir, entry.name));
19070
+ const manifest = readManifest(join15(bundledDir, entry.name));
18788
19071
  if (!manifest) continue;
18789
19072
  results.push({
18790
19073
  manifest,
@@ -18798,15 +19081,15 @@ function installBundledPlugin(pluginId) {
18798
19081
  const bundledDir = getBundledDir();
18799
19082
  if (bundledDir == null) return false;
18800
19083
  const pluginDir = getPluginDir();
18801
- mkdirSync8(pluginDir, { recursive: true });
19084
+ mkdirSync9(pluginDir, { recursive: true });
18802
19085
  for (const entry of readdirSync3(bundledDir, { withFileTypes: true })) {
18803
19086
  if (!entry.isDirectory()) continue;
18804
- const manifest = readManifest(join14(bundledDir, entry.name));
19087
+ const manifest = readManifest(join15(bundledDir, entry.name));
18805
19088
  if (!manifest || manifest.id !== pluginId) continue;
18806
- const targetPath = join14(pluginDir, entry.name);
19089
+ const targetPath = join15(pluginDir, entry.name);
18807
19090
  try {
18808
- if (existsSync12(targetPath)) rmSync7(targetPath, { recursive: true, force: true });
18809
- cpSync(join14(bundledDir, entry.name), targetPath, { recursive: true, force: true });
19091
+ if (existsSync13(targetPath)) rmSync7(targetPath, { recursive: true, force: true });
19092
+ cpSync(join15(bundledDir, entry.name), targetPath, { recursive: true, force: true });
18810
19093
  undismissBundledPlugin(pluginId);
18811
19094
  console.log(`[plugins] Installed bundled plugin: ${manifest.name}`);
18812
19095
  return true;
@@ -18821,21 +19104,21 @@ function installBundledPlugins() {
18821
19104
  const bundledDir = getBundledDir();
18822
19105
  if (bundledDir == null) return;
18823
19106
  const pluginDir = getPluginDir();
18824
- mkdirSync8(pluginDir, { recursive: true });
19107
+ mkdirSync9(pluginDir, { recursive: true });
18825
19108
  const dismissed = getDismissedPlugins();
18826
19109
  for (const entry of readdirSync3(bundledDir, { withFileTypes: true })) {
18827
19110
  if (!entry.isDirectory()) continue;
18828
- const bundledManifestCheck = readManifest(join14(bundledDir, entry.name));
19111
+ const bundledManifestCheck = readManifest(join15(bundledDir, entry.name));
18829
19112
  if (bundledManifestCheck && dismissed.has(bundledManifestCheck.id)) continue;
18830
- const targetPath = join14(pluginDir, entry.name);
18831
- const sourcePath = join14(bundledDir, entry.name);
18832
- if (existsSync12(targetPath)) {
19113
+ const targetPath = join15(pluginDir, entry.name);
19114
+ const sourcePath = join15(bundledDir, entry.name);
19115
+ if (existsSync13(targetPath)) {
18833
19116
  const installedManifest = readManifest(targetPath);
18834
19117
  const bundledManifest = readManifest(sourcePath);
18835
19118
  if (installedManifest && bundledManifest) {
18836
19119
  const entryFile = installedManifest.entry ?? "index.js";
18837
- const entryExists = existsSync12(join14(targetPath, entryFile));
18838
- if (entryExists && installedManifest.version >= bundledManifest.version) {
19120
+ const entryExists = existsSync13(join15(targetPath, entryFile));
19121
+ if (entryExists && compareSemver(installedManifest.version, bundledManifest.version) >= 0) {
18839
19122
  continue;
18840
19123
  }
18841
19124
  }
@@ -18864,8 +19147,8 @@ async function loadAllPlugins(enabledPlugins) {
18864
19147
  }
18865
19148
  async function loadPlugin(pluginPath, manifest, enabled) {
18866
19149
  const entry = manifest.entry ?? "index.js";
18867
- const entryPath = join14(pluginPath, entry);
18868
- if (!existsSync12(entryPath)) {
19150
+ const entryPath = join15(pluginPath, entry);
19151
+ if (!existsSync13(entryPath)) {
18869
19152
  console.warn(`[plugins] Plugin ${manifest.id}: entry point ${entry} not found`);
18870
19153
  loadedPlugins.set(manifest.id, {
18871
19154
  manifest,
@@ -18916,13 +19199,13 @@ async function loadPlugin(pluginPath, manifest, enabled) {
18916
19199
  }
18917
19200
  }
18918
19201
  function getGlobalConfigPath() {
18919
- return join14(homedir5(), ".hotsheet", "plugin-config.json");
19202
+ return join15(homedir6(), ".hotsheet", "plugin-config.json");
18920
19203
  }
18921
19204
  function readGlobalConfig2() {
18922
19205
  const configPath = getGlobalConfigPath();
18923
- if (!existsSync12(configPath)) return {};
19206
+ if (!existsSync13(configPath)) return {};
18924
19207
  try {
18925
- const result = external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.string()).optional()).safeParse(JSON.parse(readFileSync10(configPath, "utf-8")));
19208
+ const result = external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.string()).optional()).safeParse(JSON.parse(readFileSync11(configPath, "utf-8")));
18926
19209
  return result.success ? result.data : {};
18927
19210
  } catch {
18928
19211
  return {};
@@ -18930,8 +19213,8 @@ function readGlobalConfig2() {
18930
19213
  }
18931
19214
  function writeGlobalConfig2(config2) {
18932
19215
  const configPath = getGlobalConfigPath();
18933
- mkdirSync8(dirname3(configPath), { recursive: true });
18934
- writeFileSync11(configPath, JSON.stringify(config2, null, 2));
19216
+ mkdirSync9(dirname3(configPath), { recursive: true });
19217
+ writeFileSync12(configPath, JSON.stringify(config2, null, 2));
18935
19218
  }
18936
19219
  function getGlobalPluginSetting(pluginId, key) {
18937
19220
  const config2 = readGlobalConfig2();
@@ -19401,29 +19684,11 @@ async function syncSingleTicketContent(backend, ticketId, remoteId) {
19401
19684
  }
19402
19685
  }
19403
19686
  }
19404
- async function syncTicketComments(backend, ticketId, remoteId) {
19405
- if (!backend.getComments) return;
19406
- const ticket = await getTicket(ticketId);
19407
- if (!ticket) return;
19408
- const localNotes = parseNotes(ticket.notes);
19409
- let remoteComments;
19410
- try {
19411
- remoteComments = await backend.getComments(remoteId);
19412
- } catch (e) {
19413
- const msg = getErrorMessage(e);
19414
- if (msg.includes("404") || msg.includes("410") || msg.includes("Not Found")) {
19415
- throw e;
19416
- }
19417
- return;
19418
- }
19419
- const mappings = await getNoteSyncRecords(ticketId, backend.id);
19420
- const noteIdToMapping = new Map(mappings.map((m) => [m.note_id, m]));
19421
- const isAttMapping = (noteId2) => noteId2.startsWith("att_");
19422
- let changed = false;
19423
- const localNoteById = new Map(localNotes.map((n) => [n.id, n]));
19424
- const remoteCommentById = new Map(remoteComments.map((c) => [c.id, c]));
19687
+ async function reconcileExistingMappings(ctx) {
19688
+ const { backend, ticketId, remoteId, localNotes, localNoteById, mappings } = ctx;
19689
+ const remoteCommentById = new Map(ctx.remoteComments.map((c) => [c.id, c]));
19425
19690
  for (const mapping of mappings) {
19426
- if (isAttMapping(mapping.note_id)) continue;
19691
+ if (mapping.note_id.startsWith("att_")) continue;
19427
19692
  const localNote = localNoteById.get(mapping.note_id);
19428
19693
  const remoteComment = remoteCommentById.get(mapping.remote_comment_id);
19429
19694
  const base = mapping.last_synced_text ?? null;
@@ -19447,7 +19712,7 @@ async function syncTicketComments(backend, ticketId, remoteId) {
19447
19712
  }
19448
19713
  } else if (remoteChanged && !localChanged) {
19449
19714
  localNote.text = remoteText;
19450
- changed = true;
19715
+ ctx.changed = true;
19451
19716
  await upsertNoteSyncRecord(ticketId, mapping.note_id, backend.id, mapping.remote_comment_id, remoteText);
19452
19717
  } else if (localChanged && remoteChanged) {
19453
19718
  if (backend.updateComment) {
@@ -19479,21 +19744,22 @@ async function syncTicketComments(backend, ticketId, remoteId) {
19479
19744
  if (idx >= 0) {
19480
19745
  localNotes.splice(idx, 1);
19481
19746
  localNoteById.delete(mapping.note_id);
19482
- changed = true;
19747
+ ctx.changed = true;
19483
19748
  }
19484
19749
  await deleteNoteSyncRecord(ticketId, mapping.note_id, backend.id);
19485
19750
  continue;
19486
19751
  }
19487
19752
  await deleteNoteSyncRecord(ticketId, mapping.note_id, backend.id);
19488
19753
  }
19754
+ }
19755
+ async function pullNewRemoteComments(ctx) {
19756
+ const { backend, ticketId, localNotes, localNoteById, remoteComments, mappings, noteIdToMapping } = ctx;
19489
19757
  const mappedRemoteIds = new Set(mappings.map((m) => m.remote_comment_id));
19490
19758
  const localTexts = new Set(localNotes.map((n) => n.text.trim()));
19491
19759
  for (const comment of remoteComments) {
19492
19760
  if (mappedRemoteIds.has(comment.id)) continue;
19493
19761
  if (localTexts.has(comment.text.trim())) {
19494
- const existing = localNotes.find(
19495
- (n) => n.text.trim() === comment.text.trim() && !noteIdToMapping.has(n.id)
19496
- );
19762
+ const existing = localNotes.find((n) => n.text.trim() === comment.text.trim() && !noteIdToMapping.has(n.id));
19497
19763
  if (existing) {
19498
19764
  await upsertNoteSyncRecord(ticketId, existing.id, backend.id, comment.id, existing.text);
19499
19765
  noteIdToMapping.set(existing.id, {
@@ -19513,21 +19779,22 @@ async function syncTicketComments(backend, ticketId, remoteId) {
19513
19779
  localNoteById.set(noteId2, localNotes[localNotes.length - 1]);
19514
19780
  await upsertNoteSyncRecord(ticketId, noteId2, backend.id, comment.id, comment.text);
19515
19781
  localTexts.add(comment.text.trim());
19516
- changed = true;
19782
+ ctx.changed = true;
19517
19783
  }
19784
+ }
19785
+ async function pushNewLocalNotes(ctx) {
19786
+ const { backend, ticketId, remoteId, localNotes, remoteComments, mappings, noteIdToMapping } = ctx;
19518
19787
  const remoteTexts = new Set(remoteComments.map((c) => c.text.trim()));
19519
19788
  const mappedRemoteIdsAfterPull = /* @__PURE__ */ new Set([
19520
- ...mappedRemoteIds,
19789
+ ...mappings.map((m) => m.remote_comment_id),
19521
19790
  ...Array.from(noteIdToMapping.values()).map((m) => m.remote_comment_id)
19522
19791
  ]);
19523
19792
  for (const note of localNotes) {
19524
- if (isAttMapping(note.id)) continue;
19793
+ if (note.id.startsWith("att_")) continue;
19525
19794
  if (noteIdToMapping.has(note.id)) continue;
19526
19795
  if (!backend.createComment) continue;
19527
19796
  if (remoteTexts.has(note.text.trim())) {
19528
- const existing = remoteComments.find(
19529
- (c) => c.text.trim() === note.text.trim() && !mappedRemoteIdsAfterPull.has(c.id)
19530
- );
19797
+ const existing = remoteComments.find((c) => c.text.trim() === note.text.trim() && !mappedRemoteIdsAfterPull.has(c.id));
19531
19798
  if (existing) {
19532
19799
  await upsertNoteSyncRecord(ticketId, note.id, backend.id, existing.id, note.text);
19533
19800
  mappedRemoteIdsAfterPull.add(existing.id);
@@ -19543,7 +19810,36 @@ async function syncTicketComments(backend, ticketId, remoteId) {
19543
19810
  console.warn(`[sync] Failed to push note ${note.id} for ticket ${ticketId}: ${getErrorMessage(e)}`);
19544
19811
  }
19545
19812
  }
19546
- if (changed) {
19813
+ }
19814
+ async function syncTicketComments(backend, ticketId, remoteId) {
19815
+ if (!backend.getComments) return;
19816
+ const ticket = await getTicket(ticketId);
19817
+ if (!ticket) return;
19818
+ const localNotes = parseNotes(ticket.notes);
19819
+ let remoteComments;
19820
+ try {
19821
+ remoteComments = await backend.getComments(remoteId);
19822
+ } catch (e) {
19823
+ const msg = getErrorMessage(e);
19824
+ if (msg.includes("404") || msg.includes("410") || msg.includes("Not Found")) throw e;
19825
+ return;
19826
+ }
19827
+ const mappings = await getNoteSyncRecords(ticketId, backend.id);
19828
+ const ctx = {
19829
+ backend,
19830
+ ticketId,
19831
+ remoteId,
19832
+ localNotes,
19833
+ remoteComments,
19834
+ mappings,
19835
+ localNoteById: new Map(localNotes.map((n) => [n.id, n])),
19836
+ noteIdToMapping: new Map(mappings.map((m) => [m.note_id, m])),
19837
+ changed: false
19838
+ };
19839
+ await reconcileExistingMappings(ctx);
19840
+ await pullNewRemoteComments(ctx);
19841
+ await pushNewLocalNotes(ctx);
19842
+ if (ctx.changed) {
19547
19843
  const { getDb: getDbForNotes } = await Promise.resolve().then(() => (init_connection(), connection_exports));
19548
19844
  const db = await getDbForNotes();
19549
19845
  await db.query("UPDATE tickets SET notes = $1 WHERE id = $2", [JSON.stringify(localNotes), ticketId]);
@@ -19585,8 +19881,8 @@ async function syncTicketAttachments(backend, ticketId, remoteId) {
19585
19881
  const attSyncId = `att_${att.id}`;
19586
19882
  if (syncedAttIds.has(attSyncId)) continue;
19587
19883
  try {
19588
- const { readFileSync: readFileSync14 } = await import("fs");
19589
- const content = readFileSync14(att.stored_path);
19884
+ const { readFileSync: readFileSync15 } = await import("fs");
19885
+ const content = readFileSync15(att.stored_path);
19590
19886
  const ext = att.original_filename.split(".").pop()?.toLowerCase() ?? "";
19591
19887
  const { getMimeType: getMimeType2 } = await Promise.resolve().then(() => (init_mime_types(), mime_types_exports));
19592
19888
  const mimeType = getMimeType2(ext);
@@ -19649,10 +19945,10 @@ __export(plugins_exports, {
19649
19945
  isPluginEnabledForProject: () => isPluginEnabledForProject,
19650
19946
  pluginRoutes: () => pluginRoutes
19651
19947
  });
19652
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync11, rmSync as rmSync8, symlinkSync } from "fs";
19948
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync12, rmSync as rmSync8, symlinkSync } from "fs";
19653
19949
  import { Hono as Hono5 } from "hono";
19654
- import { homedir as homedir6 } from "os";
19655
- import { basename as basename2, join as join15 } from "path";
19950
+ import { homedir as homedir7 } from "os";
19951
+ import { basename as basename2, join as join16 } from "path";
19656
19952
  async function getActivatedBackend(pluginId) {
19657
19953
  await reactivatePlugin(pluginId);
19658
19954
  const plugin = getPluginById(pluginId);
@@ -19746,6 +20042,7 @@ var init_plugins = __esm({
19746
20042
  init_errorMessage();
19747
20043
  init_helpers();
19748
20044
  init_notify();
20045
+ init_validation();
19749
20046
  pluginRoutes = new Hono5();
19750
20047
  pluginRoutes.get("/plugins", async (c) => {
19751
20048
  const loaded = getLoadedPlugins();
@@ -19784,7 +20081,10 @@ var init_plugins = __esm({
19784
20081
  let plugin = getPluginById(pluginId);
19785
20082
  if (!plugin) return c.json({ error: "Plugin not found" }, 404);
19786
20083
  if (!plugin.instance.onAction) return c.json({ error: "Plugin does not handle actions" }, 400);
19787
- const body = await c.req.json();
20084
+ const raw = await c.req.json();
20085
+ const parsed = parseBody(PluginActionSchema, raw);
20086
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20087
+ const body = parsed.data;
19788
20088
  await reactivatePlugin(pluginId);
19789
20089
  plugin = getPluginById(pluginId);
19790
20090
  if (!plugin.instance.onAction) return c.json({ error: "Plugin does not handle actions" }, 400);
@@ -19802,7 +20102,10 @@ var init_plugins = __esm({
19802
20102
  pluginRoutes.post("/plugins/validate/:id", async (c) => {
19803
20103
  const plugin = getPluginById(c.req.param("id"));
19804
20104
  if (!plugin?.instance.validateField) return c.json(null);
19805
- const body = await c.req.json();
20105
+ const raw = await c.req.json();
20106
+ const parsed = parseBody(PluginValidateSchema, raw);
20107
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20108
+ const body = parsed.data;
19806
20109
  try {
19807
20110
  const result = await plugin.instance.validateField(body.key, body.value);
19808
20111
  return c.json(result);
@@ -19976,7 +20279,10 @@ var init_plugins = __esm({
19976
20279
  const pluginId = c.req.param("id");
19977
20280
  const plugin = getPluginById(pluginId);
19978
20281
  if (!plugin) return c.json({ error: "Plugin not found" }, 404);
19979
- const body = await c.req.json();
20282
+ const raw = await c.req.json();
20283
+ const parsed = parseBody(PluginSyncScheduleSchema, raw);
20284
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20285
+ const body = parsed.data;
19980
20286
  if (body.interval_minutes === null || body.interval_minutes === 0) {
19981
20287
  stopScheduledSync(pluginId);
19982
20288
  return c.json({ ok: true, scheduled: false });
@@ -20019,26 +20325,27 @@ var init_plugins = __esm({
20019
20325
  pluginRoutes.post("/sync/conflicts/:ticketId/resolve", async (c) => {
20020
20326
  const ticketId = parseIntParam(c, "ticketId");
20021
20327
  if (ticketId === null) return c.json({ error: "Invalid ticket ID" }, 400);
20022
- const body = await c.req.json();
20023
- if (body.plugin_id == null || body.plugin_id === "" || body.resolution == null) {
20024
- return c.json({ error: "plugin_id and resolution required" }, 400);
20025
- }
20026
- await resolveConflict(ticketId, body.plugin_id, body.resolution);
20328
+ const raw = await c.req.json();
20329
+ const parsed = parseBody(PluginConflictResolveSchema, raw);
20330
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20331
+ await resolveConflict(ticketId, parsed.data.plugin_id, parsed.data.resolution);
20027
20332
  return c.json({ ok: true });
20028
20333
  });
20029
20334
  pluginRoutes.post("/plugins/install", async (c) => {
20030
- const body = await c.req.json();
20031
- if (body.path == null || body.path === "") return c.json({ error: "path is required" }, 400);
20335
+ const raw = await c.req.json();
20336
+ const parsed = parseBody(PluginInstallSchema, raw);
20337
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20338
+ const body = parsed.data;
20032
20339
  const sourcePath = body.path;
20033
- if (!existsSync13(sourcePath)) {
20340
+ if (!existsSync14(sourcePath)) {
20034
20341
  return c.json({ error: `Path does not exist: ${sourcePath}` }, 400);
20035
20342
  }
20036
- const hasManifest = existsSync13(join15(sourcePath, "manifest.json"));
20343
+ const hasManifest = existsSync14(join16(sourcePath, "manifest.json"));
20037
20344
  const hasPkgHotsheet = (() => {
20038
- const pkgPath = join15(sourcePath, "package.json");
20039
- if (!existsSync13(pkgPath)) return false;
20345
+ const pkgPath = join16(sourcePath, "package.json");
20346
+ if (!existsSync14(pkgPath)) return false;
20040
20347
  try {
20041
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
20348
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
20042
20349
  return pkg.hotsheet !== void 0;
20043
20350
  } catch {
20044
20351
  return false;
@@ -20047,11 +20354,11 @@ var init_plugins = __esm({
20047
20354
  if (!hasManifest && !hasPkgHotsheet) {
20048
20355
  return c.json({ error: "Directory must contain manifest.json or package.json with hotsheet field" }, 400);
20049
20356
  }
20050
- const pluginsDir = join15(homedir6(), ".hotsheet", "plugins");
20051
- mkdirSync9(pluginsDir, { recursive: true });
20357
+ const pluginsDir = join16(homedir7(), ".hotsheet", "plugins");
20358
+ mkdirSync10(pluginsDir, { recursive: true });
20052
20359
  const linkName = basename2(sourcePath);
20053
- const linkPath = join15(pluginsDir, linkName);
20054
- if (existsSync13(linkPath)) {
20360
+ const linkPath = join16(pluginsDir, linkName);
20361
+ if (existsSync14(linkPath)) {
20055
20362
  return c.json({ error: `Plugin already exists at ${linkPath}` }, 400);
20056
20363
  }
20057
20364
  try {
@@ -20069,14 +20376,14 @@ var init_plugins = __esm({
20069
20376
  if (plugin?.enabled === true) {
20070
20377
  await disablePlugin(pluginId);
20071
20378
  }
20072
- const pluginsDir = join15(homedir6(), ".hotsheet", "plugins");
20379
+ const pluginsDir = join16(homedir7(), ".hotsheet", "plugins");
20073
20380
  const candidates = [
20074
20381
  plugin?.path,
20075
- join15(pluginsDir, pluginId)
20382
+ join16(pluginsDir, pluginId)
20076
20383
  ].filter((p) => p !== void 0);
20077
20384
  let removed = false;
20078
20385
  for (const candidate of candidates) {
20079
- if (existsSync13(candidate)) {
20386
+ if (existsSync14(candidate)) {
20080
20387
  try {
20081
20388
  rmSync8(candidate, { recursive: true, force: true });
20082
20389
  removed = true;
@@ -20108,8 +20415,10 @@ var init_plugins = __esm({
20108
20415
  });
20109
20416
  pluginRoutes.post("/plugins/:id/global-config", async (c) => {
20110
20417
  const pluginId = c.req.param("id");
20111
- const body = await c.req.json();
20112
- if (body.key == null || body.key === "") return c.json({ error: "key is required" }, 400);
20418
+ const raw = await c.req.json();
20419
+ const parsed = parseBody(PluginGlobalConfigSchema, raw);
20420
+ if (!parsed.success) return c.json({ error: parsed.error }, 400);
20421
+ const body = parsed.data;
20113
20422
  const plugin = getPluginById(pluginId);
20114
20423
  const isSecret = plugin?.manifest.preferences?.find((p) => p.key === body.key)?.secret === true;
20115
20424
  if (isSecret) {
@@ -20178,9 +20487,9 @@ var init_plugins = __esm({
20178
20487
  // src/cli.ts
20179
20488
  init_backup();
20180
20489
  import { execFile as execFile3 } from "child_process";
20181
- import { existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
20490
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12 } from "fs";
20182
20491
  import { tmpdir as tmpdir2 } from "os";
20183
- import { join as join18, resolve as resolve8 } from "path";
20492
+ import { join as join19, resolve as resolve8 } from "path";
20184
20493
 
20185
20494
  // src/cleanup.ts
20186
20495
  init_queries();
@@ -20338,9 +20647,9 @@ init_mime_types();
20338
20647
  init_projects();
20339
20648
  import { serve } from "@hono/node-server";
20340
20649
  import { execFile as execFile2 } from "child_process";
20341
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
20650
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
20342
20651
  import { Hono as Hono13 } from "hono";
20343
- import { basename as basename3, dirname as dirname4, join as join16 } from "path";
20652
+ import { basename as basename3, dirname as dirname4, join as join17 } from "path";
20344
20653
  import { fileURLToPath as fileURLToPath3 } from "url";
20345
20654
 
20346
20655
  // src/routes/api.ts
@@ -20375,8 +20684,8 @@ attachmentRoutes.post("/tickets/:id/attachments", async (c) => {
20375
20684
  mkdirSync6(attachDir, { recursive: true });
20376
20685
  const storedPath = join10(attachDir, storedName);
20377
20686
  const buffer = Buffer.from(await file2.arrayBuffer());
20378
- const { writeFileSync: writeFileSync13 } = await import("fs");
20379
- writeFileSync13(storedPath, buffer);
20687
+ const { writeFileSync: writeFileSync14 } = await import("fs");
20688
+ writeFileSync14(storedPath, buffer);
20380
20689
  const attachment = await addAttachment(id, originalName, storedPath);
20381
20690
  notifyMutation(c.get("dataDir"));
20382
20691
  return c.json(attachment, 201);
@@ -20413,8 +20722,8 @@ attachmentRoutes.get("/attachments/file/*", async (c) => {
20413
20722
  if (!existsSync8(fullPath)) {
20414
20723
  return c.json({ error: "File not found" }, 404);
20415
20724
  }
20416
- const { readFileSync: readFileSync14 } = await import("fs");
20417
- const content = readFileSync14(fullPath);
20725
+ const { readFileSync: readFileSync15 } = await import("fs");
20726
+ const content = readFileSync15(fullPath);
20418
20727
  const ext = extname(fullPath).toLowerCase();
20419
20728
  const contentType = getMimeType(ext);
20420
20729
  return new Response(content, {
@@ -20426,120 +20735,8 @@ attachmentRoutes.get("/attachments/file/*", async (c) => {
20426
20735
  init_commandLog();
20427
20736
  init_settings();
20428
20737
  init_notify();
20738
+ init_validation();
20429
20739
  import { Hono as Hono2 } from "hono";
20430
-
20431
- // src/routes/validation.ts
20432
- init_zod();
20433
- var TicketPrioritySchema = external_exports.enum(["highest", "high", "default", "low", "lowest"]);
20434
- var TicketStatusSchema = external_exports.enum(["not_started", "started", "completed", "verified", "backlog", "archive", "deleted"]);
20435
- var SortBySchema = external_exports.enum(["created", "priority", "category", "status"]);
20436
- var SortDirSchema = external_exports.enum(["asc", "desc"]);
20437
- var CreateTicketSchema = external_exports.object({
20438
- title: external_exports.string().optional().default(""),
20439
- defaults: external_exports.object({
20440
- category: external_exports.string().optional(),
20441
- priority: TicketPrioritySchema.or(external_exports.literal("")).optional(),
20442
- status: TicketStatusSchema.or(external_exports.literal("")).optional(),
20443
- up_next: external_exports.boolean().optional(),
20444
- details: external_exports.string().optional(),
20445
- tags: external_exports.string().optional()
20446
- }).optional()
20447
- });
20448
- var UpdateTicketSchema = external_exports.object({
20449
- title: external_exports.string().optional(),
20450
- details: external_exports.string().optional(),
20451
- notes: external_exports.string().optional(),
20452
- tags: external_exports.string().optional(),
20453
- category: external_exports.string().optional(),
20454
- priority: TicketPrioritySchema.optional(),
20455
- status: TicketStatusSchema.optional(),
20456
- up_next: external_exports.boolean().optional(),
20457
- last_read_at: external_exports.string().nullable().optional()
20458
- });
20459
- var BatchActionSchema = external_exports.object({
20460
- ids: external_exports.array(external_exports.number().int()),
20461
- action: external_exports.enum(["delete", "restore", "category", "priority", "status", "up_next", "mark_read", "mark_unread"]),
20462
- value: external_exports.union([external_exports.string(), external_exports.boolean()]).optional()
20463
- });
20464
- var DuplicateSchema = external_exports.object({
20465
- ids: external_exports.array(external_exports.number().int())
20466
- });
20467
- var NotesEditSchema = external_exports.object({
20468
- text: external_exports.string()
20469
- });
20470
- var NotesBulkSchema = external_exports.object({
20471
- notes: external_exports.string()
20472
- });
20473
- var QueryTicketsSchema = external_exports.object({
20474
- logic: external_exports.enum(["all", "any"]),
20475
- conditions: external_exports.array(external_exports.object({
20476
- field: external_exports.enum(["category", "priority", "status", "title", "details", "up_next", "tags"]),
20477
- operator: external_exports.enum(["equals", "not_equals", "contains", "not_contains", "lt", "lte", "gt", "gte"]),
20478
- value: external_exports.string()
20479
- })),
20480
- sort_by: external_exports.string().optional(),
20481
- sort_dir: SortDirSchema.optional(),
20482
- required_tag: external_exports.string().optional(),
20483
- include_archived: external_exports.boolean().optional()
20484
- });
20485
- var UpdateSettingsSchema = external_exports.record(external_exports.string(), external_exports.string());
20486
- var BackupTierSchema = external_exports.enum(["5min", "hourly", "daily"]);
20487
- var CreateBackupSchema = external_exports.object({
20488
- tier: BackupTierSchema
20489
- });
20490
- var RestoreBackupSchema = external_exports.object({
20491
- tier: external_exports.string().min(1),
20492
- filename: external_exports.string().min(1)
20493
- });
20494
- var ShellExecSchema = external_exports.object({
20495
- command: external_exports.string().min(1, "Command cannot be empty"),
20496
- name: external_exports.string().optional()
20497
- });
20498
- var ShellKillSchema = external_exports.object({
20499
- id: external_exports.number().int()
20500
- });
20501
- var ChannelTriggerSchema = external_exports.object({
20502
- message: external_exports.string().optional()
20503
- });
20504
- var PermissionRespondSchema = external_exports.object({
20505
- request_id: external_exports.string(),
20506
- behavior: external_exports.enum(["allow", "deny"]),
20507
- tool_name: external_exports.string().optional()
20508
- });
20509
- var RegisterProjectSchema = external_exports.object({
20510
- dataDir: external_exports.string().min(1, "dataDir is required")
20511
- });
20512
- var ReorderProjectsSchema = external_exports.object({
20513
- secrets: external_exports.array(external_exports.string())
20514
- });
20515
- var CategoryDefSchema = external_exports.object({
20516
- id: external_exports.string().min(1),
20517
- label: external_exports.string().min(1),
20518
- shortLabel: external_exports.string().min(1),
20519
- color: external_exports.string().min(1),
20520
- shortcutKey: external_exports.string(),
20521
- description: external_exports.string()
20522
- }).loose();
20523
- var UpdateCategoriesSchema = external_exports.array(CategoryDefSchema).min(1);
20524
- var PrintSchema = external_exports.object({
20525
- html: external_exports.string()
20526
- });
20527
- var GlobalConfigSchema = external_exports.object({
20528
- channelEnabled: external_exports.boolean().optional(),
20529
- shareTotalSeconds: external_exports.number().optional(),
20530
- shareLastPrompted: external_exports.string().optional(),
20531
- shareAccepted: external_exports.boolean().optional()
20532
- }).strict();
20533
- function parseBody(schema, data) {
20534
- const result = schema.safeParse(data);
20535
- if (!result.success) {
20536
- const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).filter((m) => m !== ": ");
20537
- return { success: false, error: messages.join("; ") || "Invalid request body" };
20538
- }
20539
- return { success: true, data: result.data };
20540
- }
20541
-
20542
- // src/routes/channel.ts
20543
20740
  var channelRoutes = new Hono2();
20544
20741
  var channelDoneFlags = /* @__PURE__ */ new Map();
20545
20742
  var loggedPermissionRequests = /* @__PURE__ */ new Map();
@@ -20690,6 +20887,7 @@ channelRoutes.post("/channel/enable", async (c) => {
20690
20887
  const { writeGlobalConfig: writeGlobalConfig3 } = await Promise.resolve().then(() => (init_global_config(), global_config_exports));
20691
20888
  const dataDir = c.get("dataDir");
20692
20889
  writeGlobalConfig3({ channelEnabled: true });
20890
+ const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
20693
20891
  try {
20694
20892
  const { getAllProjects: getAllProjects2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
20695
20893
  const { ensureSkillsForDir: ensureSkillsForDir2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
@@ -20701,6 +20899,8 @@ channelRoutes.post("/channel/enable", async (c) => {
20701
20899
  } catch {
20702
20900
  registerChannel2(dataDir);
20703
20901
  }
20902
+ const { installHeartbeatHook: installHeartbeatHook2 } = await Promise.resolve().then(() => (init_claude_hooks(), claude_hooks_exports));
20903
+ installHeartbeatHook2(serverPort);
20704
20904
  notifyChange();
20705
20905
  return c.json({ ok: true });
20706
20906
  });
@@ -20720,9 +20920,35 @@ channelRoutes.post("/channel/disable", async (c) => {
20720
20920
  unregisterChannel2(dataDir);
20721
20921
  await shutdownChannel2(dataDir);
20722
20922
  }
20923
+ const { removeHeartbeatHook: removeHeartbeatHook2 } = await Promise.resolve().then(() => (init_claude_hooks(), claude_hooks_exports));
20924
+ removeHeartbeatHook2();
20723
20925
  notifyChange();
20724
20926
  return c.json({ ok: true });
20725
20927
  });
20928
+ channelRoutes.post("/channel/heartbeat", async (c) => {
20929
+ const { getAllProjects: getAllProjects2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
20930
+ const raw = await c.req.json().catch(() => ({}));
20931
+ const parsed = parseBody(ChannelHeartbeatSchema, raw);
20932
+ if (!parsed.success) return c.json({ ok: false });
20933
+ const projectDir = parsed.data.projectDir;
20934
+ const hookState = parsed.data.state ?? "heartbeat";
20935
+ if (projectDir === void 0 || projectDir === "") return c.json({ ok: false });
20936
+ const projects2 = getAllProjects2();
20937
+ const match = projects2.find((p) => {
20938
+ const rootDir = p.dataDir.replace(/\/.hotsheet\/?$/, "");
20939
+ return rootDir === projectDir || projectDir.startsWith(rootDir + "/");
20940
+ });
20941
+ if (!match) return c.json({ ok: false });
20942
+ heartbeatUpdates.push({ secret: match.secret, state: hookState });
20943
+ notifyChange();
20944
+ return c.json({ ok: true, project: match.name });
20945
+ });
20946
+ var heartbeatUpdates = [];
20947
+ channelRoutes.get("/channel/heartbeat-status", (c) => {
20948
+ const updates = [...heartbeatUpdates];
20949
+ heartbeatUpdates.length = 0;
20950
+ return c.json({ updates });
20951
+ });
20726
20952
  channelRoutes.post("/channel/notify", (c) => {
20727
20953
  notifyChange();
20728
20954
  return c.json({ ok: true });
@@ -20763,10 +20989,11 @@ init_open_in_file_manager();
20763
20989
  init_projects();
20764
20990
  init_skills();
20765
20991
  init_notify();
20766
- import { existsSync as existsSync11, readdirSync as readdirSync2, writeFileSync as writeFileSync10 } from "fs";
20992
+ init_validation();
20993
+ import { existsSync as existsSync12, readdirSync as readdirSync2, writeFileSync as writeFileSync11 } from "fs";
20767
20994
  import { Hono as Hono4 } from "hono";
20768
- import { homedir as homedir4, tmpdir } from "os";
20769
- import { join as join13, relative as relative2, resolve as resolve6 } from "path";
20995
+ import { homedir as homedir5, tmpdir } from "os";
20996
+ import { join as join14, relative as relative2, resolve as resolve6 } from "path";
20770
20997
  var dashboardRoutes = new Hono4();
20771
20998
  dashboardRoutes.get("/poll", async (c) => {
20772
20999
  const clientVersion = Math.max(0, parseInt(c.req.query("version") ?? "0", 10) || 0);
@@ -20800,7 +21027,7 @@ dashboardRoutes.get("/dashboard", async (c) => {
20800
21027
  dashboardRoutes.get("/worklist-info", (c) => {
20801
21028
  const dataDir = c.get("dataDir");
20802
21029
  const cwd = process.cwd();
20803
- const worklistRel = relative2(cwd, join13(dataDir, "worklist.md"));
21030
+ const worklistRel = relative2(cwd, join14(dataDir, "worklist.md"));
20804
21031
  const prompt = `Read ${worklistRel} for current work items.`;
20805
21032
  for (const p of getAllProjects()) {
20806
21033
  ensureSkillsForDir(p.dataDir.replace(/\/.hotsheet\/?$/, ""));
@@ -20809,23 +21036,23 @@ dashboardRoutes.get("/worklist-info", (c) => {
20809
21036
  return c.json({ prompt, skillCreated });
20810
21037
  });
20811
21038
  dashboardRoutes.get("/browse", (c) => {
20812
- const requestedPath = c.req.query("path") ?? homedir4();
21039
+ const requestedPath = c.req.query("path") ?? homedir5();
20813
21040
  const absPath = resolve6(requestedPath);
20814
- if (!existsSync11(absPath)) {
21041
+ if (!existsSync12(absPath)) {
20815
21042
  return c.json({ error: "Path does not exist", path: absPath }, 404);
20816
21043
  }
20817
21044
  try {
20818
21045
  const entries = readdirSync2(absPath, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({
20819
21046
  name: e.name,
20820
- path: join13(absPath, e.name),
20821
- hasHotsheet: existsSync11(join13(absPath, e.name, ".hotsheet"))
21047
+ path: join14(absPath, e.name),
21048
+ hasHotsheet: existsSync12(join14(absPath, e.name, ".hotsheet"))
20822
21049
  }));
20823
21050
  const parentPath = resolve6(absPath, "..");
20824
21051
  return c.json({
20825
21052
  path: absPath,
20826
21053
  parent: parentPath !== absPath ? parentPath : null,
20827
21054
  entries,
20828
- hasHotsheet: existsSync11(join13(absPath, ".hotsheet"))
21055
+ hasHotsheet: existsSync12(join14(absPath, ".hotsheet"))
20829
21056
  });
20830
21057
  } catch {
20831
21058
  return c.json({ error: "Cannot read directory", path: absPath }, 403);
@@ -20891,8 +21118,8 @@ dashboardRoutes.post("/print", async (c) => {
20891
21118
  const raw = await c.req.json();
20892
21119
  const parsed = parseBody(PrintSchema, raw);
20893
21120
  if (!parsed.success) return c.json({ error: parsed.error }, 400);
20894
- const tmpPath = join13(tmpdir(), `hotsheet-print-${Date.now()}.html`);
20895
- writeFileSync10(tmpPath, parsed.data.html, "utf-8");
21121
+ const tmpPath = join14(tmpdir(), `hotsheet-print-${Date.now()}.html`);
21122
+ writeFileSync11(tmpPath, parsed.data.html, "utf-8");
20896
21123
  await openInFileManager(tmpPath);
20897
21124
  return c.json({ ok: true, path: tmpPath });
20898
21125
  });
@@ -20904,6 +21131,7 @@ init_plugins();
20904
21131
  init_queries();
20905
21132
  init_types();
20906
21133
  init_notify();
21134
+ init_validation();
20907
21135
  import { Hono as Hono6 } from "hono";
20908
21136
  var settingsRoutes = new Hono6();
20909
21137
  settingsRoutes.get("/tags", async (c) => {
@@ -20971,6 +21199,7 @@ settingsRoutes.patch("/file-settings", async (c) => {
20971
21199
 
20972
21200
  // src/routes/shell.ts
20973
21201
  init_commandLog();
21202
+ init_validation();
20974
21203
  import { Hono as Hono7 } from "hono";
20975
21204
  var shellRoutes = new Hono7();
20976
21205
  var runningProcesses = /* @__PURE__ */ new Map();
@@ -21059,6 +21288,7 @@ init_syncEngine();
21059
21288
  init_helpers();
21060
21289
  init_notify();
21061
21290
  init_plugins();
21291
+ init_validation();
21062
21292
  import { rmSync as rmSync9 } from "fs";
21063
21293
  import { Hono as Hono8 } from "hono";
21064
21294
  var VALID_STATUS_FILTERS = /* @__PURE__ */ new Set([
@@ -21361,6 +21591,7 @@ if (PLUGINS_ENABLED) apiRoutes.route("/", pluginRoutes);
21361
21591
  // src/routes/backups.ts
21362
21592
  init_backup();
21363
21593
  init_markdown();
21594
+ init_validation();
21364
21595
  import { Hono as Hono10 } from "hono";
21365
21596
  var backupRoutes = new Hono10();
21366
21597
  backupRoutes.get("/", (c) => {
@@ -22236,13 +22467,14 @@ init_open_in_file_manager();
22236
22467
  init_project_list();
22237
22468
  init_projects();
22238
22469
  init_notify();
22239
- import { existsSync as existsSync14 } from "fs";
22470
+ init_validation();
22471
+ import { existsSync as existsSync15 } from "fs";
22240
22472
  import { Hono as Hono12 } from "hono";
22241
22473
  import { resolve as resolve7 } from "path";
22242
22474
  var projectRoutes = new Hono12();
22243
22475
  projectRoutes.get("/", async (c) => {
22244
22476
  const projects2 = getAllProjects();
22245
- const stale = projects2.filter((p) => !existsSync14(p.dataDir));
22477
+ const stale = projects2.filter((p) => !existsSync15(p.dataDir));
22246
22478
  for (const p of stale) {
22247
22479
  removeFromProjectList(p.dataDir);
22248
22480
  unregisterProject(p.secret);
@@ -22250,7 +22482,7 @@ projectRoutes.get("/", async (c) => {
22250
22482
  const persisted = readProjectList();
22251
22483
  const inMemoryDirs = new Set(getAllProjects().map((p) => p.dataDir));
22252
22484
  for (const dir of persisted) {
22253
- if (!inMemoryDirs.has(dir) && !existsSync14(dir)) {
22485
+ if (!inMemoryDirs.has(dir) && !existsSync15(dir)) {
22254
22486
  removeFromProjectList(dir);
22255
22487
  }
22256
22488
  }
@@ -22379,7 +22611,7 @@ projectRoutes.post("/:secret/reveal", async (c) => {
22379
22611
  const project = getProjectBySecret(secret);
22380
22612
  if (!project) return c.json({ error: "Project not found" }, 404);
22381
22613
  const projectRoot2 = resolve7(project.dataDir, "..");
22382
- if (!existsSync14(projectRoot2)) return c.json({ error: "Folder not found on disk" }, 404);
22614
+ if (!existsSync15(projectRoot2)) return c.json({ error: "Folder not found on disk" }, 404);
22383
22615
  await openInFileManager(projectRoot2);
22384
22616
  return c.json({ ok: true });
22385
22617
  });
@@ -22429,25 +22661,25 @@ async function startServer(port, dataDir, options) {
22429
22661
  await runWithDataDir(resolvedDataDir, () => next());
22430
22662
  });
22431
22663
  const selfDir = dirname4(fileURLToPath3(import.meta.url));
22432
- const distDir = existsSync15(join16(selfDir, "client", "styles.css")) ? join16(selfDir, "client") : join16(selfDir, "..", "dist", "client");
22664
+ const distDir = existsSync16(join17(selfDir, "client", "styles.css")) ? join17(selfDir, "client") : join17(selfDir, "..", "dist", "client");
22433
22665
  app.get("/static/styles.css", (c) => {
22434
- const css = readFileSync12(join16(distDir, "styles.css"), "utf-8");
22666
+ const css = readFileSync13(join17(distDir, "styles.css"), "utf-8");
22435
22667
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
22436
22668
  });
22437
22669
  app.get("/static/app.js", (c) => {
22438
- const js = readFileSync12(join16(distDir, "app.global.js"), "utf-8");
22670
+ const js = readFileSync13(join17(distDir, "app.global.js"), "utf-8");
22439
22671
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
22440
22672
  });
22441
22673
  app.get("/static/assets/:filename", (c) => {
22442
22674
  const filename = basename3(c.req.param("filename"));
22443
- const filePath = join16(distDir, "assets", filename);
22444
- if (!existsSync15(filePath)) return c.notFound();
22445
- const content = readFileSync12(filePath);
22675
+ const filePath = join17(distDir, "assets", filename);
22676
+ if (!existsSync16(filePath)) return c.notFound();
22677
+ const content = readFileSync13(filePath);
22446
22678
  const ext = filename.split(".").pop() ?? "";
22447
22679
  return new Response(content, { headers: { "Content-Type": getMimeType(ext), "Cache-Control": "max-age=86400" } });
22448
22680
  });
22449
22681
  app.use("/api/*", async (c, next) => {
22450
- if (c.req.path.startsWith("/api/projects")) {
22682
+ if (c.req.path.startsWith("/api/projects") || c.req.path === "/api/channel/heartbeat") {
22451
22683
  await next();
22452
22684
  return;
22453
22685
  }
@@ -22536,18 +22768,18 @@ init_skills();
22536
22768
  init_markdown();
22537
22769
 
22538
22770
  // src/update-check.ts
22539
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "fs";
22771
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "fs";
22540
22772
  import { get } from "https";
22541
- import { homedir as homedir7 } from "os";
22542
- import { dirname as dirname5, join as join17 } from "path";
22773
+ import { homedir as homedir8 } from "os";
22774
+ import { dirname as dirname5, join as join18 } from "path";
22543
22775
  import { fileURLToPath as fileURLToPath4 } from "url";
22544
- var DATA_DIR = join17(homedir7(), ".hotsheet");
22545
- var CHECK_FILE = join17(DATA_DIR, "last-update-check");
22776
+ var DATA_DIR = join18(homedir8(), ".hotsheet");
22777
+ var CHECK_FILE = join18(DATA_DIR, "last-update-check");
22546
22778
  var PACKAGE_NAME = "hotsheet";
22547
22779
  function getCurrentVersion() {
22548
22780
  try {
22549
22781
  const dir = dirname5(fileURLToPath4(import.meta.url));
22550
- const pkg = JSON.parse(readFileSync13(join17(dir, "..", "package.json"), "utf-8"));
22782
+ const pkg = JSON.parse(readFileSync14(join18(dir, "..", "package.json"), "utf-8"));
22551
22783
  return pkg.version;
22552
22784
  } catch {
22553
22785
  return "0.0.0";
@@ -22555,16 +22787,16 @@ function getCurrentVersion() {
22555
22787
  }
22556
22788
  function getLastCheckDate() {
22557
22789
  try {
22558
- if (existsSync16(CHECK_FILE)) {
22559
- return readFileSync13(CHECK_FILE, "utf-8").trim();
22790
+ if (existsSync17(CHECK_FILE)) {
22791
+ return readFileSync14(CHECK_FILE, "utf-8").trim();
22560
22792
  }
22561
22793
  } catch {
22562
22794
  }
22563
22795
  return null;
22564
22796
  }
22565
22797
  function saveCheckDate() {
22566
- mkdirSync10(DATA_DIR, { recursive: true });
22567
- writeFileSync12(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
22798
+ mkdirSync11(DATA_DIR, { recursive: true });
22799
+ writeFileSync13(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
22568
22800
  }
22569
22801
  function isFirstUseToday() {
22570
22802
  const last = getLastCheckDate();
@@ -22676,7 +22908,7 @@ Examples:
22676
22908
  function parseArgs(argv) {
22677
22909
  const args = argv.slice(2);
22678
22910
  let port = 4174;
22679
- let dataDir = join18(process.cwd(), ".hotsheet");
22911
+ let dataDir = join19(process.cwd(), ".hotsheet");
22680
22912
  let demo = null;
22681
22913
  let forceUpdateCheck = false;
22682
22914
  let noOpen = false;
@@ -22824,21 +23056,28 @@ async function handleEarlyFlags(args) {
22824
23056
  return false;
22825
23057
  }
22826
23058
  async function initializeProject(dataDir, demo) {
22827
- mkdirSync11(dataDir, { recursive: true });
23059
+ const t0 = Date.now();
23060
+ const elapsed = () => `${Date.now() - t0}ms`;
23061
+ mkdirSync12(dataDir, { recursive: true });
22828
23062
  if (demo === null) {
22829
23063
  acquireLock(dataDir);
22830
23064
  ensureGitignore(process.cwd());
22831
23065
  }
23066
+ console.error(`[init-project ${elapsed()}] initializing DB...`);
22832
23067
  setDataDir(dataDir);
22833
23068
  const db = await getDb();
23069
+ console.error(`[init-project ${elapsed()}] DB ready`);
22834
23070
  if (demo !== null) {
22835
23071
  await seedDemoData(demo);
22836
23072
  }
22837
23073
  if (demo === null) {
22838
23074
  const { runWithDataDir: runWithDataDir2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
23075
+ console.error(`[init-project ${elapsed()}] migrating settings...`);
22839
23076
  const { migrateDbSettingsToFile: migrateDbSettingsToFile2 } = await Promise.resolve().then(() => (init_migrate_settings(), migrate_settings_exports));
22840
23077
  await runWithDataDir2(dataDir, () => migrateDbSettingsToFile2(dataDir));
23078
+ console.error(`[init-project ${elapsed()}] cleaning up attachments...`);
22841
23079
  await runWithDataDir2(dataDir, () => cleanupAttachments());
23080
+ console.error(`[init-project ${elapsed()}] done`);
22842
23081
  }
22843
23082
  console.log(` Data directory: ${dataDir}`);
22844
23083
  return db;
@@ -22870,14 +23109,22 @@ async function startAndConfigure(port, dataDir, strictPort) {
22870
23109
  return { actualPort, secret };
22871
23110
  }
22872
23111
  async function postStartup(dataDir, actualPort, demo, noOpen) {
23112
+ const t0 = Date.now();
23113
+ const elapsed = () => `${Date.now() - t0}ms`;
22873
23114
  if (demo === null) {
22874
23115
  initBackupScheduler(dataDir);
22875
23116
  addToProjectList(dataDir);
23117
+ console.error(`[post-startup ${elapsed()}] restoring previous projects...`);
22876
23118
  await restorePreviousProjects(dataDir, actualPort);
23119
+ console.error(`[post-startup ${elapsed()}] migrating global config...`);
22877
23120
  await migrateGlobalConfig();
23121
+ console.error(`[post-startup ${elapsed()}] cleaning up stale channels...`);
22878
23122
  await cleanupStaleChannels();
22879
- await setupSkillsAndChannels();
23123
+ console.error(`[post-startup ${elapsed()}] setting up skills and channels...`);
23124
+ await setupSkillsAndChannels(actualPort);
23125
+ console.error(`[post-startup ${elapsed()}] setting up instance lifecycle...`);
22880
23126
  setupInstanceLifecycle(actualPort);
23127
+ console.error(`[post-startup ${elapsed()}] done`);
22881
23128
  }
22882
23129
  if (!noOpen) {
22883
23130
  const url2 = `http://localhost:${actualPort}`;
@@ -22894,7 +23141,7 @@ async function restorePreviousProjects(dataDir, actualPort) {
22894
23141
  validProjects.push(prevDir);
22895
23142
  continue;
22896
23143
  }
22897
- if (!existsSync17(prevDir)) continue;
23144
+ if (!existsSync18(prevDir)) continue;
22898
23145
  try {
22899
23146
  await registerProject(prevDir, actualPort);
22900
23147
  validProjects.push(prevDir);
@@ -22931,7 +23178,7 @@ async function cleanupStaleChannels() {
22931
23178
  await cleanupStaleChannel2(p.dataDir);
22932
23179
  }
22933
23180
  }
22934
- async function setupSkillsAndChannels() {
23181
+ async function setupSkillsAndChannels(port) {
22935
23182
  const { getAllProjects: getAllProjects2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
22936
23183
  const { ensureSkillsForDir: ensureSkillsForDir2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
22937
23184
  for (const p of getAllProjects2()) {
@@ -22942,6 +23189,18 @@ async function setupSkillsAndChannels() {
22942
23189
  if (readGlobalConfig3().channelEnabled === true) {
22943
23190
  const { registerChannelForAll: registerChannelForAll2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
22944
23191
  registerChannelForAll2(getAllProjects2().map((p) => p.dataDir));
23192
+ const { installHeartbeatHook: installHeartbeatHook2 } = await Promise.resolve().then(() => (init_claude_hooks(), claude_hooks_exports));
23193
+ installHeartbeatHook2(port);
23194
+ }
23195
+ }
23196
+ async function ensureHooksForRunningInstance(port) {
23197
+ try {
23198
+ const { readGlobalConfig: readGlobalConfig3 } = await Promise.resolve().then(() => (init_global_config(), global_config_exports));
23199
+ if (readGlobalConfig3().channelEnabled === true) {
23200
+ const { installHeartbeatHook: installHeartbeatHook2 } = await Promise.resolve().then(() => (init_claude_hooks(), claude_hooks_exports));
23201
+ installHeartbeatHook2(port);
23202
+ }
23203
+ } catch {
22945
23204
  }
22946
23205
  }
22947
23206
  function setupInstanceLifecycle(actualPort) {
@@ -22958,6 +23217,13 @@ function setupInstanceLifecycle(actualPort) {
22958
23217
  });
22959
23218
  }
22960
23219
  async function main() {
23220
+ const t0 = Date.now();
23221
+ const elapsed = () => `${Date.now() - t0}ms`;
23222
+ const watchdog = setTimeout(() => {
23223
+ console.error(`[startup] WARNING: startup has taken ${elapsed()} \u2014 still not ready`);
23224
+ console.error("[startup] This may indicate a hang in DB init, network check, or project restore.");
23225
+ console.error("[startup] Check the timing logs above to identify the stuck phase.");
23226
+ }, 1e4);
22961
23227
  const parsed = parseArgs(process.argv);
22962
23228
  if (!parsed) {
22963
23229
  printUsage();
@@ -22965,8 +23231,11 @@ async function main() {
22965
23231
  }
22966
23232
  const { port, demo, forceUpdateCheck, noOpen, strictPort } = parsed;
22967
23233
  let { dataDir } = parsed;
23234
+ console.error(`[startup ${elapsed()}] parsed args`);
22968
23235
  await handleEarlyFlags(parsed);
23236
+ console.error(`[startup ${elapsed()}] checking for updates...`);
22969
23237
  await checkForUpdates(forceUpdateCheck);
23238
+ console.error(`[startup ${elapsed()}] update check done`);
22970
23239
  if (demo !== null) {
22971
23240
  const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
22972
23241
  if (!scenario) {
@@ -22977,17 +23246,22 @@ async function main() {
22977
23246
  }
22978
23247
  process.exit(1);
22979
23248
  }
22980
- dataDir = join18(tmpdir2(), `hotsheet-demo-${Date.now()}`);
23249
+ dataDir = join19(tmpdir2(), `hotsheet-demo-${Date.now()}`);
22981
23250
  console.log(`
22982
23251
  DEMO MODE: ${scenario.label}
22983
23252
  `);
22984
23253
  }
22985
23254
  if (demo === null) {
23255
+ console.error(`[startup ${elapsed()}] cleaning up stale instances...`);
22986
23256
  await cleanupStaleInstance();
23257
+ console.error(`[startup ${elapsed()}] stale cleanup done`);
22987
23258
  const instance = readInstanceFile();
22988
23259
  if (instance !== null) {
23260
+ console.error(`[startup ${elapsed()}] checking if instance on port ${instance.port} is running...`);
22989
23261
  const running = await isInstanceRunning(instance.port);
23262
+ console.error(`[startup ${elapsed()}] instance check: running=${running}`);
22990
23263
  if (running) {
23264
+ await ensureHooksForRunningInstance(instance.port);
22991
23265
  if (!noOpen) {
22992
23266
  await joinRunningInstance(instance.port, dataDir);
22993
23267
  } else {
@@ -23010,13 +23284,21 @@ async function main() {
23010
23284
  }
23011
23285
  }
23012
23286
  }
23287
+ console.error(`[startup ${elapsed()}] initializing project...`);
23013
23288
  const db = await initializeProject(dataDir, demo);
23289
+ console.error(`[startup ${elapsed()}] project initialized`);
23014
23290
  if (demo !== null) {
23015
23291
  writeFileSettings(dataDir, { appName: "Hot Sheet Demo" });
23016
23292
  }
23293
+ console.error(`[startup ${elapsed()}] starting server...`);
23017
23294
  const { actualPort, secret } = await startAndConfigure(port, dataDir, strictPort);
23295
+ console.error(`[startup ${elapsed()}] server started on port ${actualPort}`);
23018
23296
  registerExistingProject(dataDir, secret, db);
23297
+ console.error(`[startup ${elapsed()}] running post-startup tasks...`);
23019
23298
  await postStartup(dataDir, actualPort, demo, noOpen);
23299
+ console.error(`[startup ${elapsed()}] post-startup complete`);
23300
+ clearTimeout(watchdog);
23301
+ console.error(`[startup ${elapsed()}] startup finished`);
23020
23302
  if (demo !== null) {
23021
23303
  const { seedDemoExtraProjects: seedDemoExtraProjects2 } = await Promise.resolve().then(() => (init_demo(), demo_exports));
23022
23304
  await seedDemoExtraProjects2(demo, dataDir, actualPort);