hotsheet 0.6.5 → 0.7.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
@@ -129,12 +129,14 @@ var init_connection = __esm({
129
129
  // src/file-settings.ts
130
130
  var file_settings_exports = {};
131
131
  __export(file_settings_exports, {
132
+ ensureSecret: () => ensureSecret,
132
133
  getBackupDir: () => getBackupDir,
133
134
  readFileSettings: () => readFileSettings,
134
135
  writeFileSettings: () => writeFileSettings
135
136
  });
137
+ import { createHash, randomBytes } from "crypto";
136
138
  import { existsSync, readFileSync, writeFileSync } from "fs";
137
- import { join as join2 } from "path";
139
+ import { join as join2, resolve } from "path";
138
140
  function settingsPath(dataDir2) {
139
141
  return join2(dataDir2, "settings.json");
140
142
  }
@@ -157,6 +159,25 @@ function getBackupDir(dataDir2) {
157
159
  const settings = readFileSettings(dataDir2);
158
160
  return settings.backupDir || join2(dataDir2, "backups");
159
161
  }
162
+ function hashPath(dataDir2) {
163
+ const absPath = resolve(settingsPath(dataDir2));
164
+ return createHash("sha256").update(absPath).digest("hex").slice(0, 16);
165
+ }
166
+ function ensureSecret(dataDir2, port2) {
167
+ const settings = readFileSettings(dataDir2);
168
+ const currentPathHash = hashPath(dataDir2);
169
+ if (settings.secret && settings.secretPathHash === currentPathHash) {
170
+ if (settings.port !== port2) {
171
+ writeFileSettings(dataDir2, { port: port2 });
172
+ }
173
+ return settings.secret;
174
+ }
175
+ const random = randomBytes(32).toString("hex");
176
+ const absPath = resolve(settingsPath(dataDir2));
177
+ const secret = createHash("sha256").update(absPath + random).digest("hex").slice(0, 32);
178
+ writeFileSettings(dataDir2, { secret, secretPathHash: currentPathHash, port: port2 });
179
+ return secret;
180
+ }
160
181
  var init_file_settings = __esm({
161
182
  "src/file-settings.ts"() {
162
183
  "use strict";
@@ -430,15 +451,15 @@ __export(channel_config_exports, {
430
451
  unregisterChannel: () => unregisterChannel
431
452
  });
432
453
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
433
- import { dirname, join as join8, resolve } from "path";
454
+ import { dirname, join as join8, resolve as resolve2 } from "path";
434
455
  import { fileURLToPath } from "url";
435
456
  function getChannelServerPath() {
436
457
  const thisDir = dirname(fileURLToPath(import.meta.url));
437
- const distPath = resolve(thisDir, "channel.js");
458
+ const distPath = resolve2(thisDir, "channel.js");
438
459
  if (existsSync6(distPath)) {
439
460
  return { command: "node", args: [distPath] };
440
461
  }
441
- const srcPath = resolve(thisDir, "channel.ts");
462
+ const srcPath = resolve2(thisDir, "channel.ts");
442
463
  if (existsSync6(srcPath)) {
443
464
  return { command: "npx", args: ["tsx", srcPath] };
444
465
  }
@@ -524,7 +545,7 @@ var init_channel_config = __esm({
524
545
  // src/cli.ts
525
546
  import { mkdirSync as mkdirSync6 } from "fs";
526
547
  import { tmpdir } from "os";
527
- import { join as join12, resolve as resolve2 } from "path";
548
+ import { join as join12, resolve as resolve3 } from "path";
528
549
 
529
550
  // src/backup.ts
530
551
  init_connection();
@@ -967,7 +988,7 @@ async function getTickets(filters = {}) {
967
988
  paramIdx++;
968
989
  }
969
990
  if (filters.search !== void 0 && filters.search !== "") {
970
- conditions.push(`(title ILIKE $${paramIdx} OR details ILIKE $${paramIdx} OR ticket_number ILIKE $${paramIdx})`);
991
+ conditions.push(`(title ILIKE $${paramIdx} OR details ILIKE $${paramIdx} OR ticket_number ILIKE $${paramIdx} OR tags ILIKE $${paramIdx})`);
971
992
  values.push(`%${filters.search}%`);
972
993
  paramIdx++;
973
994
  }
@@ -1185,6 +1206,16 @@ async function queryTickets(logic, conditions, sortBy, sortDir, requiredTag) {
1185
1206
  function normalizeTag(input) {
1186
1207
  return input.replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase();
1187
1208
  }
1209
+ function extractBracketTags(input) {
1210
+ const tags = [];
1211
+ const cleaned = input.replace(/\[([^\]]*)\]/g, (_match, content) => {
1212
+ const tag = normalizeTag(content);
1213
+ if (tag && !tags.includes(tag)) tags.push(tag);
1214
+ return " ";
1215
+ });
1216
+ const title = cleaned.replace(/\s+/g, " ").trim();
1217
+ return { title, tags };
1218
+ }
1188
1219
  async function getAllTags() {
1189
1220
  const db2 = await getDb();
1190
1221
  const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
@@ -1316,6 +1347,7 @@ async function cleanupAttachments() {
1316
1347
 
1317
1348
  // src/cli.ts
1318
1349
  init_connection();
1350
+ init_file_settings();
1319
1351
 
1320
1352
  // src/lock.ts
1321
1353
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -2344,6 +2376,7 @@ async function seedDemoData(scenario) {
2344
2376
  init_gitignore();
2345
2377
 
2346
2378
  // src/server.ts
2379
+ init_file_settings();
2347
2380
  import { serve } from "@hono/node-server";
2348
2381
  import { exec } from "child_process";
2349
2382
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -2357,9 +2390,10 @@ import { Hono } from "hono";
2357
2390
  import { basename, extname, join as join9, relative as relative2 } from "path";
2358
2391
 
2359
2392
  // src/skills.ts
2393
+ init_file_settings();
2360
2394
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2361
2395
  import { join as join6, relative } from "path";
2362
- var SKILL_VERSION = 3;
2396
+ var SKILL_VERSION = 4;
2363
2397
  var skillPort;
2364
2398
  var skillDataDir;
2365
2399
  var skillCategories = DEFAULT_CATEGORIES;
@@ -2398,7 +2432,10 @@ function updateFile(path, content) {
2398
2432
  return true;
2399
2433
  }
2400
2434
  function ticketSkillBody(skill) {
2401
- return [
2435
+ const settings = readFileSettings(skillDataDir);
2436
+ const secret = settings.secret || "";
2437
+ const secretLine = secret ? ` -H "X-Hotsheet-Secret: ${secret}" \\` : "";
2438
+ const lines = [
2402
2439
  `Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
2403
2440
  "",
2404
2441
  "**Parsing the input:**",
@@ -2408,16 +2445,25 @@ function ticketSkillBody(skill) {
2408
2445
  "**Create the ticket** by running:",
2409
2446
  "```bash",
2410
2447
  `curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
2411
- ' -H "Content-Type: application/json" \\',
2448
+ ' -H "Content-Type: application/json" \\'
2449
+ ];
2450
+ if (secretLine) lines.push(secretLine);
2451
+ lines.push(
2412
2452
  ` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
2413
2453
  "```",
2414
2454
  "",
2455
+ `If the request fails (connection refused or 403), re-read \`.hotsheet/settings.json\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`,
2456
+ "",
2415
2457
  "Report the created ticket number and title to the user."
2416
- ].join("\n");
2458
+ );
2459
+ return lines.join("\n");
2417
2460
  }
2418
2461
  function mainSkillBody() {
2419
2462
  const worklistRel = relative(process.cwd(), join6(skillDataDir, "worklist.md"));
2463
+ const settingsRel = relative(process.cwd(), join6(skillDataDir, "settings.json"));
2420
2464
  return [
2465
+ `Base directory for this skill: ${join6(process.cwd(), ".claude", "skills", "hotsheet")}`,
2466
+ "",
2421
2467
  `Read \`${worklistRel}\` and work through the tickets in priority order.`,
2422
2468
  "",
2423
2469
  "For each ticket:",
@@ -2425,7 +2471,9 @@ function mainSkillBody() {
2425
2471
  "2. Implement the work described",
2426
2472
  "3. When complete, mark it done via the Hot Sheet UI",
2427
2473
  "",
2428
- "Work through them in order of priority, where reasonable."
2474
+ "Work through them in order of priority, where reasonable.",
2475
+ "",
2476
+ `If API calls fail (connection refused or 403), re-read \`${settingsRel}\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`
2429
2477
  ].join("\n");
2430
2478
  }
2431
2479
  var HOTSHEET_ALLOW_PATTERNS = [
@@ -2606,6 +2654,7 @@ function consumeSkillsCreatedFlag() {
2606
2654
  // src/sync/markdown.ts
2607
2655
  import { writeFileSync as writeFileSync5 } from "fs";
2608
2656
  import { join as join7 } from "path";
2657
+ init_file_settings();
2609
2658
  var dataDir;
2610
2659
  var port;
2611
2660
  var worklistTimeout = null;
@@ -2642,7 +2691,7 @@ function parseTicketNotes(raw) {
2642
2691
  if (raw.trim()) return [{ text: raw, created_at: "" }];
2643
2692
  return [];
2644
2693
  }
2645
- async function formatTicket(ticket) {
2694
+ async function formatTicket(ticket, autoContext) {
2646
2695
  const attachments = await getAttachments(ticket.id);
2647
2696
  const lines = [];
2648
2697
  lines.push(`TICKET ${ticket.ticket_number}:`);
@@ -2651,16 +2700,24 @@ async function formatTicket(ticket) {
2651
2700
  lines.push(`- Priority: ${ticket.priority}`);
2652
2701
  lines.push(`- Status: ${ticket.status.replace("_", " ")}`);
2653
2702
  lines.push(`- Title: ${ticket.title}`);
2703
+ let ticketTags = [];
2654
2704
  try {
2655
2705
  const tags = JSON.parse(ticket.tags);
2656
2706
  if (Array.isArray(tags) && tags.length > 0) {
2707
+ ticketTags = tags;
2657
2708
  const display = tags.map((t) => t.replace(/\b\w/g, (c) => c.toUpperCase()));
2658
2709
  lines.push(`- Tags: ${display.join(", ")}`);
2659
2710
  }
2660
2711
  } catch {
2661
2712
  }
2662
- if (ticket.details.trim()) {
2663
- const detailLines = ticket.details.split("\n");
2713
+ const contextParts = [];
2714
+ const catContext = autoContext.find((ac) => ac.type === "category" && ac.key === ticket.category);
2715
+ if (catContext) contextParts.push(catContext.text);
2716
+ const tagContexts = autoContext.filter((ac) => ac.type === "tag" && ticketTags.some((t) => t.toLowerCase() === ac.key.toLowerCase())).sort((a, b) => a.key.localeCompare(b.key));
2717
+ for (const tc of tagContexts) contextParts.push(tc.text);
2718
+ const fullDetails = contextParts.length > 0 ? contextParts.join("\n\n") + (ticket.details.trim() ? "\n\n" + ticket.details : "") : ticket.details;
2719
+ if (fullDetails.trim()) {
2720
+ const detailLines = fullDetails.split("\n");
2664
2721
  lines.push(`- Details: ${detailLines[0]}`);
2665
2722
  for (let i = 1; i < detailLines.length; i++) {
2666
2723
  lines.push(` ${detailLines[i]}`);
@@ -2682,6 +2739,17 @@ async function formatTicket(ticket) {
2682
2739
  }
2683
2740
  return lines.join("\n");
2684
2741
  }
2742
+ async function loadAutoContext() {
2743
+ try {
2744
+ const settings = await getSettings();
2745
+ if (settings.auto_context) {
2746
+ const parsed = JSON.parse(settings.auto_context);
2747
+ if (Array.isArray(parsed)) return parsed;
2748
+ }
2749
+ } catch {
2750
+ }
2751
+ return [];
2752
+ }
2685
2753
  async function formatCategoryDescriptions(usedCategories) {
2686
2754
  const allCategories = await getCategories();
2687
2755
  const descMap = Object.fromEntries(allCategories.map((c) => [c.id, c.description]));
@@ -2702,18 +2770,21 @@ async function syncWorklist() {
2702
2770
  sections.push("");
2703
2771
  sections.push("## Workflow");
2704
2772
  sections.push("");
2773
+ const settings = readFileSettings(dataDir);
2774
+ const secret = settings.secret || "";
2775
+ const secretHeader = secret ? ` -H "X-Hotsheet-Secret: ${secret}"` : "";
2705
2776
  sections.push(`The Hot Sheet API is available at http://localhost:${port}/api. **You MUST update ticket status** as you work \u2014 this is required, not optional.`);
2706
2777
  sections.push("");
2707
2778
  sections.push('- **BEFORE starting work on a ticket**, set its status to "started":');
2708
- sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json" -d '{"status": "started"}'\``);
2779
+ sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"status": "started"}'\``);
2709
2780
  sections.push("");
2710
2781
  sections.push('- **AFTER completing work on a ticket**, set its status to "completed" and **include notes** describing what was done:');
2711
- sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json" -d '{"status": "completed", "notes": "Describe the specific changes made"}'\``);
2782
+ sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"status": "completed", "notes": "Describe the specific changes made"}'\``);
2712
2783
  sections.push("");
2713
2784
  sections.push("**IMPORTANT:**");
2714
2785
  sections.push('- Update status for EVERY ticket \u2014 "started" when you begin, "completed" when you finish.');
2715
2786
  sections.push('- The "notes" field is REQUIRED when completing a ticket. Describe the specific work done.');
2716
- sections.push("- If an API call fails (e.g. connection refused, error response), log a visible warning to the user and continue your work. Do NOT silently skip status updates.");
2787
+ sections.push("- If an API call fails (e.g. connection refused, 403 secret mismatch, or error response), **re-read `.hotsheet/settings.json`** to get the correct `port` and `secret` values \u2014 you may be connecting to the wrong Hot Sheet instance. Log a visible warning to the user and continue your work. Do NOT silently skip status updates.");
2717
2788
  sections.push('- Do NOT set tickets to "verified" \u2014 that status is reserved for human review.');
2718
2789
  sections.push("");
2719
2790
  sections.push("## Creating Tickets");
@@ -2727,7 +2798,7 @@ async function syncWorklist() {
2727
2798
  sections.push("To create a ticket:");
2728
2799
  const allCats = await getCategories();
2729
2800
  const catIds = allCats.map((c) => c.id).join("|");
2730
- sections.push(` \`curl -s -X POST http://localhost:${port}/api/tickets -H "Content-Type: application/json" -d '{"title": "Title", "defaults": {"category": "${catIds}", "up_next": false}}'\``);
2801
+ sections.push(` \`curl -s -X POST http://localhost:${port}/api/tickets -H "Content-Type: application/json"${secretHeader} -d '{"title": "Title", "defaults": {"category": "${catIds}", "up_next": false}}'\``);
2731
2802
  sections.push("");
2732
2803
  sections.push('You can also include `"details"` in the defaults object for longer descriptions.');
2733
2804
  sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
@@ -2735,11 +2806,12 @@ async function syncWorklist() {
2735
2806
  if (tickets.length === 0) {
2736
2807
  sections.push("No items in the Up Next list.");
2737
2808
  } else {
2809
+ const autoContext = await loadAutoContext();
2738
2810
  for (const ticket of tickets) {
2739
2811
  categories.add(ticket.category);
2740
2812
  sections.push("---");
2741
2813
  sections.push("");
2742
- const formatted = await formatTicket(ticket);
2814
+ const formatted = await formatTicket(ticket, autoContext);
2743
2815
  sections.push(formatted);
2744
2816
  sections.push("");
2745
2817
  }
@@ -2764,12 +2836,13 @@ async function syncOpenTickets() {
2764
2836
  sections.push("");
2765
2837
  const started = tickets.filter((t) => t.status === "started");
2766
2838
  const notStarted = tickets.filter((t) => t.status === "not_started");
2839
+ const autoContext = await loadAutoContext();
2767
2840
  if (started.length > 0) {
2768
2841
  sections.push(`## Started (${started.length})`);
2769
2842
  sections.push("");
2770
2843
  for (const ticket of started) {
2771
2844
  categories.add(ticket.category);
2772
- const formatted = await formatTicket(ticket);
2845
+ const formatted = await formatTicket(ticket, autoContext);
2773
2846
  sections.push(formatted);
2774
2847
  sections.push("");
2775
2848
  }
@@ -2779,7 +2852,7 @@ async function syncOpenTickets() {
2779
2852
  sections.push("");
2780
2853
  for (const ticket of notStarted) {
2781
2854
  categories.add(ticket.category);
2782
- const formatted = await formatTicket(ticket);
2855
+ const formatted = await formatTicket(ticket, autoContext);
2783
2856
  sections.push(formatted);
2784
2857
  sections.push("");
2785
2858
  }
@@ -2806,8 +2879,8 @@ function notifyChange() {
2806
2879
  changeVersion++;
2807
2880
  const waiters = pollWaiters;
2808
2881
  pollWaiters = [];
2809
- for (const resolve3 of waiters) {
2810
- resolve3(changeVersion);
2882
+ for (const resolve4 of waiters) {
2883
+ resolve4(changeVersion);
2811
2884
  }
2812
2885
  }
2813
2886
  apiRoutes.get("/poll", async (c) => {
@@ -2816,11 +2889,11 @@ apiRoutes.get("/poll", async (c) => {
2816
2889
  return c.json({ version: changeVersion });
2817
2890
  }
2818
2891
  const version = await Promise.race([
2819
- new Promise((resolve3) => {
2820
- pollWaiters.push(resolve3);
2892
+ new Promise((resolve4) => {
2893
+ pollWaiters.push(resolve4);
2821
2894
  }),
2822
- new Promise((resolve3) => {
2823
- setTimeout(() => resolve3(changeVersion), 3e4);
2895
+ new Promise((resolve4) => {
2896
+ setTimeout(() => resolve4(changeVersion), 3e4);
2824
2897
  })
2825
2898
  ]);
2826
2899
  return c.json({ version });
@@ -2846,7 +2919,24 @@ apiRoutes.get("/tickets", async (c) => {
2846
2919
  });
2847
2920
  apiRoutes.post("/tickets", async (c) => {
2848
2921
  const body = await c.req.json();
2849
- const ticket = await createTicket(body.title || "", body.defaults);
2922
+ let title = body.title || "";
2923
+ const defaults = body.defaults || {};
2924
+ const { title: cleanTitle, tags: bracketTags } = extractBracketTags(title);
2925
+ if (bracketTags.length > 0) {
2926
+ title = cleanTitle || title;
2927
+ let existingTags = [];
2928
+ if (defaults.tags) {
2929
+ try {
2930
+ existingTags = JSON.parse(defaults.tags);
2931
+ } catch {
2932
+ }
2933
+ }
2934
+ for (const tag of bracketTags) {
2935
+ if (!existingTags.some((t) => t.toLowerCase() === tag.toLowerCase())) existingTags.push(tag);
2936
+ }
2937
+ defaults.tags = JSON.stringify(existingTags);
2938
+ }
2939
+ const ticket = await createTicket(title, defaults);
2850
2940
  scheduleAllSync();
2851
2941
  notifyChange();
2852
2942
  return c.json(ticket, 201);
@@ -3837,6 +3927,16 @@ pageRoutes.get("/", (c) => {
3837
3927
  ] }),
3838
3928
  /* @__PURE__ */ jsx("span", { children: "Backups" })
3839
3929
  ] }),
3930
+ /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "context", children: [
3931
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3932
+ /* @__PURE__ */ jsx("path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }),
3933
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
3934
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "8", y1: "13", y2: "13" }),
3935
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "8", y1: "17", y2: "17" }),
3936
+ /* @__PURE__ */ jsx("line", { x1: "10", x2: "8", y1: "9", y2: "9" })
3937
+ ] }),
3938
+ /* @__PURE__ */ jsx("span", { children: "Context" })
3939
+ ] }),
3840
3940
  /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "experimental", id: "settings-tab-experimental", style: "display:none", children: [
3841
3941
  /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3842
3942
  /* @__PURE__ */ jsx("path", { d: "M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" }),
@@ -3868,6 +3968,22 @@ pageRoutes.get("/", (c) => {
3868
3968
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3869
3969
  /* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
3870
3970
  /* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
3971
+ ] }),
3972
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3973
+ /* @__PURE__ */ jsx("label", { children: "When Claude needs permission" }),
3974
+ /* @__PURE__ */ jsx("select", { id: "settings-notify-permission", children: [
3975
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Don't notify" }),
3976
+ /* @__PURE__ */ jsx("option", { value: "once", children: "Notify once" }),
3977
+ /* @__PURE__ */ jsx("option", { value: "persistent", selected: true, children: "Notify until focused" })
3978
+ ] })
3979
+ ] }),
3980
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3981
+ /* @__PURE__ */ jsx("label", { children: "When Claude finishes work" }),
3982
+ /* @__PURE__ */ jsx("select", { id: "settings-notify-completed", children: [
3983
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Don't notify" }),
3984
+ /* @__PURE__ */ jsx("option", { value: "once", selected: true, children: "Notify once" }),
3985
+ /* @__PURE__ */ jsx("option", { value: "persistent", children: "Notify until focused" })
3986
+ ] })
3871
3987
  ] })
3872
3988
  ] }),
3873
3989
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
@@ -3890,6 +4006,14 @@ pageRoutes.get("/", (c) => {
3890
4006
  ] }),
3891
4007
  /* @__PURE__ */ jsx("div", { id: "backup-list", className: "backup-list", children: "Loading backups..." })
3892
4008
  ] }),
4009
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "context", children: [
4010
+ /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
4011
+ /* @__PURE__ */ jsx("h3", { children: "Auto-Context" }),
4012
+ /* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "auto-context-add-btn", children: "+ Add" })
4013
+ ] }),
4014
+ /* @__PURE__ */ jsx("span", { className: "settings-hint", style: "margin-bottom:12px;display:block", children: "Automatically prepend instructions to ticket details in the worklist, based on category or tag. Category context appears first, then tag context in alphabetical order." }),
4015
+ /* @__PURE__ */ jsx("div", { id: "auto-context-list" })
4016
+ ] }),
3893
4017
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "experimental", id: "settings-experimental-panel", style: "display:none", children: [
3894
4018
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3895
4019
  /* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
@@ -3938,10 +4062,10 @@ pageRoutes.get("/", (c) => {
3938
4062
 
3939
4063
  // src/server.ts
3940
4064
  function tryServe(fetch2, port2) {
3941
- return new Promise((resolve3, reject) => {
4065
+ return new Promise((resolve4, reject) => {
3942
4066
  const server = serve({ fetch: fetch2, port: port2 });
3943
4067
  server.on("listening", () => {
3944
- resolve3(port2);
4068
+ resolve4(port2);
3945
4069
  });
3946
4070
  server.on("error", (err) => {
3947
4071
  reject(err);
@@ -3973,6 +4097,19 @@ async function startServer(port2, dataDir2, options) {
3973
4097
  const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
3974
4098
  return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
3975
4099
  });
4100
+ app.use("/api/*", async (c, next) => {
4101
+ const headerSecret = c.req.header("X-Hotsheet-Secret");
4102
+ if (headerSecret) {
4103
+ const settings = readFileSettings(dataDir2);
4104
+ if (settings.secret && headerSecret !== settings.secret) {
4105
+ return c.json({
4106
+ error: "Secret mismatch \u2014 you may be connecting to the wrong Hot Sheet instance.",
4107
+ recovery: "Re-read .hotsheet/settings.json to get the correct port and secret, and re-read your skill files (e.g. .claude/skills/hotsheet/SKILL.md) for updated instructions."
4108
+ }, 403);
4109
+ }
4110
+ }
4111
+ await next();
4112
+ });
3976
4113
  app.route("/api", apiRoutes);
3977
4114
  app.route("/api/backups", backupRoutes);
3978
4115
  app.route("/", pageRoutes);
@@ -4049,10 +4186,10 @@ function isFirstUseToday() {
4049
4186
  return last !== today;
4050
4187
  }
4051
4188
  function fetchLatestVersion() {
4052
- return new Promise((resolve3) => {
4189
+ return new Promise((resolve4) => {
4053
4190
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
4054
4191
  if (res.statusCode !== 200) {
4055
- resolve3(null);
4192
+ resolve4(null);
4056
4193
  return;
4057
4194
  }
4058
4195
  let data = "";
@@ -4061,18 +4198,18 @@ function fetchLatestVersion() {
4061
4198
  });
4062
4199
  res.on("end", () => {
4063
4200
  try {
4064
- resolve3(JSON.parse(data).version);
4201
+ resolve4(JSON.parse(data).version);
4065
4202
  } catch {
4066
- resolve3(null);
4203
+ resolve4(null);
4067
4204
  }
4068
4205
  });
4069
4206
  });
4070
4207
  req.on("error", () => {
4071
- resolve3(null);
4208
+ resolve4(null);
4072
4209
  });
4073
4210
  req.on("timeout", () => {
4074
4211
  req.destroy();
4075
- resolve3(null);
4212
+ resolve4(null);
4076
4213
  });
4077
4214
  });
4078
4215
  }
@@ -4174,7 +4311,7 @@ function parseArgs(argv) {
4174
4311
  }
4175
4312
  break;
4176
4313
  case "--data-dir":
4177
- dataDir2 = resolve2(args[++i]);
4314
+ dataDir2 = resolve3(args[++i]);
4178
4315
  break;
4179
4316
  case "--check-for-updates":
4180
4317
  forceUpdateCheck = true;
@@ -4232,6 +4369,7 @@ async function main() {
4232
4369
  }
4233
4370
  console.log(` Data directory: ${dataDir2}`);
4234
4371
  const actualPort = await startServer(port2, dataDir2, { noOpen, strictPort });
4372
+ ensureSecret(dataDir2, actualPort);
4235
4373
  initMarkdownSync(dataDir2, actualPort);
4236
4374
  scheduleAllSync();
4237
4375
  initSkills(actualPort, dataDir2);