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 +175 -37
- package/dist/client/app.global.js +40 -40
- 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
|
}
|
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2663
|
-
|
|
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),
|
|
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
|
|
2810
|
-
|
|
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((
|
|
2820
|
-
pollWaiters.push(
|
|
2892
|
+
new Promise((resolve4) => {
|
|
2893
|
+
pollWaiters.push(resolve4);
|
|
2821
2894
|
}),
|
|
2822
|
-
new Promise((
|
|
2823
|
-
setTimeout(() =>
|
|
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
|
-
|
|
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((
|
|
4065
|
+
return new Promise((resolve4, reject) => {
|
|
3942
4066
|
const server = serve({ fetch: fetch2, port: port2 });
|
|
3943
4067
|
server.on("listening", () => {
|
|
3944
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
4201
|
+
resolve4(JSON.parse(data).version);
|
|
4065
4202
|
} catch {
|
|
4066
|
-
|
|
4203
|
+
resolve4(null);
|
|
4067
4204
|
}
|
|
4068
4205
|
});
|
|
4069
4206
|
});
|
|
4070
4207
|
req.on("error", () => {
|
|
4071
|
-
|
|
4208
|
+
resolve4(null);
|
|
4072
4209
|
});
|
|
4073
4210
|
req.on("timeout", () => {
|
|
4074
4211
|
req.destroy();
|
|
4075
|
-
|
|
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 =
|
|
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);
|