hotsheet 0.13.1 → 0.14.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/README.md +12 -10
- package/dist/channel.js +13 -2
- package/dist/cli.js +139 -47
- package/dist/client/app.global.js +48 -46
- package/dist/client/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ No cloud. No logins. No JIRA. Just tickets and a tight feedback loop.
|
|
|
21
21
|
| Linux | `.AppImage` / `.deb` |
|
|
22
22
|
| Windows | `.msi` / `.exe` |
|
|
23
23
|
|
|
24
|
-
After installing, open the app and click **Install CLI** to add the `hotsheet` command to your PATH.
|
|
24
|
+
After installing, open the app and use **Open Folder** to get started — or click **Install CLI** to add the `hotsheet` command to your PATH for terminal launching.
|
|
25
25
|
|
|
26
26
|
**Or install via npm:**
|
|
27
27
|
|
|
@@ -92,7 +92,7 @@ The loop stays tight because the AI always knows what to work on next.
|
|
|
92
92
|
<img src="docs/demo-5.png" alt="Multiple tickets selected with the batch toolbar and context menu" width="900">
|
|
93
93
|
</p>
|
|
94
94
|
|
|
95
|
-
**Detail panel** — side or bottom orientation (toggle in the toolbar), resizable. Shows category, priority, status, and Up Next in a compact grid, plus title, details, tags, attachments, and editable notes. Click a note to edit inline; right-click to delete.
|
|
95
|
+
**Detail panel** — side or bottom orientation (toggle in the toolbar), resizable, collapsible (click the active position to hide). Shows category, priority, status, and Up Next in a compact grid, plus title, details, tags, attachments, and editable notes. Click a note to edit inline; right-click to delete.
|
|
96
96
|
|
|
97
97
|
<p align="center">
|
|
98
98
|
<img src="docs/demo-6.png" alt="Detail panel in bottom orientation showing ticket details, tags, and notes" width="900">
|
|
@@ -118,16 +118,16 @@ The loop stays tight because the AI always knows what to work on next.
|
|
|
118
118
|
- **Up Next flag** — star tickets to add them to the AI worklist
|
|
119
119
|
- **Drag and drop** — drag tickets onto sidebar views to change category, priority, or status; drop files onto the detail panel to attach; reorder project tabs and custom views
|
|
120
120
|
- **Right-click context menus** — full context menu on tickets with category/priority/status submenus, tags, duplicate, backlog, archive, delete — all with Lucide icons
|
|
121
|
-
- **Search** — full-text search across ticket titles, details,
|
|
121
|
+
- **Search** — full-text search across ticket titles, details, ticket numbers, and tags
|
|
122
122
|
- **Print** — print the dashboard, all tickets, selected tickets, or individual tickets in checklist, summary, or full-detail format
|
|
123
123
|
- **Keyboard-driven** — `Enter` to create, `Cmd+I/B/F/R/K/G` for categories, `Alt+1-5` for priority, `Cmd+D` for Up Next, `Delete` to trash, `Cmd+P` to print, `Cmd+Z/Shift+Z` for undo/redo
|
|
124
124
|
- **Undo/redo** — `Cmd+Z` and `Cmd+Shift+Z` for all operations including notes, batch changes, and deletions
|
|
125
125
|
- **Animated transitions** — smooth FLIP animations when tickets reorder after property changes
|
|
126
|
-
- **Copy
|
|
126
|
+
- **Copy / cut / paste** — `Cmd+C` copies selected tickets (formatted text to clipboard + structured data for paste), `Cmd+X` cuts, `Cmd+V` pastes into the current project with title dedup. Works across projects.
|
|
127
127
|
- **File attachments** — attach files via file picker or drag-and-drop onto the detail panel, reveal in file manager
|
|
128
128
|
- **Markdown sync** — `worklist.md` and `open-tickets.md` auto-generated on every change
|
|
129
129
|
- **Automatic backups** — tiered snapshots (every 5 min, hourly, daily) with preview-before-restore recovery
|
|
130
|
-
- **Auto-cleanup** —
|
|
130
|
+
- **Auto-cleanup** — verified tickets auto-archive after a configurable number of days; trashed tickets auto-delete
|
|
131
131
|
- **App icon variants** — 9 icon variants to choose from in Settings, applied instantly to the dock icon
|
|
132
132
|
- **Fully local** — embedded PostgreSQL (PGLite), no network calls, no accounts, no telemetry
|
|
133
133
|
|
|
@@ -164,9 +164,9 @@ Hot Sheet automatically generates skill files for Claude Code (as well as Cursor
|
|
|
164
164
|
Hot Sheet can push events directly to a running Claude Code session via MCP channels. Enable it in Settings → Experimental:
|
|
165
165
|
|
|
166
166
|
- **Play button** — appears in the sidebar. Single-click sends the worklist to Claude on demand.
|
|
167
|
-
- **Auto mode** — double-click the play button to enable automatic mode.
|
|
167
|
+
- **Auto mode** — double-click the play button to enable automatic mode. Claude is triggered immediately, then continues monitoring for new Up Next items with debounce. Exponential backoff prevents runaway retries.
|
|
168
168
|
- **Auto-prioritize** — when no tickets are flagged as Up Next, Claude automatically evaluates open tickets and picks the most important ones to work on.
|
|
169
|
-
- **Custom commands** — create named buttons that send custom prompts to Claude **or run shell commands** directly. Toggle between "Claude Code" and "Shell" targets per command. Shell commands execute server-side with stdout/stderr captured to the commands log.
|
|
169
|
+
- **Custom commands** — create named buttons that send custom prompts to Claude **or run shell commands** directly. Organize into collapsible groups. Toggle between "Claude Code" and "Shell" targets per command. Shell commands execute server-side with stdout/stderr captured to the commands log.
|
|
170
170
|
- **Permission relay** — when Claude needs tool approval (Bash, Edit, etc.), a full-screen overlay shows the tool name and command preview with Allow/Deny/Dismiss buttons — no need to switch to the terminal.
|
|
171
171
|
- **Commands log** — a resizable bottom panel that records all communication: triggers, completions, permission requests, and shell command output. Filter by type, search, and copy entries. Shell commands show a stop button for running processes.
|
|
172
172
|
- **Status indicator** — shows "Claude working" / "Shell running" / idle in the footer.
|
|
@@ -256,7 +256,7 @@ Only one Hot Sheet instance can use a data directory at a time. If you accidenta
|
|
|
256
256
|
|
|
257
257
|
Download the latest release for your platform from [GitHub Releases](https://github.com/brianwestphal/hotsheet/releases).
|
|
258
258
|
|
|
259
|
-
On first launch,
|
|
259
|
+
On first launch, use **Open Folder** to point the app at your project directory. The app remembers your projects and reopens them on subsequent launches. Optionally install the CLI for terminal-based launching:
|
|
260
260
|
|
|
261
261
|
**macOS:**
|
|
262
262
|
```bash
|
|
@@ -340,6 +340,8 @@ All settings can also be changed from the settings panel UI.
|
|
|
340
340
|
| `Cmd+D` | Toggle Up Next |
|
|
341
341
|
| `Delete` / `Backspace` | Delete selected tickets |
|
|
342
342
|
| `Cmd+C` | Copy ticket info |
|
|
343
|
+
| `Cmd+X` | Cut tickets |
|
|
344
|
+
| `Cmd+V` | Paste tickets |
|
|
343
345
|
| `Cmd+A` | Select all |
|
|
344
346
|
| `Cmd+Z` | Undo |
|
|
345
347
|
| `Cmd+Shift+Z` | Redo |
|
|
@@ -381,14 +383,14 @@ npm install
|
|
|
381
383
|
npm run dev # Build client assets, then run via tsx
|
|
382
384
|
npm run build # Build to dist/cli.js
|
|
383
385
|
npm test # Unit tests with coverage (446 tests)
|
|
384
|
-
npm run test:e2e # E2E browser tests (
|
|
386
|
+
npm run test:e2e # E2E browser tests (64 tests)
|
|
385
387
|
npm run test:all # Merged coverage report (unit + E2E)
|
|
386
388
|
npm run lint # ESLint
|
|
387
389
|
npm run clean # Remove dist and caches
|
|
388
390
|
npm link # Symlink for global 'hotsheet' command
|
|
389
391
|
```
|
|
390
392
|
|
|
391
|
-
The project has comprehensive test coverage with 446 unit tests (vitest) and
|
|
393
|
+
The project has comprehensive test coverage with 446 unit tests (vitest) and 64 Playwright E2E browser tests, plus 12 smoke tests for production install verification.
|
|
392
394
|
|
|
393
395
|
---
|
|
394
396
|
|
package/dist/channel.js
CHANGED
|
@@ -33475,6 +33475,7 @@ var StdioServerTransport = class {
|
|
|
33475
33475
|
import { readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
33476
33476
|
import { createServer } from "http";
|
|
33477
33477
|
import { join } from "path";
|
|
33478
|
+
var CHANNEL_VERSION = 3;
|
|
33478
33479
|
var dataDir = ".hotsheet";
|
|
33479
33480
|
var args = process.argv.slice(2);
|
|
33480
33481
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -33498,7 +33499,8 @@ var mcp = new Server(
|
|
|
33498
33499
|
"When you receive a channel event from hotsheet-channel, follow the instructions in the event content.",
|
|
33499
33500
|
"Typically this means running /hotsheet to process the current Up Next work items.",
|
|
33500
33501
|
"Do not ask for confirmation \u2014 just execute the requested action.",
|
|
33501
|
-
"IMPORTANT: When you finish processing (or if there was nothing to process), you MUST run the curl command provided in the event to signal completion."
|
|
33502
|
+
"IMPORTANT: When you finish processing (or if there was nothing to process), you MUST run the curl command provided in the event to signal completion.",
|
|
33503
|
+
"IMPORTANT: Do NOT use the Hot Sheet API (curl commands) to read or list tickets. Always use /hotsheet to read the worklist. The API should only be used for updating ticket status and creating new tickets as documented in the worklist."
|
|
33502
33504
|
].join(" ")
|
|
33503
33505
|
}
|
|
33504
33506
|
);
|
|
@@ -33537,7 +33539,7 @@ var httpServer = createServer(async (req, res) => {
|
|
|
33537
33539
|
}
|
|
33538
33540
|
if (req.method === "GET" && req.url === "/health") {
|
|
33539
33541
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
33540
|
-
res.end(JSON.stringify({ ok: true }));
|
|
33542
|
+
res.end(JSON.stringify({ ok: true, version: CHANNEL_VERSION }));
|
|
33541
33543
|
return;
|
|
33542
33544
|
}
|
|
33543
33545
|
if (req.method === "GET" && req.url === "/permission") {
|
|
@@ -33611,6 +33613,12 @@ var httpServer = createServer(async (req, res) => {
|
|
|
33611
33613
|
}
|
|
33612
33614
|
return;
|
|
33613
33615
|
}
|
|
33616
|
+
if (req.method === "POST" && req.url === "/shutdown") {
|
|
33617
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
33618
|
+
res.end(JSON.stringify({ ok: true }));
|
|
33619
|
+
setTimeout(() => void cleanup(), 100);
|
|
33620
|
+
return;
|
|
33621
|
+
}
|
|
33614
33622
|
res.writeHead(404);
|
|
33615
33623
|
res.end("not found");
|
|
33616
33624
|
});
|
|
@@ -33675,3 +33683,6 @@ process.on("exit", () => {
|
|
|
33675
33683
|
} catch {
|
|
33676
33684
|
}
|
|
33677
33685
|
});
|
|
33686
|
+
export {
|
|
33687
|
+
CHANNEL_VERSION
|
|
33688
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -143,7 +143,6 @@ async function initSchema(db) {
|
|
|
143
143
|
INSERT INTO settings (key, value) VALUES ('detail_width', '360') ON CONFLICT DO NOTHING;
|
|
144
144
|
INSERT INTO settings (key, value) VALUES ('detail_height', '300') ON CONFLICT DO NOTHING;
|
|
145
145
|
INSERT INTO settings (key, value) VALUES ('trash_cleanup_days', '3') ON CONFLICT DO NOTHING;
|
|
146
|
-
INSERT INTO settings (key, value) VALUES ('completed_cleanup_days', '30') ON CONFLICT DO NOTHING;
|
|
147
146
|
INSERT INTO settings (key, value) VALUES ('verified_cleanup_days', '30') ON CONFLICT DO NOTHING;
|
|
148
147
|
`);
|
|
149
148
|
await db.exec(`
|
|
@@ -970,9 +969,6 @@ async function getTickets(filters = {}) {
|
|
|
970
969
|
case "status":
|
|
971
970
|
orderBy = STATUS_ORD;
|
|
972
971
|
break;
|
|
973
|
-
case "ticket_number":
|
|
974
|
-
orderBy = "id";
|
|
975
|
-
break;
|
|
976
972
|
case "created":
|
|
977
973
|
case void 0:
|
|
978
974
|
orderBy = "created_at";
|
|
@@ -1024,7 +1020,11 @@ async function batchDeleteTickets(ids) {
|
|
|
1024
1020
|
async function toggleUpNext(id) {
|
|
1025
1021
|
const db = await getDb();
|
|
1026
1022
|
const result = await db.query(
|
|
1027
|
-
`UPDATE tickets SET up_next = NOT up_next, updated_at = NOW()
|
|
1023
|
+
`UPDATE tickets SET up_next = NOT up_next, updated_at = NOW(),
|
|
1024
|
+
status = CASE WHEN NOT up_next AND status IN ('completed', 'verified') THEN 'not_started' ELSE status END,
|
|
1025
|
+
completed_at = CASE WHEN NOT up_next AND status IN ('completed', 'verified') THEN NULL ELSE completed_at END,
|
|
1026
|
+
verified_at = CASE WHEN NOT up_next AND status IN ('completed', 'verified') THEN NULL ELSE verified_at END
|
|
1027
|
+
WHERE id = $1 RETURNING *`,
|
|
1028
1028
|
[id]
|
|
1029
1029
|
);
|
|
1030
1030
|
return result.rows[0] ?? null;
|
|
@@ -1623,7 +1623,7 @@ function mainSkillBody(projectRoot2) {
|
|
|
1623
1623
|
].join("\n");
|
|
1624
1624
|
}
|
|
1625
1625
|
function ensureClaudePermissions(cwd) {
|
|
1626
|
-
if (skillPort < 4170 || skillPort >
|
|
1626
|
+
if (skillPort < 4170 || skillPort > 4199) return false;
|
|
1627
1627
|
const settingsPath2 = join5(cwd, ".claude", "settings.json");
|
|
1628
1628
|
let settings = {};
|
|
1629
1629
|
if (existsSync4(settingsPath2)) {
|
|
@@ -1802,9 +1802,10 @@ var init_skills = __esm({
|
|
|
1802
1802
|
skillCategories = DEFAULT_CATEGORIES;
|
|
1803
1803
|
HOTSHEET_ALLOW_PATTERNS = [
|
|
1804
1804
|
"Bash(curl * http://localhost:417*/api/*)",
|
|
1805
|
-
"Bash(curl * http://localhost:418*/api/*)"
|
|
1805
|
+
"Bash(curl * http://localhost:418*/api/*)",
|
|
1806
|
+
"Bash(curl * http://localhost:419*/api/*)"
|
|
1806
1807
|
];
|
|
1807
|
-
HOTSHEET_CURL_RE = /^Bash\(curl \* http:\/\/localhost:\d+\/api\/\*\)$|^Bash\(curl \* http:\/\/localhost:41[
|
|
1808
|
+
HOTSHEET_CURL_RE = /^Bash\(curl \* http:\/\/localhost:\d+\/api\/\*\)$|^Bash\(curl \* http:\/\/localhost:41[789]\*\/api\/\*\)$/;
|
|
1808
1809
|
pendingCreatedFlag = false;
|
|
1809
1810
|
}
|
|
1810
1811
|
});
|
|
@@ -1946,6 +1947,7 @@ async function buildWorkflowInstructions(port, secretHeader) {
|
|
|
1946
1947
|
sections.push('- The "notes" field is REQUIRED when completing a ticket. Describe the specific work done.');
|
|
1947
1948
|
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.");
|
|
1948
1949
|
sections.push('- Do NOT set tickets to "verified" \u2014 that status is reserved for human review.');
|
|
1950
|
+
sections.push("- Do NOT use the API to read or list tickets (e.g., GET /api/tickets). Always read this worklist file for current work items. The API is only for updating ticket status and creating new tickets.");
|
|
1949
1951
|
sections.push("");
|
|
1950
1952
|
sections.push("## Creating Tickets");
|
|
1951
1953
|
sections.push("");
|
|
@@ -2271,13 +2273,12 @@ async function seedDemoExtraProjects(scenario, primaryDataDir, port) {
|
|
|
2271
2273
|
const path = await import("path");
|
|
2272
2274
|
const { registerProject: registerProject2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
2273
2275
|
const { writeFileSettings: writeFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
|
|
2274
|
-
const {
|
|
2276
|
+
const { getDbForDir: getDbForDir2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
|
|
2275
2277
|
const baseDir = path.dirname(primaryDataDir);
|
|
2276
2278
|
for (const extra of EXTRA_PROJECTS) {
|
|
2277
|
-
const extraDataDir = path.join(baseDir,
|
|
2279
|
+
const extraDataDir = path.join(baseDir, `${path.basename(primaryDataDir)}-${extra.appName.toLowerCase().replace(/\s+/g, "-")}`);
|
|
2278
2280
|
fs.mkdirSync(extraDataDir, { recursive: true });
|
|
2279
|
-
|
|
2280
|
-
const db = await getDatabase();
|
|
2281
|
+
const db = await getDbForDir2(extraDataDir);
|
|
2281
2282
|
for (let i = 0; i < extra.tickets.length; i++) {
|
|
2282
2283
|
const t = extra.tickets[i];
|
|
2283
2284
|
const ticketNumber = `HS-${i + 1}`;
|
|
@@ -2293,10 +2294,8 @@ async function seedDemoExtraProjects(scenario, primaryDataDir, port) {
|
|
|
2293
2294
|
}
|
|
2294
2295
|
await db.query(`SELECT setval('ticket_seq', $1)`, [extra.tickets.length]);
|
|
2295
2296
|
writeFileSettings2(extraDataDir, { appName: extra.appName });
|
|
2296
|
-
setDir(primaryDataDir);
|
|
2297
2297
|
await registerProject2(extraDataDir, port);
|
|
2298
2298
|
}
|
|
2299
|
-
setDir(primaryDataDir);
|
|
2300
2299
|
}
|
|
2301
2300
|
var DEMO_SCENARIOS, noteId, SCENARIO_1, SCENARIO_2, SCENARIO_3, SCENARIO_4, SCENARIO_5, SCENARIO_6, SCENARIO_7, SCENARIO_8, SCENARIO_9, SCENARIO_DATA, SCENARIO_3_VIEWS, SCENARIO_9_COMMANDS, EXTRA_PROJECTS;
|
|
2302
2301
|
var init_demo = __esm({
|
|
@@ -3217,9 +3216,14 @@ var init_demo = __esm({
|
|
|
3217
3216
|
];
|
|
3218
3217
|
SCENARIO_9_COMMANDS = [
|
|
3219
3218
|
{ name: "Commit Changes", prompt: "Make a commit for the recently completed tickets.", icon: "git-commit-horizontal", color: "#6b7280" },
|
|
3220
|
-
{
|
|
3221
|
-
|
|
3222
|
-
|
|
3219
|
+
{ type: "group", name: "Testing", children: [
|
|
3220
|
+
{ name: "Run Tests", prompt: "Run the test suite and report any failures.", icon: "test-tubes", color: "#3b82f6" },
|
|
3221
|
+
{ name: "Code Review", prompt: "Review the recent changes for code quality and potential issues.", icon: "search-code", color: "#8b5cf6" }
|
|
3222
|
+
] },
|
|
3223
|
+
{ type: "group", name: "Deploy", children: [
|
|
3224
|
+
{ name: "Deploy Staging", prompt: "Deploy the current branch to the staging environment.", icon: "rocket", color: "#f97316" },
|
|
3225
|
+
{ name: "Deploy Production", prompt: "Deploy to production after staging verification.", icon: "rocket", color: "#ef4444", target: "shell" }
|
|
3226
|
+
] }
|
|
3223
3227
|
];
|
|
3224
3228
|
EXTRA_PROJECTS = [
|
|
3225
3229
|
{
|
|
@@ -3400,7 +3404,6 @@ function notifyPermission() {
|
|
|
3400
3404
|
permissionVersion++;
|
|
3401
3405
|
const waiters = permissionWaiters;
|
|
3402
3406
|
permissionWaiters = [];
|
|
3403
|
-
console.log(`[perm] notifyPermission v${permissionVersion}, waking ${waiters.length} waiters`);
|
|
3404
3407
|
for (const resolve9 of waiters) resolve9();
|
|
3405
3408
|
}
|
|
3406
3409
|
var changeVersion, pollWaiters, permissionVersion, permissionWaiters;
|
|
@@ -3418,15 +3421,18 @@ var init_notify = __esm({
|
|
|
3418
3421
|
// src/channel-config.ts
|
|
3419
3422
|
var channel_config_exports = {};
|
|
3420
3423
|
__export(channel_config_exports, {
|
|
3424
|
+
checkChannelVersion: () => checkChannelVersion,
|
|
3425
|
+
cleanupStaleChannel: () => cleanupStaleChannel,
|
|
3421
3426
|
getChannelPort: () => getChannelPort,
|
|
3422
3427
|
isChannelAlive: () => isChannelAlive,
|
|
3423
3428
|
registerChannel: () => registerChannel,
|
|
3424
3429
|
registerChannelForAll: () => registerChannelForAll,
|
|
3430
|
+
shutdownChannel: () => shutdownChannel,
|
|
3425
3431
|
triggerChannel: () => triggerChannel,
|
|
3426
3432
|
unregisterChannel: () => unregisterChannel,
|
|
3427
3433
|
unregisterChannelForAll: () => unregisterChannelForAll
|
|
3428
3434
|
});
|
|
3429
|
-
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3435
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
3430
3436
|
import { dirname as dirname2, join as join11, resolve as resolve5 } from "path";
|
|
3431
3437
|
import { fileURLToPath } from "url";
|
|
3432
3438
|
function getChannelServerPath() {
|
|
@@ -3496,6 +3502,41 @@ function getChannelPort(dataDir) {
|
|
|
3496
3502
|
return null;
|
|
3497
3503
|
}
|
|
3498
3504
|
}
|
|
3505
|
+
async function cleanupStaleChannel(dataDir) {
|
|
3506
|
+
const port = getChannelPort(dataDir);
|
|
3507
|
+
if (port === null) return;
|
|
3508
|
+
const alive = await isChannelAlive(dataDir);
|
|
3509
|
+
if (!alive) {
|
|
3510
|
+
try {
|
|
3511
|
+
unlinkSync(join11(dataDir, "channel-port"));
|
|
3512
|
+
} catch {
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
async function checkChannelVersion(dataDir) {
|
|
3517
|
+
const port = getChannelPort(dataDir);
|
|
3518
|
+
if (port === null) return null;
|
|
3519
|
+
try {
|
|
3520
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
3521
|
+
const data = await res.json();
|
|
3522
|
+
const running = data.version ?? 0;
|
|
3523
|
+
return { match: running === EXPECTED_CHANNEL_VERSION, running, expected: EXPECTED_CHANNEL_VERSION };
|
|
3524
|
+
} catch {
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
async function shutdownChannel(dataDir) {
|
|
3529
|
+
const port = getChannelPort(dataDir);
|
|
3530
|
+
if (port === null) return;
|
|
3531
|
+
try {
|
|
3532
|
+
await fetch(`http://127.0.0.1:${port}/shutdown`, { method: "POST" });
|
|
3533
|
+
} catch {
|
|
3534
|
+
}
|
|
3535
|
+
try {
|
|
3536
|
+
unlinkSync(join11(dataDir, "channel-port"));
|
|
3537
|
+
} catch {
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3499
3540
|
async function isChannelAlive(dataDir) {
|
|
3500
3541
|
const port = getChannelPort(dataDir);
|
|
3501
3542
|
if (port === null) return false;
|
|
@@ -3532,11 +3573,12 @@ curl -s -X POST http://localhost:${serverPort}/api/channel/done${secretHeader}`;
|
|
|
3532
3573
|
return false;
|
|
3533
3574
|
}
|
|
3534
3575
|
}
|
|
3535
|
-
var MCP_SERVER_KEY;
|
|
3576
|
+
var MCP_SERVER_KEY, EXPECTED_CHANNEL_VERSION;
|
|
3536
3577
|
var init_channel_config = __esm({
|
|
3537
3578
|
"src/channel-config.ts"() {
|
|
3538
3579
|
"use strict";
|
|
3539
3580
|
MCP_SERVER_KEY = "hotsheet-channel";
|
|
3581
|
+
EXPECTED_CHANNEL_VERSION = 3;
|
|
3540
3582
|
}
|
|
3541
3583
|
});
|
|
3542
3584
|
|
|
@@ -3592,23 +3634,32 @@ async function cleanupAttachments() {
|
|
|
3592
3634
|
const trashDays = parseInt(settings.trash_cleanup_days, 10) || 3;
|
|
3593
3635
|
const tickets = await getTicketsForCleanup(verifiedDays, trashDays);
|
|
3594
3636
|
if (tickets.length === 0) return;
|
|
3595
|
-
let
|
|
3637
|
+
let archived = 0;
|
|
3638
|
+
let deleted = 0;
|
|
3596
3639
|
for (const ticket of tickets) {
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3640
|
+
if (ticket.status === "verified") {
|
|
3641
|
+
await updateTicket(ticket.id, { status: "archive" });
|
|
3642
|
+
archived++;
|
|
3643
|
+
} else {
|
|
3644
|
+
const attachments = await getAttachments(ticket.id);
|
|
3645
|
+
for (const att of attachments) {
|
|
3646
|
+
try {
|
|
3647
|
+
rmSync3(att.stored_path, { force: true });
|
|
3648
|
+
} catch {
|
|
3649
|
+
}
|
|
3602
3650
|
}
|
|
3651
|
+
await hardDeleteTicket(ticket.id);
|
|
3652
|
+
deleted++;
|
|
3603
3653
|
}
|
|
3604
|
-
await hardDeleteTicket(ticket.id);
|
|
3605
|
-
cleaned++;
|
|
3606
3654
|
}
|
|
3607
|
-
if (
|
|
3608
|
-
|
|
3655
|
+
if (archived > 0 || deleted > 0) {
|
|
3656
|
+
const parts = [];
|
|
3657
|
+
if (archived > 0) parts.push(`archived ${archived} verified ticket(s)`);
|
|
3658
|
+
if (deleted > 0) parts.push(`deleted ${deleted} trashed ticket(s)`);
|
|
3659
|
+
console.log(` Cleanup: ${parts.join(", ")}.`);
|
|
3609
3660
|
}
|
|
3610
3661
|
} catch (err) {
|
|
3611
|
-
console.error("
|
|
3662
|
+
console.error("Cleanup failed:", err);
|
|
3612
3663
|
}
|
|
3613
3664
|
}
|
|
3614
3665
|
|
|
@@ -17572,7 +17623,7 @@ config(en_default());
|
|
|
17572
17623
|
// src/routes/validation.ts
|
|
17573
17624
|
var TicketPrioritySchema = external_exports.enum(["highest", "high", "default", "low", "lowest"]);
|
|
17574
17625
|
var TicketStatusSchema = external_exports.enum(["not_started", "started", "completed", "verified", "backlog", "archive", "deleted"]);
|
|
17575
|
-
var SortBySchema = external_exports.enum(["created", "priority", "category", "status"
|
|
17626
|
+
var SortBySchema = external_exports.enum(["created", "priority", "category", "status"]);
|
|
17576
17627
|
var SortDirSchema = external_exports.enum(["asc", "desc"]);
|
|
17577
17628
|
var CreateTicketSchema = external_exports.object({
|
|
17578
17629
|
title: external_exports.string().optional().default(""),
|
|
@@ -17662,6 +17713,12 @@ var UpdateCategoriesSchema = external_exports.array(CategoryDefSchema).min(1);
|
|
|
17662
17713
|
var PrintSchema = external_exports.object({
|
|
17663
17714
|
html: external_exports.string()
|
|
17664
17715
|
});
|
|
17716
|
+
var GlobalConfigSchema = external_exports.object({
|
|
17717
|
+
channelEnabled: external_exports.boolean().optional(),
|
|
17718
|
+
shareTotalSeconds: external_exports.number().optional(),
|
|
17719
|
+
shareLastPrompted: external_exports.string().optional(),
|
|
17720
|
+
shareAccepted: external_exports.boolean().optional()
|
|
17721
|
+
}).strict();
|
|
17665
17722
|
function parseBody(schema, data) {
|
|
17666
17723
|
const result = schema.safeParse(data);
|
|
17667
17724
|
if (!result.success) {
|
|
@@ -17689,7 +17746,7 @@ channelRoutes.get("/channel/claude-check", async (c) => {
|
|
|
17689
17746
|
}
|
|
17690
17747
|
});
|
|
17691
17748
|
channelRoutes.get("/channel/status", async (c) => {
|
|
17692
|
-
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
17749
|
+
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2, checkChannelVersion: checkChannelVersion2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
17693
17750
|
const { readGlobalConfig: readGlobalConfig2 } = await Promise.resolve().then(() => (init_global_config(), global_config_exports));
|
|
17694
17751
|
const dataDir = c.get("dataDir");
|
|
17695
17752
|
const globalConfig2 = readGlobalConfig2();
|
|
@@ -17705,7 +17762,12 @@ channelRoutes.get("/channel/status", async (c) => {
|
|
|
17705
17762
|
const projectSecret = c.get("projectSecret");
|
|
17706
17763
|
const done = channelDoneFlags.get(projectSecret) === true;
|
|
17707
17764
|
if (done) channelDoneFlags.delete(projectSecret);
|
|
17708
|
-
|
|
17765
|
+
let versionMismatch = false;
|
|
17766
|
+
if (alive) {
|
|
17767
|
+
const vCheck = await checkChannelVersion2(dataDir);
|
|
17768
|
+
if (vCheck !== null && !vCheck.match) versionMismatch = true;
|
|
17769
|
+
}
|
|
17770
|
+
return c.json({ enabled, alive, port, done, versionMismatch });
|
|
17709
17771
|
});
|
|
17710
17772
|
channelRoutes.post("/channel/trigger", async (c) => {
|
|
17711
17773
|
const { triggerChannel: triggerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
@@ -17830,26 +17892,29 @@ channelRoutes.post("/channel/enable", async (c) => {
|
|
|
17830
17892
|
return c.json({ ok: true });
|
|
17831
17893
|
});
|
|
17832
17894
|
channelRoutes.post("/channel/disable", async (c) => {
|
|
17833
|
-
const { unregisterChannel: unregisterChannel2, unregisterChannelForAll: unregisterChannelForAll2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
17895
|
+
const { unregisterChannel: unregisterChannel2, unregisterChannelForAll: unregisterChannelForAll2, shutdownChannel: shutdownChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
17834
17896
|
const { writeGlobalConfig: writeGlobalConfig2 } = await Promise.resolve().then(() => (init_global_config(), global_config_exports));
|
|
17835
17897
|
const dataDir = c.get("dataDir");
|
|
17836
17898
|
writeGlobalConfig2({ channelEnabled: false });
|
|
17837
17899
|
try {
|
|
17838
17900
|
const { getAllProjects: getAllProjects2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
17839
|
-
|
|
17901
|
+
const projects2 = getAllProjects2();
|
|
17902
|
+
unregisterChannelForAll2(projects2.map((p) => p.dataDir));
|
|
17903
|
+
for (const p of projects2) {
|
|
17904
|
+
await shutdownChannel2(p.dataDir);
|
|
17905
|
+
}
|
|
17840
17906
|
} catch {
|
|
17841
17907
|
unregisterChannel2(dataDir);
|
|
17908
|
+
await shutdownChannel2(dataDir);
|
|
17842
17909
|
}
|
|
17843
17910
|
notifyChange();
|
|
17844
17911
|
return c.json({ ok: true });
|
|
17845
17912
|
});
|
|
17846
17913
|
channelRoutes.post("/channel/notify", (c) => {
|
|
17847
|
-
console.log(`[notify] channel/notify received \u2014 waking poll + permission waiters`);
|
|
17848
17914
|
notifyChange();
|
|
17849
17915
|
return c.json({ ok: true });
|
|
17850
17916
|
});
|
|
17851
17917
|
channelRoutes.post("/channel/permission/notify", (c) => {
|
|
17852
|
-
console.log(`[perm] notify received, waking waiters at ${Date.now()}`);
|
|
17853
17918
|
notifyPermission();
|
|
17854
17919
|
return c.json({ ok: true });
|
|
17855
17920
|
});
|
|
@@ -17957,12 +18022,11 @@ dashboardRoutes.get("/global-config", async (c) => {
|
|
|
17957
18022
|
return c.json(readGlobalConfig2());
|
|
17958
18023
|
});
|
|
17959
18024
|
dashboardRoutes.patch("/global-config", async (c) => {
|
|
17960
|
-
const
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
}
|
|
18025
|
+
const raw = await c.req.json();
|
|
18026
|
+
const parsed = parseBody(GlobalConfigSchema, raw);
|
|
18027
|
+
if (!parsed.success) return c.json({ error: parsed.error }, 400);
|
|
17964
18028
|
const { writeGlobalConfig: writeGlobalConfig2 } = await Promise.resolve().then(() => (init_global_config(), global_config_exports));
|
|
17965
|
-
const merged = writeGlobalConfig2(
|
|
18029
|
+
const merged = writeGlobalConfig2(parsed.data);
|
|
17966
18030
|
return c.json(merged);
|
|
17967
18031
|
});
|
|
17968
18032
|
dashboardRoutes.post("/ensure-skills", (c) => {
|
|
@@ -18054,6 +18118,7 @@ settingsRoutes.patch("/settings", async (c) => {
|
|
|
18054
18118
|
for (const [key, value] of Object.entries(parsed.data)) {
|
|
18055
18119
|
await updateSetting(key, value);
|
|
18056
18120
|
}
|
|
18121
|
+
notifyChange();
|
|
18057
18122
|
return c.json({ ok: true });
|
|
18058
18123
|
});
|
|
18059
18124
|
settingsRoutes.get("/file-settings", async (c) => {
|
|
@@ -18754,6 +18819,11 @@ pageRoutes.get("/", (c) => {
|
|
|
18754
18819
|
/* @__PURE__ */ jsx("div", { className: "app-body", children: [
|
|
18755
18820
|
/* @__PURE__ */ jsx("nav", { className: "sidebar", children: [
|
|
18756
18821
|
/* @__PURE__ */ jsx("div", { className: "channel-disconnected-warning", id: "channel-disconnected", style: "display:none", children: "Claude not connected" }),
|
|
18822
|
+
/* @__PURE__ */ jsx("div", { className: "channel-version-warning", id: "channel-version-warning", style: "display:none", children: [
|
|
18823
|
+
"Channel outdated \u2014 run ",
|
|
18824
|
+
/* @__PURE__ */ jsx("code", { children: "/mcp" }),
|
|
18825
|
+
" in Claude Code to reconnect"
|
|
18826
|
+
] }),
|
|
18757
18827
|
/* @__PURE__ */ jsx("div", { className: "sidebar-channel-play", id: "channel-play-section", style: "display:none", children: /* @__PURE__ */ jsx("button", { className: "channel-play-btn", id: "channel-play-btn", title: "Run worklist (double-click for auto mode)", children: [
|
|
18758
18828
|
/* @__PURE__ */ jsx("span", { className: "channel-play-icon", id: "channel-play-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", stroke: "none", children: /* @__PURE__ */ jsx("polygon", { points: "6 3 20 12 6 21 6 3" }) }) }),
|
|
18759
18829
|
/* @__PURE__ */ jsx("span", { className: "channel-auto-icon", id: "channel-auto-icon", style: "display:none", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", stroke: "none", children: [
|
|
@@ -19110,11 +19180,11 @@ pageRoutes.get("/", (c) => {
|
|
|
19110
19180
|
/* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-ticket-prefix-hint", children: "Prefix for ticket numbers (e.g. HS-1). Alphanumeric, hyphens, underscores. Max 10 characters." })
|
|
19111
19181
|
] }),
|
|
19112
19182
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
19113
|
-
/* @__PURE__ */ jsx("label", { children: "Auto-
|
|
19183
|
+
/* @__PURE__ */ jsx("label", { children: "Auto-delete trash after (days)" }),
|
|
19114
19184
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-trash-days", min: "1", value: "3" })
|
|
19115
19185
|
] }),
|
|
19116
19186
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
19117
|
-
/* @__PURE__ */ jsx("label", { children: "Auto-
|
|
19187
|
+
/* @__PURE__ */ jsx("label", { children: "Auto-archive verified after (days)" }),
|
|
19118
19188
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
|
|
19119
19189
|
] }),
|
|
19120
19190
|
/* @__PURE__ */ jsx("div", { className: "settings-field settings-field-checkbox", children: [
|
|
@@ -19124,6 +19194,13 @@ pageRoutes.get("/", (c) => {
|
|
|
19124
19194
|
] }),
|
|
19125
19195
|
/* @__PURE__ */ jsx("span", { className: "settings-hint", children: "When no Up Next items exist, the AI will evaluate open tickets and choose what to work on next." })
|
|
19126
19196
|
] }),
|
|
19197
|
+
/* @__PURE__ */ jsx("div", { className: "settings-field settings-field-checkbox", children: [
|
|
19198
|
+
/* @__PURE__ */ jsx("label", { children: [
|
|
19199
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-hide-verified-column" }),
|
|
19200
|
+
" Hide Verified column in column view"
|
|
19201
|
+
] }),
|
|
19202
|
+
/* @__PURE__ */ jsx("span", { className: "settings-hint", children: "Hides the Verified column in column view. Verified tickets will appear in the Completed column instead." })
|
|
19203
|
+
] }),
|
|
19127
19204
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
19128
19205
|
/* @__PURE__ */ jsx("label", { children: "When Claude needs permission" }),
|
|
19129
19206
|
/* @__PURE__ */ jsx("select", { id: "settings-notify-permission", children: [
|
|
@@ -19866,9 +19943,12 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
|
19866
19943
|
addToProjectList(dataDir);
|
|
19867
19944
|
const previousProjects = readProjectList();
|
|
19868
19945
|
const absDataDir = resolve8(dataDir);
|
|
19869
|
-
const validProjects = [
|
|
19946
|
+
const validProjects = [];
|
|
19870
19947
|
for (const prevDir of previousProjects) {
|
|
19871
|
-
if (prevDir === absDataDir)
|
|
19948
|
+
if (prevDir === absDataDir) {
|
|
19949
|
+
validProjects.push(prevDir);
|
|
19950
|
+
continue;
|
|
19951
|
+
}
|
|
19872
19952
|
if (!existsSync15(prevDir)) continue;
|
|
19873
19953
|
try {
|
|
19874
19954
|
await registerProject(prevDir, actualPort);
|
|
@@ -19880,6 +19960,11 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
|
19880
19960
|
const { reorderProjectList: reorderProjectList2 } = await Promise.resolve().then(() => (init_project_list(), project_list_exports));
|
|
19881
19961
|
reorderProjectList2(validProjects);
|
|
19882
19962
|
}
|
|
19963
|
+
if (validProjects.length > 1) {
|
|
19964
|
+
const { getProjectByDataDir: getByDir, reorderProjects: reorder } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
19965
|
+
const secrets = validProjects.map((dir) => getByDir(dir)?.secret).filter((s) => s !== void 0);
|
|
19966
|
+
if (secrets.length > 1) reorder(secrets);
|
|
19967
|
+
}
|
|
19883
19968
|
if (validProjects.length > 1) {
|
|
19884
19969
|
const { notifyChange: notifyChange2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
|
|
19885
19970
|
notifyChange2();
|
|
@@ -19892,6 +19977,13 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
|
19892
19977
|
const legacy = settings.channel_enabled === "true";
|
|
19893
19978
|
writeGlobalConfig2({ channelEnabled: legacy });
|
|
19894
19979
|
}
|
|
19980
|
+
{
|
|
19981
|
+
const { cleanupStaleChannel: cleanupStaleChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
19982
|
+
const { getAllProjects: allProjects } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
19983
|
+
for (const p of allProjects()) {
|
|
19984
|
+
await cleanupStaleChannel2(p.dataDir);
|
|
19985
|
+
}
|
|
19986
|
+
}
|
|
19895
19987
|
{
|
|
19896
19988
|
const { getAllProjects: getAllProjects2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
19897
19989
|
const { ensureSkillsForDir: ensureSkillsForDir2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
|