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 +206 -41
- package/dist/client/app.global.js +46 -46
- package/dist/client/assets/icon-default.png +0 -0
- package/dist/client/assets/icon-variant-1.png +0 -0
- package/dist/client/assets/icon-variant-2.png +0 -0
- package/dist/client/assets/icon-variant-3.png +0 -0
- package/dist/client/assets/icon-variant-4.png +0 -0
- package/dist/client/assets/icon-variant-5.png +0 -0
- package/dist/client/assets/icon-variant-6.png +0 -0
- package/dist/client/assets/icon-variant-7.png +0 -0
- package/dist/client/assets/icon-variant-8.png +0 -0
- package/dist/client/assets/icon-variant-9.png +0 -0
- package/dist/client/styles.css +1 -1
- package/package.json +1 -1
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 =
|
|
458
|
+
const distPath = resolve2(thisDir, "channel.js");
|
|
438
459
|
if (existsSync6(distPath)) {
|
|
439
460
|
return { command: "node", args: [distPath] };
|
|
440
461
|
}
|
|
441
|
-
const srcPath =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2663
|
-
|
|
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),
|
|
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
|
|
2810
|
-
|
|
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((
|
|
2820
|
-
pollWaiters.push(
|
|
2899
|
+
new Promise((resolve4) => {
|
|
2900
|
+
pollWaiters.push(resolve4);
|
|
2821
2901
|
}),
|
|
2822
|
-
new Promise((
|
|
2823
|
-
setTimeout(() =>
|
|
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
|
-
|
|
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("
|
|
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((
|
|
4075
|
+
return new Promise((resolve4, reject) => {
|
|
3942
4076
|
const server = serve({ fetch: fetch2, port: port2 });
|
|
3943
4077
|
server.on("listening", () => {
|
|
3944
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
4228
|
+
resolve4(JSON.parse(data).version);
|
|
4065
4229
|
} catch {
|
|
4066
|
-
|
|
4230
|
+
resolve4(null);
|
|
4067
4231
|
}
|
|
4068
4232
|
});
|
|
4069
4233
|
});
|
|
4070
4234
|
req.on("error", () => {
|
|
4071
|
-
|
|
4235
|
+
resolve4(null);
|
|
4072
4236
|
});
|
|
4073
4237
|
req.on("timeout", () => {
|
|
4074
4238
|
req.destroy();
|
|
4075
|
-
|
|
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 =
|
|
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);
|