hotsheet 0.6.5 → 0.8.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
  }
@@ -498,10 +519,17 @@ async function isChannelAlive(dataDir2) {
498
519
  async function triggerChannel(dataDir2, serverPort, message) {
499
520
  const port2 = getChannelPort(dataDir2);
500
521
  if (!port2) return false;
522
+ let secretHeader = "";
523
+ try {
524
+ const { readFileSettings: readFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
525
+ const settings = readFileSettings2(dataDir2);
526
+ if (settings.secret) secretHeader = ` -H "X-Hotsheet-Secret: ${settings.secret}"`;
527
+ } catch {
528
+ }
501
529
  const doneSignal = `
502
530
 
503
531
  When you are completely finished (or if there was nothing to do), signal completion by running:
504
- curl -s -X POST http://localhost:${serverPort}/api/channel/done`;
532
+ curl -s -X POST http://localhost:${serverPort}/api/channel/done${secretHeader}`;
505
533
  const content = message ? message + doneSignal : "Process the Hot Sheet worklist. Run /hotsheet to work through the current Up Next items." + doneSignal;
506
534
  try {
507
535
  const res = await fetch(`http://127.0.0.1:${port2}/trigger`, {
@@ -524,7 +552,7 @@ var init_channel_config = __esm({
524
552
  // src/cli.ts
525
553
  import { mkdirSync as mkdirSync6 } from "fs";
526
554
  import { tmpdir } from "os";
527
- import { join as join12, resolve as resolve2 } from "path";
555
+ import { join as join12, resolve as resolve3 } from "path";
528
556
 
529
557
  // src/backup.ts
530
558
  init_connection();
@@ -967,7 +995,7 @@ async function getTickets(filters = {}) {
967
995
  paramIdx++;
968
996
  }
969
997
  if (filters.search !== void 0 && filters.search !== "") {
970
- conditions.push(`(title ILIKE $${paramIdx} OR details ILIKE $${paramIdx} OR ticket_number ILIKE $${paramIdx})`);
998
+ conditions.push(`(title ILIKE $${paramIdx} OR details ILIKE $${paramIdx} OR ticket_number ILIKE $${paramIdx} OR tags ILIKE $${paramIdx})`);
971
999
  values.push(`%${filters.search}%`);
972
1000
  paramIdx++;
973
1001
  }
@@ -1185,6 +1213,16 @@ async function queryTickets(logic, conditions, sortBy, sortDir, requiredTag) {
1185
1213
  function normalizeTag(input) {
1186
1214
  return input.replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase();
1187
1215
  }
1216
+ function extractBracketTags(input) {
1217
+ const tags = [];
1218
+ const cleaned = input.replace(/\[([^\]]*)\]/g, (_match, content) => {
1219
+ const tag = normalizeTag(content);
1220
+ if (tag && !tags.includes(tag)) tags.push(tag);
1221
+ return " ";
1222
+ });
1223
+ const title = cleaned.replace(/\s+/g, " ").trim();
1224
+ return { title, tags };
1225
+ }
1188
1226
  async function getAllTags() {
1189
1227
  const db2 = await getDb();
1190
1228
  const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
@@ -1316,6 +1354,7 @@ async function cleanupAttachments() {
1316
1354
 
1317
1355
  // src/cli.ts
1318
1356
  init_connection();
1357
+ init_file_settings();
1319
1358
 
1320
1359
  // src/lock.ts
1321
1360
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -2344,6 +2383,7 @@ async function seedDemoData(scenario) {
2344
2383
  init_gitignore();
2345
2384
 
2346
2385
  // src/server.ts
2386
+ init_file_settings();
2347
2387
  import { serve } from "@hono/node-server";
2348
2388
  import { exec } from "child_process";
2349
2389
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -2357,9 +2397,10 @@ import { Hono } from "hono";
2357
2397
  import { basename, extname, join as join9, relative as relative2 } from "path";
2358
2398
 
2359
2399
  // src/skills.ts
2400
+ init_file_settings();
2360
2401
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2361
2402
  import { join as join6, relative } from "path";
2362
- var SKILL_VERSION = 3;
2403
+ var SKILL_VERSION = 4;
2363
2404
  var skillPort;
2364
2405
  var skillDataDir;
2365
2406
  var skillCategories = DEFAULT_CATEGORIES;
@@ -2398,7 +2439,10 @@ function updateFile(path, content) {
2398
2439
  return true;
2399
2440
  }
2400
2441
  function ticketSkillBody(skill) {
2401
- return [
2442
+ const settings = readFileSettings(skillDataDir);
2443
+ const secret = settings.secret || "";
2444
+ const secretLine = secret ? ` -H "X-Hotsheet-Secret: ${secret}" \\` : "";
2445
+ const lines = [
2402
2446
  `Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
2403
2447
  "",
2404
2448
  "**Parsing the input:**",
@@ -2408,16 +2452,25 @@ function ticketSkillBody(skill) {
2408
2452
  "**Create the ticket** by running:",
2409
2453
  "```bash",
2410
2454
  `curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
2411
- ' -H "Content-Type: application/json" \\',
2455
+ ' -H "Content-Type: application/json" \\'
2456
+ ];
2457
+ if (secretLine) lines.push(secretLine);
2458
+ lines.push(
2412
2459
  ` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
2413
2460
  "```",
2414
2461
  "",
2462
+ `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.`,
2463
+ "",
2415
2464
  "Report the created ticket number and title to the user."
2416
- ].join("\n");
2465
+ );
2466
+ return lines.join("\n");
2417
2467
  }
2418
2468
  function mainSkillBody() {
2419
2469
  const worklistRel = relative(process.cwd(), join6(skillDataDir, "worklist.md"));
2470
+ const settingsRel = relative(process.cwd(), join6(skillDataDir, "settings.json"));
2420
2471
  return [
2472
+ `Base directory for this skill: ${join6(process.cwd(), ".claude", "skills", "hotsheet")}`,
2473
+ "",
2421
2474
  `Read \`${worklistRel}\` and work through the tickets in priority order.`,
2422
2475
  "",
2423
2476
  "For each ticket:",
@@ -2425,7 +2478,9 @@ function mainSkillBody() {
2425
2478
  "2. Implement the work described",
2426
2479
  "3. When complete, mark it done via the Hot Sheet UI",
2427
2480
  "",
2428
- "Work through them in order of priority, where reasonable."
2481
+ "Work through them in order of priority, where reasonable.",
2482
+ "",
2483
+ `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
2484
  ].join("\n");
2430
2485
  }
2431
2486
  var HOTSHEET_ALLOW_PATTERNS = [
@@ -2606,6 +2661,7 @@ function consumeSkillsCreatedFlag() {
2606
2661
  // src/sync/markdown.ts
2607
2662
  import { writeFileSync as writeFileSync5 } from "fs";
2608
2663
  import { join as join7 } from "path";
2664
+ init_file_settings();
2609
2665
  var dataDir;
2610
2666
  var port;
2611
2667
  var worklistTimeout = null;
@@ -2642,7 +2698,7 @@ function parseTicketNotes(raw) {
2642
2698
  if (raw.trim()) return [{ text: raw, created_at: "" }];
2643
2699
  return [];
2644
2700
  }
2645
- async function formatTicket(ticket) {
2701
+ async function formatTicket(ticket, autoContext) {
2646
2702
  const attachments = await getAttachments(ticket.id);
2647
2703
  const lines = [];
2648
2704
  lines.push(`TICKET ${ticket.ticket_number}:`);
@@ -2651,16 +2707,24 @@ async function formatTicket(ticket) {
2651
2707
  lines.push(`- Priority: ${ticket.priority}`);
2652
2708
  lines.push(`- Status: ${ticket.status.replace("_", " ")}`);
2653
2709
  lines.push(`- Title: ${ticket.title}`);
2710
+ let ticketTags = [];
2654
2711
  try {
2655
2712
  const tags = JSON.parse(ticket.tags);
2656
2713
  if (Array.isArray(tags) && tags.length > 0) {
2714
+ ticketTags = tags;
2657
2715
  const display = tags.map((t) => t.replace(/\b\w/g, (c) => c.toUpperCase()));
2658
2716
  lines.push(`- Tags: ${display.join(", ")}`);
2659
2717
  }
2660
2718
  } catch {
2661
2719
  }
2662
- if (ticket.details.trim()) {
2663
- const detailLines = ticket.details.split("\n");
2720
+ const contextParts = [];
2721
+ const catContext = autoContext.find((ac) => ac.type === "category" && ac.key === ticket.category);
2722
+ if (catContext) contextParts.push(catContext.text);
2723
+ const tagContexts = autoContext.filter((ac) => ac.type === "tag" && ticketTags.some((t) => t.toLowerCase() === ac.key.toLowerCase())).sort((a, b) => a.key.localeCompare(b.key));
2724
+ for (const tc of tagContexts) contextParts.push(tc.text);
2725
+ const fullDetails = contextParts.length > 0 ? contextParts.join("\n\n") + (ticket.details.trim() ? "\n\n" + ticket.details : "") : ticket.details;
2726
+ if (fullDetails.trim()) {
2727
+ const detailLines = fullDetails.split("\n");
2664
2728
  lines.push(`- Details: ${detailLines[0]}`);
2665
2729
  for (let i = 1; i < detailLines.length; i++) {
2666
2730
  lines.push(` ${detailLines[i]}`);
@@ -2682,6 +2746,17 @@ async function formatTicket(ticket) {
2682
2746
  }
2683
2747
  return lines.join("\n");
2684
2748
  }
2749
+ async function loadAutoContext() {
2750
+ try {
2751
+ const settings = await getSettings();
2752
+ if (settings.auto_context) {
2753
+ const parsed = JSON.parse(settings.auto_context);
2754
+ if (Array.isArray(parsed)) return parsed;
2755
+ }
2756
+ } catch {
2757
+ }
2758
+ return [];
2759
+ }
2685
2760
  async function formatCategoryDescriptions(usedCategories) {
2686
2761
  const allCategories = await getCategories();
2687
2762
  const descMap = Object.fromEntries(allCategories.map((c) => [c.id, c.description]));
@@ -2702,18 +2777,21 @@ async function syncWorklist() {
2702
2777
  sections.push("");
2703
2778
  sections.push("## Workflow");
2704
2779
  sections.push("");
2780
+ const settings = readFileSettings(dataDir);
2781
+ const secret = settings.secret || "";
2782
+ const secretHeader = secret ? ` -H "X-Hotsheet-Secret: ${secret}"` : "";
2705
2783
  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
2784
  sections.push("");
2707
2785
  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"}'\``);
2786
+ sections.push(` \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"status": "started"}'\``);
2709
2787
  sections.push("");
2710
2788
  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"}'\``);
2789
+ 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
2790
  sections.push("");
2713
2791
  sections.push("**IMPORTANT:**");
2714
2792
  sections.push('- Update status for EVERY ticket \u2014 "started" when you begin, "completed" when you finish.');
2715
2793
  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.");
2794
+ 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
2795
  sections.push('- Do NOT set tickets to "verified" \u2014 that status is reserved for human review.');
2718
2796
  sections.push("");
2719
2797
  sections.push("## Creating Tickets");
@@ -2727,7 +2805,7 @@ async function syncWorklist() {
2727
2805
  sections.push("To create a ticket:");
2728
2806
  const allCats = await getCategories();
2729
2807
  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}}'\``);
2808
+ 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
2809
  sections.push("");
2732
2810
  sections.push('You can also include `"details"` in the defaults object for longer descriptions.');
2733
2811
  sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
@@ -2735,11 +2813,12 @@ async function syncWorklist() {
2735
2813
  if (tickets.length === 0) {
2736
2814
  sections.push("No items in the Up Next list.");
2737
2815
  } else {
2816
+ const autoContext = await loadAutoContext();
2738
2817
  for (const ticket of tickets) {
2739
2818
  categories.add(ticket.category);
2740
2819
  sections.push("---");
2741
2820
  sections.push("");
2742
- const formatted = await formatTicket(ticket);
2821
+ const formatted = await formatTicket(ticket, autoContext);
2743
2822
  sections.push(formatted);
2744
2823
  sections.push("");
2745
2824
  }
@@ -2764,12 +2843,13 @@ async function syncOpenTickets() {
2764
2843
  sections.push("");
2765
2844
  const started = tickets.filter((t) => t.status === "started");
2766
2845
  const notStarted = tickets.filter((t) => t.status === "not_started");
2846
+ const autoContext = await loadAutoContext();
2767
2847
  if (started.length > 0) {
2768
2848
  sections.push(`## Started (${started.length})`);
2769
2849
  sections.push("");
2770
2850
  for (const ticket of started) {
2771
2851
  categories.add(ticket.category);
2772
- const formatted = await formatTicket(ticket);
2852
+ const formatted = await formatTicket(ticket, autoContext);
2773
2853
  sections.push(formatted);
2774
2854
  sections.push("");
2775
2855
  }
@@ -2779,7 +2859,7 @@ async function syncOpenTickets() {
2779
2859
  sections.push("");
2780
2860
  for (const ticket of notStarted) {
2781
2861
  categories.add(ticket.category);
2782
- const formatted = await formatTicket(ticket);
2862
+ const formatted = await formatTicket(ticket, autoContext);
2783
2863
  sections.push(formatted);
2784
2864
  sections.push("");
2785
2865
  }
@@ -2806,8 +2886,8 @@ function notifyChange() {
2806
2886
  changeVersion++;
2807
2887
  const waiters = pollWaiters;
2808
2888
  pollWaiters = [];
2809
- for (const resolve3 of waiters) {
2810
- resolve3(changeVersion);
2889
+ for (const resolve4 of waiters) {
2890
+ resolve4(changeVersion);
2811
2891
  }
2812
2892
  }
2813
2893
  apiRoutes.get("/poll", async (c) => {
@@ -2816,11 +2896,11 @@ apiRoutes.get("/poll", async (c) => {
2816
2896
  return c.json({ version: changeVersion });
2817
2897
  }
2818
2898
  const version = await Promise.race([
2819
- new Promise((resolve3) => {
2820
- pollWaiters.push(resolve3);
2899
+ new Promise((resolve4) => {
2900
+ pollWaiters.push(resolve4);
2821
2901
  }),
2822
- new Promise((resolve3) => {
2823
- setTimeout(() => resolve3(changeVersion), 3e4);
2902
+ new Promise((resolve4) => {
2903
+ setTimeout(() => resolve4(changeVersion), 3e4);
2824
2904
  })
2825
2905
  ]);
2826
2906
  return c.json({ version });
@@ -2846,7 +2926,24 @@ apiRoutes.get("/tickets", async (c) => {
2846
2926
  });
2847
2927
  apiRoutes.post("/tickets", async (c) => {
2848
2928
  const body = await c.req.json();
2849
- const ticket = await createTicket(body.title || "", body.defaults);
2929
+ let title = body.title || "";
2930
+ const defaults = body.defaults || {};
2931
+ const { title: cleanTitle, tags: bracketTags } = extractBracketTags(title);
2932
+ if (bracketTags.length > 0) {
2933
+ title = cleanTitle || title;
2934
+ let existingTags = [];
2935
+ if (defaults.tags) {
2936
+ try {
2937
+ existingTags = JSON.parse(defaults.tags);
2938
+ } catch {
2939
+ }
2940
+ }
2941
+ for (const tag of bracketTags) {
2942
+ if (!existingTags.some((t) => t.toLowerCase() === tag.toLowerCase())) existingTags.push(tag);
2943
+ }
2944
+ defaults.tags = JSON.stringify(existingTags);
2945
+ }
2946
+ const ticket = await createTicket(title, defaults);
2850
2947
  scheduleAllSync();
2851
2948
  notifyChange();
2852
2949
  return c.json(ticket, 201);
@@ -3747,7 +3844,7 @@ pageRoutes.get("/", (c) => {
3747
3844
  /* @__PURE__ */ jsx("div", { id: "detail-attachments", className: "detail-attachments" }),
3748
3845
  /* @__PURE__ */ jsx("label", { className: "btn btn-sm upload-btn", children: [
3749
3846
  "Attach File",
3750
- /* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none" })
3847
+ /* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none", multiple: true })
3751
3848
  ] })
3752
3849
  ] }),
3753
3850
  /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section", children: [
@@ -3772,7 +3869,7 @@ pageRoutes.get("/", (c) => {
3772
3869
  /* @__PURE__ */ jsx("kbd", { children: "Enter" }),
3773
3870
  " new ticket"
3774
3871
  ] }),
3775
- /* @__PURE__ */ jsx("span", { children: [
3872
+ /* @__PURE__ */ jsx("span", { "data-hint": "category", children: [
3776
3873
  /* @__PURE__ */ jsx("kbd", { children: [
3777
3874
  "\u2318",
3778
3875
  "I/B/F/R/K/G"
@@ -3837,6 +3934,16 @@ pageRoutes.get("/", (c) => {
3837
3934
  ] }),
3838
3935
  /* @__PURE__ */ jsx("span", { children: "Backups" })
3839
3936
  ] }),
3937
+ /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "context", children: [
3938
+ /* @__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: [
3939
+ /* @__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" }),
3940
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
3941
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "8", y1: "13", y2: "13" }),
3942
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "8", y1: "17", y2: "17" }),
3943
+ /* @__PURE__ */ jsx("line", { x1: "10", x2: "8", y1: "9", y2: "9" })
3944
+ ] }),
3945
+ /* @__PURE__ */ jsx("span", { children: "Context" })
3946
+ ] }),
3840
3947
  /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "experimental", id: "settings-tab-experimental", style: "display:none", children: [
3841
3948
  /* @__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
3949
  /* @__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" }),
@@ -3858,7 +3965,10 @@ pageRoutes.get("/", (c) => {
3858
3965
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel active", "data-panel": "general", children: [
3859
3966
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3860
3967
  /* @__PURE__ */ jsx("label", { children: "App name" }),
3861
- /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" }),
3968
+ /* @__PURE__ */ jsx("div", { className: "settings-app-name-row", children: [
3969
+ /* @__PURE__ */ jsx("button", { className: "app-icon-picker-btn", id: "app-icon-picker-btn", title: "Change app icon", children: /* @__PURE__ */ jsx("img", { id: "app-icon-preview", src: "/static/assets/icon-default.png", width: "28", height: "28" }) }),
3970
+ /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" })
3971
+ ] }),
3862
3972
  /* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-app-name-hint", children: "Custom name shown in the title bar. Leave empty for default." })
3863
3973
  ] }),
3864
3974
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
@@ -3868,6 +3978,22 @@ pageRoutes.get("/", (c) => {
3868
3978
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3869
3979
  /* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
3870
3980
  /* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
3981
+ ] }),
3982
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3983
+ /* @__PURE__ */ jsx("label", { children: "When Claude needs permission" }),
3984
+ /* @__PURE__ */ jsx("select", { id: "settings-notify-permission", children: [
3985
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Don't notify" }),
3986
+ /* @__PURE__ */ jsx("option", { value: "once", children: "Notify once" }),
3987
+ /* @__PURE__ */ jsx("option", { value: "persistent", selected: true, children: "Notify until focused" })
3988
+ ] })
3989
+ ] }),
3990
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3991
+ /* @__PURE__ */ jsx("label", { children: "When Claude finishes work" }),
3992
+ /* @__PURE__ */ jsx("select", { id: "settings-notify-completed", children: [
3993
+ /* @__PURE__ */ jsx("option", { value: "none", children: "Don't notify" }),
3994
+ /* @__PURE__ */ jsx("option", { value: "once", selected: true, children: "Notify once" }),
3995
+ /* @__PURE__ */ jsx("option", { value: "persistent", children: "Notify until focused" })
3996
+ ] })
3871
3997
  ] })
3872
3998
  ] }),
3873
3999
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
@@ -3890,6 +4016,14 @@ pageRoutes.get("/", (c) => {
3890
4016
  ] }),
3891
4017
  /* @__PURE__ */ jsx("div", { id: "backup-list", className: "backup-list", children: "Loading backups..." })
3892
4018
  ] }),
4019
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "context", children: [
4020
+ /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
4021
+ /* @__PURE__ */ jsx("h3", { children: "Auto-Context" }),
4022
+ /* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "auto-context-add-btn", children: "+ Add" })
4023
+ ] }),
4024
+ /* @__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." }),
4025
+ /* @__PURE__ */ jsx("div", { id: "auto-context-list" })
4026
+ ] }),
3893
4027
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "experimental", id: "settings-experimental-panel", style: "display:none", children: [
3894
4028
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3895
4029
  /* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
@@ -3938,10 +4072,10 @@ pageRoutes.get("/", (c) => {
3938
4072
 
3939
4073
  // src/server.ts
3940
4074
  function tryServe(fetch2, port2) {
3941
- return new Promise((resolve3, reject) => {
4075
+ return new Promise((resolve4, reject) => {
3942
4076
  const server = serve({ fetch: fetch2, port: port2 });
3943
4077
  server.on("listening", () => {
3944
- resolve3(port2);
4078
+ resolve4(port2);
3945
4079
  });
3946
4080
  server.on("error", (err) => {
3947
4081
  reject(err);
@@ -3973,6 +4107,36 @@ async function startServer(port2, dataDir2, options) {
3973
4107
  const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
3974
4108
  return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
3975
4109
  });
4110
+ app.use("/api/*", async (c, next) => {
4111
+ const settings = readFileSettings(dataDir2);
4112
+ const expectedSecret = settings.secret;
4113
+ if (!expectedSecret) {
4114
+ await next();
4115
+ return;
4116
+ }
4117
+ const headerSecret = c.req.header("X-Hotsheet-Secret");
4118
+ const method = c.req.method;
4119
+ const isMutation = method === "POST" || method === "PATCH" || method === "PUT" || method === "DELETE";
4120
+ if (headerSecret) {
4121
+ if (headerSecret !== expectedSecret) {
4122
+ return c.json({
4123
+ error: "Secret mismatch \u2014 you may be connecting to the wrong Hot Sheet instance.",
4124
+ 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."
4125
+ }, 403);
4126
+ }
4127
+ } else if (isMutation) {
4128
+ const origin = c.req.header("Origin");
4129
+ const referer = c.req.header("Referer");
4130
+ const isBrowser = !!(origin || referer);
4131
+ if (!isBrowser) {
4132
+ return c.json({
4133
+ error: "Missing X-Hotsheet-Secret header. Read .hotsheet/settings.json for the correct port and secret.",
4134
+ recovery: "Re-read .hotsheet/settings.json to get the correct port and secret, and re-read your skill files for updated instructions."
4135
+ }, 403);
4136
+ }
4137
+ }
4138
+ await next();
4139
+ });
3976
4140
  app.route("/api", apiRoutes);
3977
4141
  app.route("/api/backups", backupRoutes);
3978
4142
  app.route("/", pageRoutes);
@@ -4049,10 +4213,10 @@ function isFirstUseToday() {
4049
4213
  return last !== today;
4050
4214
  }
4051
4215
  function fetchLatestVersion() {
4052
- return new Promise((resolve3) => {
4216
+ return new Promise((resolve4) => {
4053
4217
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
4054
4218
  if (res.statusCode !== 200) {
4055
- resolve3(null);
4219
+ resolve4(null);
4056
4220
  return;
4057
4221
  }
4058
4222
  let data = "";
@@ -4061,18 +4225,18 @@ function fetchLatestVersion() {
4061
4225
  });
4062
4226
  res.on("end", () => {
4063
4227
  try {
4064
- resolve3(JSON.parse(data).version);
4228
+ resolve4(JSON.parse(data).version);
4065
4229
  } catch {
4066
- resolve3(null);
4230
+ resolve4(null);
4067
4231
  }
4068
4232
  });
4069
4233
  });
4070
4234
  req.on("error", () => {
4071
- resolve3(null);
4235
+ resolve4(null);
4072
4236
  });
4073
4237
  req.on("timeout", () => {
4074
4238
  req.destroy();
4075
- resolve3(null);
4239
+ resolve4(null);
4076
4240
  });
4077
4241
  });
4078
4242
  }
@@ -4174,7 +4338,7 @@ function parseArgs(argv) {
4174
4338
  }
4175
4339
  break;
4176
4340
  case "--data-dir":
4177
- dataDir2 = resolve2(args[++i]);
4341
+ dataDir2 = resolve3(args[++i]);
4178
4342
  break;
4179
4343
  case "--check-for-updates":
4180
4344
  forceUpdateCheck = true;
@@ -4232,6 +4396,7 @@ async function main() {
4232
4396
  }
4233
4397
  console.log(` Data directory: ${dataDir2}`);
4234
4398
  const actualPort = await startServer(port2, dataDir2, { noOpen, strictPort });
4399
+ ensureSecret(dataDir2, actualPort);
4235
4400
  initMarkdownSync(dataDir2, actualPort);
4236
4401
  scheduleAllSync();
4237
4402
  initSkills(actualPort, dataDir2);