hotsheet 0.6.4 → 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/README.md +4 -0
- package/dist/cli.js +310 -43
- package/dist/client/app.global.js +43 -43
- package/dist/client/styles.css +1 -1
- package/package.json +3 -2
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();
|
|
@@ -865,6 +886,10 @@ async function createTicket(title, defaults) {
|
|
|
865
886
|
cols.push("details");
|
|
866
887
|
vals.push(defaults.details);
|
|
867
888
|
}
|
|
889
|
+
if (defaults?.tags !== void 0 && defaults.tags !== "" && defaults.tags !== "[]") {
|
|
890
|
+
cols.push("tags");
|
|
891
|
+
vals.push(defaults.tags);
|
|
892
|
+
}
|
|
868
893
|
const placeholders = vals.map((_, i) => `$${i + 1}`).join(", ");
|
|
869
894
|
const result = await db2.query(
|
|
870
895
|
`INSERT INTO tickets (${cols.join(", ")}) VALUES (${placeholders}) RETURNING *`,
|
|
@@ -963,7 +988,7 @@ async function getTickets(filters = {}) {
|
|
|
963
988
|
paramIdx++;
|
|
964
989
|
}
|
|
965
990
|
if (filters.search !== void 0 && filters.search !== "") {
|
|
966
|
-
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})`);
|
|
967
992
|
values.push(`%${filters.search}%`);
|
|
968
993
|
paramIdx++;
|
|
969
994
|
}
|
|
@@ -1098,7 +1123,7 @@ function ordinalValue(field, value) {
|
|
|
1098
1123
|
if (field === "status") return STATUS_RANK[value] ?? null;
|
|
1099
1124
|
return null;
|
|
1100
1125
|
}
|
|
1101
|
-
async function queryTickets(logic, conditions, sortBy, sortDir) {
|
|
1126
|
+
async function queryTickets(logic, conditions, sortBy, sortDir, requiredTag) {
|
|
1102
1127
|
const db2 = await getDb();
|
|
1103
1128
|
const where = [];
|
|
1104
1129
|
const values = [];
|
|
@@ -1145,6 +1170,11 @@ async function queryTickets(logic, conditions, sortBy, sortDir) {
|
|
|
1145
1170
|
break;
|
|
1146
1171
|
}
|
|
1147
1172
|
}
|
|
1173
|
+
if (requiredTag) {
|
|
1174
|
+
where[0] += ` AND tags ILIKE $${paramIdx}`;
|
|
1175
|
+
values.push(`%${requiredTag}%`);
|
|
1176
|
+
paramIdx++;
|
|
1177
|
+
}
|
|
1148
1178
|
const joiner = logic === "any" ? " OR " : " AND ";
|
|
1149
1179
|
const userConditions = where.slice(1);
|
|
1150
1180
|
let whereClause = where[0];
|
|
@@ -1173,6 +1203,19 @@ async function queryTickets(logic, conditions, sortBy, sortDir) {
|
|
|
1173
1203
|
);
|
|
1174
1204
|
return result.rows;
|
|
1175
1205
|
}
|
|
1206
|
+
function normalizeTag(input) {
|
|
1207
|
+
return input.replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase();
|
|
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
|
+
}
|
|
1176
1219
|
async function getAllTags() {
|
|
1177
1220
|
const db2 = await getDb();
|
|
1178
1221
|
const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
|
|
@@ -1182,7 +1225,10 @@ async function getAllTags() {
|
|
|
1182
1225
|
const parsed = JSON.parse(row.tags);
|
|
1183
1226
|
if (Array.isArray(parsed)) {
|
|
1184
1227
|
for (const tag of parsed) {
|
|
1185
|
-
if (typeof tag === "string" && tag.trim())
|
|
1228
|
+
if (typeof tag === "string" && tag.trim()) {
|
|
1229
|
+
const norm = normalizeTag(tag);
|
|
1230
|
+
if (norm) tagSet.add(norm);
|
|
1231
|
+
}
|
|
1186
1232
|
}
|
|
1187
1233
|
}
|
|
1188
1234
|
} catch {
|
|
@@ -1301,6 +1347,7 @@ async function cleanupAttachments() {
|
|
|
1301
1347
|
|
|
1302
1348
|
// src/cli.ts
|
|
1303
1349
|
init_connection();
|
|
1350
|
+
init_file_settings();
|
|
1304
1351
|
|
|
1305
1352
|
// src/lock.ts
|
|
1306
1353
|
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -1360,7 +1407,8 @@ var DEMO_SCENARIOS = [
|
|
|
1360
1407
|
{ id: 5, label: "Batch operations \u2014 multi-select toolbar" },
|
|
1361
1408
|
{ id: 6, label: "Detail panel \u2014 bottom orientation with tags and notes" },
|
|
1362
1409
|
{ id: 7, label: "Column view \u2014 kanban board by status" },
|
|
1363
|
-
{ id: 8, label: "Dashboard \u2014 stats and charts" }
|
|
1410
|
+
{ id: 8, label: "Dashboard \u2014 stats and charts" },
|
|
1411
|
+
{ id: 9, label: "Claude Channel \u2014 AI integration with custom commands" }
|
|
1364
1412
|
];
|
|
1365
1413
|
function daysAgo(days) {
|
|
1366
1414
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -2144,6 +2192,104 @@ for (let i = 0; i < 30; i++) {
|
|
|
2144
2192
|
verified_ago: verified
|
|
2145
2193
|
});
|
|
2146
2194
|
}
|
|
2195
|
+
var SCENARIO_9 = [
|
|
2196
|
+
{
|
|
2197
|
+
title: "Fix race condition in WebSocket message ordering",
|
|
2198
|
+
details: "Messages arriving during reconnect can be delivered out of order. Need to add sequence numbers and a reorder buffer on the client side.\n\nReproduction: disconnect WiFi briefly during a burst of real-time updates, then reconnect \u2014 events appear in wrong order.",
|
|
2199
|
+
category: "bug",
|
|
2200
|
+
priority: "highest",
|
|
2201
|
+
status: "started",
|
|
2202
|
+
up_next: true,
|
|
2203
|
+
tags: ["websocket", "real-time"],
|
|
2204
|
+
notes: notesJson([
|
|
2205
|
+
{ text: "Investigating \u2014 the issue is in the reconnect handler. When the socket reconnects, buffered server-side messages are flushed immediately without checking the client sequence counter.", days_ago: 0.1 }
|
|
2206
|
+
]),
|
|
2207
|
+
days_ago: 3,
|
|
2208
|
+
updated_ago: 0.1
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
title: "Add rate limiting to public API endpoints",
|
|
2212
|
+
details: "Implement token bucket rate limiting for all /api/v2/ endpoints. 100 requests per minute per API key, with burst allowance of 20.",
|
|
2213
|
+
category: "feature",
|
|
2214
|
+
priority: "high",
|
|
2215
|
+
status: "not_started",
|
|
2216
|
+
up_next: true,
|
|
2217
|
+
tags: ["api", "security"],
|
|
2218
|
+
notes: "",
|
|
2219
|
+
days_ago: 2,
|
|
2220
|
+
updated_ago: 2
|
|
2221
|
+
},
|
|
2222
|
+
{
|
|
2223
|
+
title: "Migrate user preferences to new schema",
|
|
2224
|
+
details: "The preferences table needs to be migrated from the old key-value format to the new typed JSON column. Write a migration script that preserves existing user settings.",
|
|
2225
|
+
category: "task",
|
|
2226
|
+
priority: "default",
|
|
2227
|
+
status: "not_started",
|
|
2228
|
+
up_next: true,
|
|
2229
|
+
tags: ["database", "migration"],
|
|
2230
|
+
notes: "",
|
|
2231
|
+
days_ago: 4,
|
|
2232
|
+
updated_ago: 4
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
title: "Investigate slow query on orders dashboard",
|
|
2236
|
+
details: "The orders dashboard takes 8+ seconds to load for merchants with >10k orders. Need to profile the SQL and add appropriate indexes.",
|
|
2237
|
+
category: "investigation",
|
|
2238
|
+
priority: "high",
|
|
2239
|
+
status: "completed",
|
|
2240
|
+
up_next: false,
|
|
2241
|
+
tags: ["performance", "database"],
|
|
2242
|
+
notes: notesJson([
|
|
2243
|
+
{ text: "Root cause: missing composite index on (merchant_id, created_at). The query was doing a full table scan. Added index and query time dropped from 8.2s to 45ms.", days_ago: 1 }
|
|
2244
|
+
]),
|
|
2245
|
+
days_ago: 5,
|
|
2246
|
+
updated_ago: 1,
|
|
2247
|
+
completed_ago: 1
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
title: "Update error handling middleware to use structured logging",
|
|
2251
|
+
details: "Replace console.error calls with structured JSON logging using pino. Include request ID, user context, and stack traces.",
|
|
2252
|
+
category: "task",
|
|
2253
|
+
priority: "default",
|
|
2254
|
+
status: "completed",
|
|
2255
|
+
up_next: false,
|
|
2256
|
+
tags: ["observability", "logging"],
|
|
2257
|
+
notes: notesJson([
|
|
2258
|
+
{ text: "Replaced all console.error/warn calls with pino logger. Added request ID propagation via AsyncLocalStorage. Error responses now include a correlationId for support debugging.", days_ago: 2 }
|
|
2259
|
+
]),
|
|
2260
|
+
days_ago: 7,
|
|
2261
|
+
updated_ago: 2,
|
|
2262
|
+
completed_ago: 2
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
title: "Fix CORS headers missing on preflight for webhook endpoints",
|
|
2266
|
+
details: "Third-party integrations sending OPTIONS preflight requests to /webhooks/* get 405 Method Not Allowed.",
|
|
2267
|
+
category: "bug",
|
|
2268
|
+
priority: "default",
|
|
2269
|
+
status: "verified",
|
|
2270
|
+
up_next: false,
|
|
2271
|
+
tags: ["api", "webhooks"],
|
|
2272
|
+
notes: notesJson([
|
|
2273
|
+
{ text: "Added CORS preflight handler for webhook routes. Configured allowed origins from the integration settings table.", days_ago: 4 }
|
|
2274
|
+
]),
|
|
2275
|
+
days_ago: 9,
|
|
2276
|
+
updated_ago: 4,
|
|
2277
|
+
completed_ago: 5,
|
|
2278
|
+
verified_ago: 4
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
title: "Design new onboarding flow for team workspaces",
|
|
2282
|
+
details: "The current onboarding drops users into an empty workspace. Design a guided setup that creates sample data and walks through key features.",
|
|
2283
|
+
category: "feature",
|
|
2284
|
+
priority: "low",
|
|
2285
|
+
status: "not_started",
|
|
2286
|
+
up_next: false,
|
|
2287
|
+
tags: ["onboarding", "ux"],
|
|
2288
|
+
notes: "",
|
|
2289
|
+
days_ago: 10,
|
|
2290
|
+
updated_ago: 10
|
|
2291
|
+
}
|
|
2292
|
+
];
|
|
2147
2293
|
var SCENARIO_DATA = {
|
|
2148
2294
|
1: SCENARIO_1,
|
|
2149
2295
|
2: SCENARIO_2,
|
|
@@ -2152,7 +2298,8 @@ var SCENARIO_DATA = {
|
|
|
2152
2298
|
5: SCENARIO_5,
|
|
2153
2299
|
6: SCENARIO_6,
|
|
2154
2300
|
7: SCENARIO_7,
|
|
2155
|
-
8: SCENARIO_8
|
|
2301
|
+
8: SCENARIO_8,
|
|
2302
|
+
9: SCENARIO_9
|
|
2156
2303
|
};
|
|
2157
2304
|
var SCENARIO_3_VIEWS = [
|
|
2158
2305
|
{
|
|
@@ -2174,6 +2321,12 @@ var SCENARIO_3_VIEWS = [
|
|
|
2174
2321
|
]
|
|
2175
2322
|
}
|
|
2176
2323
|
];
|
|
2324
|
+
var SCENARIO_9_COMMANDS = [
|
|
2325
|
+
{ name: "Commit Changes", prompt: "Make a commit for the recently completed tickets.", icon: "git-commit-horizontal", color: "#6b7280" },
|
|
2326
|
+
{ name: "Run Tests", prompt: "Run the test suite and report any failures.", icon: "test-tubes", color: "#3b82f6" },
|
|
2327
|
+
{ name: "Code Review", prompt: "Review the recent changes for code quality and potential issues.", icon: "search-code", color: "#8b5cf6" },
|
|
2328
|
+
{ name: "Deploy Staging", prompt: "Deploy the current branch to the staging environment.", icon: "rocket", color: "#f97316" }
|
|
2329
|
+
];
|
|
2177
2330
|
async function seedDemoData(scenario) {
|
|
2178
2331
|
const db2 = await getDb();
|
|
2179
2332
|
const tickets = SCENARIO_DATA[scenario];
|
|
@@ -2210,12 +2363,20 @@ async function seedDemoData(scenario) {
|
|
|
2210
2363
|
await backfillSnapshots2();
|
|
2211
2364
|
await recordDailySnapshot2();
|
|
2212
2365
|
}
|
|
2366
|
+
if (scenario === 9) {
|
|
2367
|
+
await db2.query(`INSERT INTO settings (key, value) VALUES ('channel_enabled', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'`);
|
|
2368
|
+
await db2.query(
|
|
2369
|
+
`INSERT INTO settings (key, value) VALUES ('custom_commands', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,
|
|
2370
|
+
[JSON.stringify(SCENARIO_9_COMMANDS)]
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2213
2373
|
}
|
|
2214
2374
|
|
|
2215
2375
|
// src/cli.ts
|
|
2216
2376
|
init_gitignore();
|
|
2217
2377
|
|
|
2218
2378
|
// src/server.ts
|
|
2379
|
+
init_file_settings();
|
|
2219
2380
|
import { serve } from "@hono/node-server";
|
|
2220
2381
|
import { exec } from "child_process";
|
|
2221
2382
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
@@ -2229,9 +2390,10 @@ import { Hono } from "hono";
|
|
|
2229
2390
|
import { basename, extname, join as join9, relative as relative2 } from "path";
|
|
2230
2391
|
|
|
2231
2392
|
// src/skills.ts
|
|
2393
|
+
init_file_settings();
|
|
2232
2394
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2233
2395
|
import { join as join6, relative } from "path";
|
|
2234
|
-
var SKILL_VERSION =
|
|
2396
|
+
var SKILL_VERSION = 4;
|
|
2235
2397
|
var skillPort;
|
|
2236
2398
|
var skillDataDir;
|
|
2237
2399
|
var skillCategories = DEFAULT_CATEGORIES;
|
|
@@ -2270,7 +2432,10 @@ function updateFile(path, content) {
|
|
|
2270
2432
|
return true;
|
|
2271
2433
|
}
|
|
2272
2434
|
function ticketSkillBody(skill) {
|
|
2273
|
-
|
|
2435
|
+
const settings = readFileSettings(skillDataDir);
|
|
2436
|
+
const secret = settings.secret || "";
|
|
2437
|
+
const secretLine = secret ? ` -H "X-Hotsheet-Secret: ${secret}" \\` : "";
|
|
2438
|
+
const lines = [
|
|
2274
2439
|
`Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
|
|
2275
2440
|
"",
|
|
2276
2441
|
"**Parsing the input:**",
|
|
@@ -2280,16 +2445,25 @@ function ticketSkillBody(skill) {
|
|
|
2280
2445
|
"**Create the ticket** by running:",
|
|
2281
2446
|
"```bash",
|
|
2282
2447
|
`curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
|
|
2283
|
-
' -H "Content-Type: application/json" \\'
|
|
2448
|
+
' -H "Content-Type: application/json" \\'
|
|
2449
|
+
];
|
|
2450
|
+
if (secretLine) lines.push(secretLine);
|
|
2451
|
+
lines.push(
|
|
2284
2452
|
` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
|
|
2285
2453
|
"```",
|
|
2286
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
|
+
"",
|
|
2287
2457
|
"Report the created ticket number and title to the user."
|
|
2288
|
-
|
|
2458
|
+
);
|
|
2459
|
+
return lines.join("\n");
|
|
2289
2460
|
}
|
|
2290
2461
|
function mainSkillBody() {
|
|
2291
2462
|
const worklistRel = relative(process.cwd(), join6(skillDataDir, "worklist.md"));
|
|
2463
|
+
const settingsRel = relative(process.cwd(), join6(skillDataDir, "settings.json"));
|
|
2292
2464
|
return [
|
|
2465
|
+
`Base directory for this skill: ${join6(process.cwd(), ".claude", "skills", "hotsheet")}`,
|
|
2466
|
+
"",
|
|
2293
2467
|
`Read \`${worklistRel}\` and work through the tickets in priority order.`,
|
|
2294
2468
|
"",
|
|
2295
2469
|
"For each ticket:",
|
|
@@ -2297,7 +2471,9 @@ function mainSkillBody() {
|
|
|
2297
2471
|
"2. Implement the work described",
|
|
2298
2472
|
"3. When complete, mark it done via the Hot Sheet UI",
|
|
2299
2473
|
"",
|
|
2300
|
-
"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.`
|
|
2301
2477
|
].join("\n");
|
|
2302
2478
|
}
|
|
2303
2479
|
var HOTSHEET_ALLOW_PATTERNS = [
|
|
@@ -2478,6 +2654,7 @@ function consumeSkillsCreatedFlag() {
|
|
|
2478
2654
|
// src/sync/markdown.ts
|
|
2479
2655
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2480
2656
|
import { join as join7 } from "path";
|
|
2657
|
+
init_file_settings();
|
|
2481
2658
|
var dataDir;
|
|
2482
2659
|
var port;
|
|
2483
2660
|
var worklistTimeout = null;
|
|
@@ -2514,7 +2691,7 @@ function parseTicketNotes(raw) {
|
|
|
2514
2691
|
if (raw.trim()) return [{ text: raw, created_at: "" }];
|
|
2515
2692
|
return [];
|
|
2516
2693
|
}
|
|
2517
|
-
async function formatTicket(ticket) {
|
|
2694
|
+
async function formatTicket(ticket, autoContext) {
|
|
2518
2695
|
const attachments = await getAttachments(ticket.id);
|
|
2519
2696
|
const lines = [];
|
|
2520
2697
|
lines.push(`TICKET ${ticket.ticket_number}:`);
|
|
@@ -2523,15 +2700,24 @@ async function formatTicket(ticket) {
|
|
|
2523
2700
|
lines.push(`- Priority: ${ticket.priority}`);
|
|
2524
2701
|
lines.push(`- Status: ${ticket.status.replace("_", " ")}`);
|
|
2525
2702
|
lines.push(`- Title: ${ticket.title}`);
|
|
2703
|
+
let ticketTags = [];
|
|
2526
2704
|
try {
|
|
2527
2705
|
const tags = JSON.parse(ticket.tags);
|
|
2528
2706
|
if (Array.isArray(tags) && tags.length > 0) {
|
|
2529
|
-
|
|
2707
|
+
ticketTags = tags;
|
|
2708
|
+
const display = tags.map((t) => t.replace(/\b\w/g, (c) => c.toUpperCase()));
|
|
2709
|
+
lines.push(`- Tags: ${display.join(", ")}`);
|
|
2530
2710
|
}
|
|
2531
2711
|
} catch {
|
|
2532
2712
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
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");
|
|
2535
2721
|
lines.push(`- Details: ${detailLines[0]}`);
|
|
2536
2722
|
for (let i = 1; i < detailLines.length; i++) {
|
|
2537
2723
|
lines.push(` ${detailLines[i]}`);
|
|
@@ -2553,6 +2739,17 @@ async function formatTicket(ticket) {
|
|
|
2553
2739
|
}
|
|
2554
2740
|
return lines.join("\n");
|
|
2555
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
|
+
}
|
|
2556
2753
|
async function formatCategoryDescriptions(usedCategories) {
|
|
2557
2754
|
const allCategories = await getCategories();
|
|
2558
2755
|
const descMap = Object.fromEntries(allCategories.map((c) => [c.id, c.description]));
|
|
@@ -2573,18 +2770,21 @@ async function syncWorklist() {
|
|
|
2573
2770
|
sections.push("");
|
|
2574
2771
|
sections.push("## Workflow");
|
|
2575
2772
|
sections.push("");
|
|
2773
|
+
const settings = readFileSettings(dataDir);
|
|
2774
|
+
const secret = settings.secret || "";
|
|
2775
|
+
const secretHeader = secret ? ` -H "X-Hotsheet-Secret: ${secret}"` : "";
|
|
2576
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.`);
|
|
2577
2777
|
sections.push("");
|
|
2578
2778
|
sections.push('- **BEFORE starting work on a ticket**, set its status to "started":');
|
|
2579
|
-
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"}'\``);
|
|
2580
2780
|
sections.push("");
|
|
2581
2781
|
sections.push('- **AFTER completing work on a ticket**, set its status to "completed" and **include notes** describing what was done:');
|
|
2582
|
-
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"}'\``);
|
|
2583
2783
|
sections.push("");
|
|
2584
2784
|
sections.push("**IMPORTANT:**");
|
|
2585
2785
|
sections.push('- Update status for EVERY ticket \u2014 "started" when you begin, "completed" when you finish.');
|
|
2586
2786
|
sections.push('- The "notes" field is REQUIRED when completing a ticket. Describe the specific work done.');
|
|
2587
|
-
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.");
|
|
2588
2788
|
sections.push('- Do NOT set tickets to "verified" \u2014 that status is reserved for human review.');
|
|
2589
2789
|
sections.push("");
|
|
2590
2790
|
sections.push("## Creating Tickets");
|
|
@@ -2598,7 +2798,7 @@ async function syncWorklist() {
|
|
|
2598
2798
|
sections.push("To create a ticket:");
|
|
2599
2799
|
const allCats = await getCategories();
|
|
2600
2800
|
const catIds = allCats.map((c) => c.id).join("|");
|
|
2601
|
-
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}}'\``);
|
|
2602
2802
|
sections.push("");
|
|
2603
2803
|
sections.push('You can also include `"details"` in the defaults object for longer descriptions.');
|
|
2604
2804
|
sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
|
|
@@ -2606,11 +2806,12 @@ async function syncWorklist() {
|
|
|
2606
2806
|
if (tickets.length === 0) {
|
|
2607
2807
|
sections.push("No items in the Up Next list.");
|
|
2608
2808
|
} else {
|
|
2809
|
+
const autoContext = await loadAutoContext();
|
|
2609
2810
|
for (const ticket of tickets) {
|
|
2610
2811
|
categories.add(ticket.category);
|
|
2611
2812
|
sections.push("---");
|
|
2612
2813
|
sections.push("");
|
|
2613
|
-
const formatted = await formatTicket(ticket);
|
|
2814
|
+
const formatted = await formatTicket(ticket, autoContext);
|
|
2614
2815
|
sections.push(formatted);
|
|
2615
2816
|
sections.push("");
|
|
2616
2817
|
}
|
|
@@ -2635,12 +2836,13 @@ async function syncOpenTickets() {
|
|
|
2635
2836
|
sections.push("");
|
|
2636
2837
|
const started = tickets.filter((t) => t.status === "started");
|
|
2637
2838
|
const notStarted = tickets.filter((t) => t.status === "not_started");
|
|
2839
|
+
const autoContext = await loadAutoContext();
|
|
2638
2840
|
if (started.length > 0) {
|
|
2639
2841
|
sections.push(`## Started (${started.length})`);
|
|
2640
2842
|
sections.push("");
|
|
2641
2843
|
for (const ticket of started) {
|
|
2642
2844
|
categories.add(ticket.category);
|
|
2643
|
-
const formatted = await formatTicket(ticket);
|
|
2845
|
+
const formatted = await formatTicket(ticket, autoContext);
|
|
2644
2846
|
sections.push(formatted);
|
|
2645
2847
|
sections.push("");
|
|
2646
2848
|
}
|
|
@@ -2650,7 +2852,7 @@ async function syncOpenTickets() {
|
|
|
2650
2852
|
sections.push("");
|
|
2651
2853
|
for (const ticket of notStarted) {
|
|
2652
2854
|
categories.add(ticket.category);
|
|
2653
|
-
const formatted = await formatTicket(ticket);
|
|
2855
|
+
const formatted = await formatTicket(ticket, autoContext);
|
|
2654
2856
|
sections.push(formatted);
|
|
2655
2857
|
sections.push("");
|
|
2656
2858
|
}
|
|
@@ -2677,8 +2879,8 @@ function notifyChange() {
|
|
|
2677
2879
|
changeVersion++;
|
|
2678
2880
|
const waiters = pollWaiters;
|
|
2679
2881
|
pollWaiters = [];
|
|
2680
|
-
for (const
|
|
2681
|
-
|
|
2882
|
+
for (const resolve4 of waiters) {
|
|
2883
|
+
resolve4(changeVersion);
|
|
2682
2884
|
}
|
|
2683
2885
|
}
|
|
2684
2886
|
apiRoutes.get("/poll", async (c) => {
|
|
@@ -2687,11 +2889,11 @@ apiRoutes.get("/poll", async (c) => {
|
|
|
2687
2889
|
return c.json({ version: changeVersion });
|
|
2688
2890
|
}
|
|
2689
2891
|
const version = await Promise.race([
|
|
2690
|
-
new Promise((
|
|
2691
|
-
pollWaiters.push(
|
|
2892
|
+
new Promise((resolve4) => {
|
|
2893
|
+
pollWaiters.push(resolve4);
|
|
2692
2894
|
}),
|
|
2693
|
-
new Promise((
|
|
2694
|
-
setTimeout(() =>
|
|
2895
|
+
new Promise((resolve4) => {
|
|
2896
|
+
setTimeout(() => resolve4(changeVersion), 3e4);
|
|
2695
2897
|
})
|
|
2696
2898
|
]);
|
|
2697
2899
|
return c.json({ version });
|
|
@@ -2717,7 +2919,24 @@ apiRoutes.get("/tickets", async (c) => {
|
|
|
2717
2919
|
});
|
|
2718
2920
|
apiRoutes.post("/tickets", async (c) => {
|
|
2719
2921
|
const body = await c.req.json();
|
|
2720
|
-
|
|
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);
|
|
2721
2940
|
scheduleAllSync();
|
|
2722
2941
|
notifyChange();
|
|
2723
2942
|
return c.json(ticket, 201);
|
|
@@ -2939,7 +3158,7 @@ apiRoutes.get("/attachments/file/*", async (c) => {
|
|
|
2939
3158
|
});
|
|
2940
3159
|
apiRoutes.post("/tickets/query", async (c) => {
|
|
2941
3160
|
const body = await c.req.json();
|
|
2942
|
-
const tickets = await queryTickets(body.logic, body.conditions, body.sort_by, body.sort_dir);
|
|
3161
|
+
const tickets = await queryTickets(body.logic, body.conditions, body.sort_by, body.sort_dir, body.required_tag);
|
|
2943
3162
|
return c.json(tickets);
|
|
2944
3163
|
});
|
|
2945
3164
|
apiRoutes.get("/tags", async (c) => {
|
|
@@ -3708,6 +3927,16 @@ pageRoutes.get("/", (c) => {
|
|
|
3708
3927
|
] }),
|
|
3709
3928
|
/* @__PURE__ */ jsx("span", { children: "Backups" })
|
|
3710
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
|
+
] }),
|
|
3711
3940
|
/* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "experimental", id: "settings-tab-experimental", style: "display:none", children: [
|
|
3712
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: [
|
|
3713
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" }),
|
|
@@ -3739,6 +3968,22 @@ pageRoutes.get("/", (c) => {
|
|
|
3739
3968
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3740
3969
|
/* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
|
|
3741
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
|
+
] })
|
|
3742
3987
|
] })
|
|
3743
3988
|
] }),
|
|
3744
3989
|
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
|
|
@@ -3761,6 +4006,14 @@ pageRoutes.get("/", (c) => {
|
|
|
3761
4006
|
] }),
|
|
3762
4007
|
/* @__PURE__ */ jsx("div", { id: "backup-list", className: "backup-list", children: "Loading backups..." })
|
|
3763
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
|
+
] }),
|
|
3764
4017
|
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "experimental", id: "settings-experimental-panel", style: "display:none", children: [
|
|
3765
4018
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3766
4019
|
/* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
|
|
@@ -3809,10 +4062,10 @@ pageRoutes.get("/", (c) => {
|
|
|
3809
4062
|
|
|
3810
4063
|
// src/server.ts
|
|
3811
4064
|
function tryServe(fetch2, port2) {
|
|
3812
|
-
return new Promise((
|
|
4065
|
+
return new Promise((resolve4, reject) => {
|
|
3813
4066
|
const server = serve({ fetch: fetch2, port: port2 });
|
|
3814
4067
|
server.on("listening", () => {
|
|
3815
|
-
|
|
4068
|
+
resolve4(port2);
|
|
3816
4069
|
});
|
|
3817
4070
|
server.on("error", (err) => {
|
|
3818
4071
|
reject(err);
|
|
@@ -3844,6 +4097,19 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3844
4097
|
const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
|
|
3845
4098
|
return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
|
|
3846
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
|
+
});
|
|
3847
4113
|
app.route("/api", apiRoutes);
|
|
3848
4114
|
app.route("/api/backups", backupRoutes);
|
|
3849
4115
|
app.route("/", pageRoutes);
|
|
@@ -3920,10 +4186,10 @@ function isFirstUseToday() {
|
|
|
3920
4186
|
return last !== today;
|
|
3921
4187
|
}
|
|
3922
4188
|
function fetchLatestVersion() {
|
|
3923
|
-
return new Promise((
|
|
4189
|
+
return new Promise((resolve4) => {
|
|
3924
4190
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
3925
4191
|
if (res.statusCode !== 200) {
|
|
3926
|
-
|
|
4192
|
+
resolve4(null);
|
|
3927
4193
|
return;
|
|
3928
4194
|
}
|
|
3929
4195
|
let data = "";
|
|
@@ -3932,18 +4198,18 @@ function fetchLatestVersion() {
|
|
|
3932
4198
|
});
|
|
3933
4199
|
res.on("end", () => {
|
|
3934
4200
|
try {
|
|
3935
|
-
|
|
4201
|
+
resolve4(JSON.parse(data).version);
|
|
3936
4202
|
} catch {
|
|
3937
|
-
|
|
4203
|
+
resolve4(null);
|
|
3938
4204
|
}
|
|
3939
4205
|
});
|
|
3940
4206
|
});
|
|
3941
4207
|
req.on("error", () => {
|
|
3942
|
-
|
|
4208
|
+
resolve4(null);
|
|
3943
4209
|
});
|
|
3944
4210
|
req.on("timeout", () => {
|
|
3945
4211
|
req.destroy();
|
|
3946
|
-
|
|
4212
|
+
resolve4(null);
|
|
3947
4213
|
});
|
|
3948
4214
|
});
|
|
3949
4215
|
}
|
|
@@ -4045,7 +4311,7 @@ function parseArgs(argv) {
|
|
|
4045
4311
|
}
|
|
4046
4312
|
break;
|
|
4047
4313
|
case "--data-dir":
|
|
4048
|
-
dataDir2 =
|
|
4314
|
+
dataDir2 = resolve3(args[++i]);
|
|
4049
4315
|
break;
|
|
4050
4316
|
case "--check-for-updates":
|
|
4051
4317
|
forceUpdateCheck = true;
|
|
@@ -4103,6 +4369,7 @@ async function main() {
|
|
|
4103
4369
|
}
|
|
4104
4370
|
console.log(` Data directory: ${dataDir2}`);
|
|
4105
4371
|
const actualPort = await startServer(port2, dataDir2, { noOpen, strictPort });
|
|
4372
|
+
ensureSecret(dataDir2, actualPort);
|
|
4106
4373
|
initMarkdownSync(dataDir2, actualPort);
|
|
4107
4374
|
scheduleAllSync();
|
|
4108
4375
|
initSkills(actualPort, dataDir2);
|