@ulrichc1/sparn 1.0.0 → 1.1.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 +46 -10
- package/dist/cli/index.cjs +646 -15
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +640 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +882 -0
- package/dist/daemon/index.cjs.map +1 -0
- package/dist/daemon/index.d.cts +2 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +880 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/hooks/post-tool-result.cjs +270 -0
- package/dist/hooks/post-tool-result.cjs.map +1 -0
- package/dist/hooks/post-tool-result.d.cts +1 -0
- package/dist/hooks/post-tool-result.d.ts +1 -0
- package/dist/hooks/post-tool-result.js +269 -0
- package/dist/hooks/post-tool-result.js.map +1 -0
- package/dist/hooks/pre-prompt.cjs +287 -0
- package/dist/hooks/pre-prompt.cjs.map +1 -0
- package/dist/hooks/pre-prompt.d.cts +1 -0
- package/dist/hooks/pre-prompt.d.ts +1 -0
- package/dist/hooks/pre-prompt.js +286 -0
- package/dist/hooks/pre-prompt.js.map +1 -0
- package/dist/index.cjs +961 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +459 -20
- package/dist/index.d.ts +459 -20
- package/dist/index.js +956 -66
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -12,9 +12,13 @@ var __export = (target, all) => {
|
|
|
12
12
|
// node_modules/tsup/assets/esm_shims.js
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
var getFilename, getDirname, __dirname;
|
|
15
16
|
var init_esm_shims = __esm({
|
|
16
17
|
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
18
|
"use strict";
|
|
19
|
+
getFilename = () => fileURLToPath(import.meta.url);
|
|
20
|
+
getDirname = () => path.dirname(getFilename());
|
|
21
|
+
__dirname = /* @__PURE__ */ getDirname();
|
|
18
22
|
}
|
|
19
23
|
});
|
|
20
24
|
|
|
@@ -349,7 +353,17 @@ var init_config = __esm({
|
|
|
349
353
|
sounds: false,
|
|
350
354
|
verbose: false
|
|
351
355
|
},
|
|
352
|
-
autoConsolidate: null
|
|
356
|
+
autoConsolidate: null,
|
|
357
|
+
realtime: {
|
|
358
|
+
tokenBudget: 5e4,
|
|
359
|
+
autoOptimizeThreshold: 8e4,
|
|
360
|
+
watchPatterns: ["**/*.jsonl"],
|
|
361
|
+
pidFile: ".sparn/daemon.pid",
|
|
362
|
+
logFile: ".sparn/daemon.log",
|
|
363
|
+
debounceMs: 5e3,
|
|
364
|
+
incremental: true,
|
|
365
|
+
windowSize: 500
|
|
366
|
+
}
|
|
353
367
|
};
|
|
354
368
|
}
|
|
355
369
|
});
|
|
@@ -372,7 +386,7 @@ function getVersion() {
|
|
|
372
386
|
} catch {
|
|
373
387
|
const __filename2 = fileURLToPath2(import.meta.url);
|
|
374
388
|
const __dirname2 = dirname(__filename2);
|
|
375
|
-
const pkg = JSON.parse(readFileSync(join(__dirname2, "
|
|
389
|
+
const pkg = JSON.parse(readFileSync(join(__dirname2, "../../package.json"), "utf-8"));
|
|
376
390
|
return pkg.version;
|
|
377
391
|
}
|
|
378
392
|
}
|
|
@@ -1456,8 +1470,494 @@ var init_config2 = __esm({
|
|
|
1456
1470
|
validate: (v) => v === null || typeof v === "number" && v > 0,
|
|
1457
1471
|
errorMessage: "autoConsolidate must be a positive number (hours) or null",
|
|
1458
1472
|
parse: (v) => v === "null" ? null : Number.parseFloat(v)
|
|
1473
|
+
},
|
|
1474
|
+
"realtime.tokenBudget": {
|
|
1475
|
+
path: ["realtime", "tokenBudget"],
|
|
1476
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1477
|
+
errorMessage: "tokenBudget must be a positive number",
|
|
1478
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1479
|
+
},
|
|
1480
|
+
"realtime.autoOptimizeThreshold": {
|
|
1481
|
+
path: ["realtime", "autoOptimizeThreshold"],
|
|
1482
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1483
|
+
errorMessage: "autoOptimizeThreshold must be a positive number",
|
|
1484
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1485
|
+
},
|
|
1486
|
+
"realtime.watchPatterns": {
|
|
1487
|
+
path: ["realtime", "watchPatterns"],
|
|
1488
|
+
validate: (v) => Array.isArray(v) && v.every((p) => typeof p === "string"),
|
|
1489
|
+
errorMessage: "watchPatterns must be an array of strings",
|
|
1490
|
+
parse: (v) => v.split(",").map((p) => p.trim())
|
|
1491
|
+
},
|
|
1492
|
+
"realtime.pidFile": {
|
|
1493
|
+
path: ["realtime", "pidFile"],
|
|
1494
|
+
validate: (v) => typeof v === "string" && v.length > 0,
|
|
1495
|
+
errorMessage: "pidFile must be a non-empty string",
|
|
1496
|
+
parse: (v) => v
|
|
1497
|
+
},
|
|
1498
|
+
"realtime.logFile": {
|
|
1499
|
+
path: ["realtime", "logFile"],
|
|
1500
|
+
validate: (v) => typeof v === "string" && v.length > 0,
|
|
1501
|
+
errorMessage: "logFile must be a non-empty string",
|
|
1502
|
+
parse: (v) => v
|
|
1503
|
+
},
|
|
1504
|
+
"realtime.debounceMs": {
|
|
1505
|
+
path: ["realtime", "debounceMs"],
|
|
1506
|
+
validate: (v) => typeof v === "number" && v >= 0,
|
|
1507
|
+
errorMessage: "debounceMs must be a non-negative number",
|
|
1508
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1509
|
+
},
|
|
1510
|
+
"realtime.incremental": {
|
|
1511
|
+
path: ["realtime", "incremental"],
|
|
1512
|
+
validate: (v) => typeof v === "boolean",
|
|
1513
|
+
errorMessage: "incremental must be true or false",
|
|
1514
|
+
parse: (v) => v === "true"
|
|
1515
|
+
},
|
|
1516
|
+
"realtime.windowSize": {
|
|
1517
|
+
path: ["realtime", "windowSize"],
|
|
1518
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1519
|
+
errorMessage: "windowSize must be a positive number",
|
|
1520
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// src/core/metrics.ts
|
|
1527
|
+
function createMetricsCollector() {
|
|
1528
|
+
const optimizations = [];
|
|
1529
|
+
let daemonMetrics = {
|
|
1530
|
+
startTime: Date.now(),
|
|
1531
|
+
sessionsWatched: 0,
|
|
1532
|
+
totalOptimizations: 0,
|
|
1533
|
+
totalTokensSaved: 0,
|
|
1534
|
+
averageLatency: 0,
|
|
1535
|
+
memoryUsage: 0
|
|
1536
|
+
};
|
|
1537
|
+
let cacheHits = 0;
|
|
1538
|
+
let cacheMisses = 0;
|
|
1539
|
+
function recordOptimization(metric) {
|
|
1540
|
+
optimizations.push(metric);
|
|
1541
|
+
daemonMetrics.totalOptimizations++;
|
|
1542
|
+
daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
|
|
1543
|
+
if (metric.cacheHitRate > 0) {
|
|
1544
|
+
const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
|
|
1545
|
+
cacheHits += hits;
|
|
1546
|
+
cacheMisses += metric.entriesProcessed - hits;
|
|
1547
|
+
}
|
|
1548
|
+
daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
|
|
1549
|
+
if (optimizations.length > 1e3) {
|
|
1550
|
+
optimizations.shift();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function updateDaemon(metric) {
|
|
1554
|
+
daemonMetrics = {
|
|
1555
|
+
...daemonMetrics,
|
|
1556
|
+
...metric
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function calculatePercentile(values, percentile) {
|
|
1560
|
+
if (values.length === 0) return 0;
|
|
1561
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1562
|
+
const index = Math.ceil(percentile / 100 * sorted.length) - 1;
|
|
1563
|
+
return sorted[index] || 0;
|
|
1564
|
+
}
|
|
1565
|
+
function getSnapshot() {
|
|
1566
|
+
const totalRuns = optimizations.length;
|
|
1567
|
+
const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
|
|
1568
|
+
const totalTokensSaved = optimizations.reduce(
|
|
1569
|
+
(sum, m) => sum + (m.tokensBefore - m.tokensAfter),
|
|
1570
|
+
0
|
|
1571
|
+
);
|
|
1572
|
+
const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
|
|
1573
|
+
const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
|
|
1574
|
+
const durations = optimizations.map((m) => m.duration);
|
|
1575
|
+
const totalCacheQueries = cacheHits + cacheMisses;
|
|
1576
|
+
const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
|
|
1577
|
+
return {
|
|
1578
|
+
timestamp: Date.now(),
|
|
1579
|
+
optimization: {
|
|
1580
|
+
totalRuns,
|
|
1581
|
+
totalDuration,
|
|
1582
|
+
totalTokensSaved,
|
|
1583
|
+
averageReduction,
|
|
1584
|
+
p50Latency: calculatePercentile(durations, 50),
|
|
1585
|
+
p95Latency: calculatePercentile(durations, 95),
|
|
1586
|
+
p99Latency: calculatePercentile(durations, 99)
|
|
1587
|
+
},
|
|
1588
|
+
cache: {
|
|
1589
|
+
hitRate,
|
|
1590
|
+
totalHits: cacheHits,
|
|
1591
|
+
totalMisses: cacheMisses,
|
|
1592
|
+
size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
|
|
1593
|
+
},
|
|
1594
|
+
daemon: {
|
|
1595
|
+
uptime: Date.now() - daemonMetrics.startTime,
|
|
1596
|
+
sessionsWatched: daemonMetrics.sessionsWatched,
|
|
1597
|
+
memoryUsage: daemonMetrics.memoryUsage
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function exportMetrics() {
|
|
1602
|
+
return JSON.stringify(getSnapshot(), null, 2);
|
|
1603
|
+
}
|
|
1604
|
+
function reset() {
|
|
1605
|
+
optimizations.length = 0;
|
|
1606
|
+
cacheHits = 0;
|
|
1607
|
+
cacheMisses = 0;
|
|
1608
|
+
daemonMetrics = {
|
|
1609
|
+
startTime: Date.now(),
|
|
1610
|
+
sessionsWatched: 0,
|
|
1611
|
+
totalOptimizations: 0,
|
|
1612
|
+
totalTokensSaved: 0,
|
|
1613
|
+
averageLatency: 0,
|
|
1614
|
+
memoryUsage: 0
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
recordOptimization,
|
|
1619
|
+
updateDaemon,
|
|
1620
|
+
getSnapshot,
|
|
1621
|
+
export: exportMetrics,
|
|
1622
|
+
reset
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function getMetrics() {
|
|
1626
|
+
if (!globalMetrics) {
|
|
1627
|
+
globalMetrics = createMetricsCollector();
|
|
1628
|
+
}
|
|
1629
|
+
return globalMetrics;
|
|
1630
|
+
}
|
|
1631
|
+
var globalMetrics;
|
|
1632
|
+
var init_metrics = __esm({
|
|
1633
|
+
"src/core/metrics.ts"() {
|
|
1634
|
+
"use strict";
|
|
1635
|
+
init_esm_shims();
|
|
1636
|
+
globalMetrics = null;
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// src/daemon/daemon-process.ts
|
|
1641
|
+
var daemon_process_exports = {};
|
|
1642
|
+
__export(daemon_process_exports, {
|
|
1643
|
+
createDaemonCommand: () => createDaemonCommand
|
|
1644
|
+
});
|
|
1645
|
+
import { fork } from "child_process";
|
|
1646
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1647
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1648
|
+
function createDaemonCommand() {
|
|
1649
|
+
function isDaemonRunning(pidFile) {
|
|
1650
|
+
if (!existsSync2(pidFile)) {
|
|
1651
|
+
return { running: false };
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
const pidStr = readFileSync3(pidFile, "utf-8").trim();
|
|
1655
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
1656
|
+
if (Number.isNaN(pid)) {
|
|
1657
|
+
return { running: false };
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
process.kill(pid, 0);
|
|
1661
|
+
return { running: true, pid };
|
|
1662
|
+
} catch {
|
|
1663
|
+
unlinkSync(pidFile);
|
|
1664
|
+
return { running: false };
|
|
1665
|
+
}
|
|
1666
|
+
} catch {
|
|
1667
|
+
return { running: false };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function writePidFile(pidFile, pid) {
|
|
1671
|
+
const dir = dirname2(pidFile);
|
|
1672
|
+
if (!existsSync2(dir)) {
|
|
1673
|
+
mkdirSync(dir, { recursive: true });
|
|
1674
|
+
}
|
|
1675
|
+
writeFileSync2(pidFile, String(pid), "utf-8");
|
|
1676
|
+
}
|
|
1677
|
+
function removePidFile(pidFile) {
|
|
1678
|
+
if (existsSync2(pidFile)) {
|
|
1679
|
+
unlinkSync(pidFile);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function start(config) {
|
|
1683
|
+
const { pidFile, logFile } = config.realtime;
|
|
1684
|
+
const status2 = isDaemonRunning(pidFile);
|
|
1685
|
+
if (status2.running) {
|
|
1686
|
+
return {
|
|
1687
|
+
success: false,
|
|
1688
|
+
pid: status2.pid,
|
|
1689
|
+
message: `Daemon already running (PID ${status2.pid})`,
|
|
1690
|
+
error: "Already running"
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const daemonPath = join2(__dirname, "index.js");
|
|
1695
|
+
const child = fork(daemonPath, [], {
|
|
1696
|
+
detached: true,
|
|
1697
|
+
stdio: "ignore",
|
|
1698
|
+
env: {
|
|
1699
|
+
...process.env,
|
|
1700
|
+
SPARN_CONFIG: JSON.stringify(config),
|
|
1701
|
+
SPARN_PID_FILE: pidFile,
|
|
1702
|
+
SPARN_LOG_FILE: logFile
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
child.unref();
|
|
1706
|
+
if (child.pid) {
|
|
1707
|
+
writePidFile(pidFile, child.pid);
|
|
1708
|
+
return {
|
|
1709
|
+
success: true,
|
|
1710
|
+
pid: child.pid,
|
|
1711
|
+
message: `Daemon started (PID ${child.pid})`
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
success: false,
|
|
1716
|
+
message: "Failed to start daemon (no PID)",
|
|
1717
|
+
error: "No PID"
|
|
1718
|
+
};
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
return {
|
|
1721
|
+
success: false,
|
|
1722
|
+
message: "Failed to start daemon",
|
|
1723
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async function stop(config) {
|
|
1728
|
+
const { pidFile } = config.realtime;
|
|
1729
|
+
const status2 = isDaemonRunning(pidFile);
|
|
1730
|
+
if (!status2.running || !status2.pid) {
|
|
1731
|
+
return {
|
|
1732
|
+
success: true,
|
|
1733
|
+
message: "Daemon not running"
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
try {
|
|
1737
|
+
process.kill(status2.pid, "SIGTERM");
|
|
1738
|
+
const maxWait = 5e3;
|
|
1739
|
+
const interval = 100;
|
|
1740
|
+
let waited = 0;
|
|
1741
|
+
while (waited < maxWait) {
|
|
1742
|
+
try {
|
|
1743
|
+
process.kill(status2.pid, 0);
|
|
1744
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
1745
|
+
waited += interval;
|
|
1746
|
+
} catch {
|
|
1747
|
+
removePidFile(pidFile);
|
|
1748
|
+
return {
|
|
1749
|
+
success: true,
|
|
1750
|
+
message: `Daemon stopped (PID ${status2.pid})`
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
try {
|
|
1755
|
+
process.kill(status2.pid, "SIGKILL");
|
|
1756
|
+
removePidFile(pidFile);
|
|
1757
|
+
return {
|
|
1758
|
+
success: true,
|
|
1759
|
+
message: `Daemon force killed (PID ${status2.pid})`
|
|
1760
|
+
};
|
|
1761
|
+
} catch {
|
|
1762
|
+
removePidFile(pidFile);
|
|
1763
|
+
return {
|
|
1764
|
+
success: true,
|
|
1765
|
+
message: `Daemon stopped (PID ${status2.pid})`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
return {
|
|
1770
|
+
success: false,
|
|
1771
|
+
message: "Failed to stop daemon",
|
|
1772
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function status(config) {
|
|
1777
|
+
const { pidFile } = config.realtime;
|
|
1778
|
+
const daemonStatus = isDaemonRunning(pidFile);
|
|
1779
|
+
if (!daemonStatus.running || !daemonStatus.pid) {
|
|
1780
|
+
return {
|
|
1781
|
+
running: false,
|
|
1782
|
+
message: "Daemon not running"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const metrics = getMetrics().getSnapshot();
|
|
1786
|
+
return {
|
|
1787
|
+
running: true,
|
|
1788
|
+
pid: daemonStatus.pid,
|
|
1789
|
+
uptime: metrics.daemon.uptime,
|
|
1790
|
+
sessionsWatched: metrics.daemon.sessionsWatched,
|
|
1791
|
+
tokensSaved: metrics.optimization.totalTokensSaved,
|
|
1792
|
+
message: `Daemon running (PID ${daemonStatus.pid})`
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
start,
|
|
1797
|
+
stop,
|
|
1798
|
+
status
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
var init_daemon_process = __esm({
|
|
1802
|
+
"src/daemon/daemon-process.ts"() {
|
|
1803
|
+
"use strict";
|
|
1804
|
+
init_esm_shims();
|
|
1805
|
+
init_metrics();
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
// src/cli/commands/hooks.ts
|
|
1810
|
+
var hooks_exports = {};
|
|
1811
|
+
__export(hooks_exports, {
|
|
1812
|
+
hooksCommand: () => hooksCommand
|
|
1813
|
+
});
|
|
1814
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1815
|
+
import { homedir } from "os";
|
|
1816
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1817
|
+
async function hooksCommand(options) {
|
|
1818
|
+
const { subcommand, global } = options;
|
|
1819
|
+
const settingsPath = global ? join3(homedir(), ".claude", "settings.json") : join3(process.cwd(), ".claude", "settings.json");
|
|
1820
|
+
const hooksDir = join3(dirname3(dirname3(dirname3(__dirname))), "dist", "hooks");
|
|
1821
|
+
const prePromptPath = join3(hooksDir, "pre-prompt.js");
|
|
1822
|
+
const postToolResultPath = join3(hooksDir, "post-tool-result.js");
|
|
1823
|
+
switch (subcommand) {
|
|
1824
|
+
case "install":
|
|
1825
|
+
return await installHooks(settingsPath, prePromptPath, postToolResultPath, global);
|
|
1826
|
+
case "uninstall":
|
|
1827
|
+
return await uninstallHooks(settingsPath, global);
|
|
1828
|
+
case "status":
|
|
1829
|
+
return await hooksStatus(settingsPath, global);
|
|
1830
|
+
default:
|
|
1831
|
+
return {
|
|
1832
|
+
success: false,
|
|
1833
|
+
message: `Unknown subcommand: ${subcommand}`,
|
|
1834
|
+
error: "Invalid subcommand"
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
async function installHooks(settingsPath, prePromptPath, postToolResultPath, global) {
|
|
1839
|
+
try {
|
|
1840
|
+
if (!existsSync3(prePromptPath)) {
|
|
1841
|
+
return {
|
|
1842
|
+
success: false,
|
|
1843
|
+
message: `Hook script not found: ${prePromptPath}`,
|
|
1844
|
+
error: "Hook scripts not built. Run `npm run build` first."
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
if (!existsSync3(postToolResultPath)) {
|
|
1848
|
+
return {
|
|
1849
|
+
success: false,
|
|
1850
|
+
message: `Hook script not found: ${postToolResultPath}`,
|
|
1851
|
+
error: "Hook scripts not built. Run `npm run build` first."
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
let settings = {};
|
|
1855
|
+
if (existsSync3(settingsPath)) {
|
|
1856
|
+
const settingsJson = readFileSync4(settingsPath, "utf-8");
|
|
1857
|
+
settings = JSON.parse(settingsJson);
|
|
1858
|
+
} else {
|
|
1859
|
+
const claudeDir = dirname3(settingsPath);
|
|
1860
|
+
if (!existsSync3(claudeDir)) {
|
|
1861
|
+
mkdirSync2(claudeDir, { recursive: true });
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
settings["hooks"] = {
|
|
1865
|
+
...typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {},
|
|
1866
|
+
"pre-prompt": `node ${prePromptPath}`,
|
|
1867
|
+
"post-tool-result": `node ${postToolResultPath}`
|
|
1868
|
+
};
|
|
1869
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1870
|
+
return {
|
|
1871
|
+
success: true,
|
|
1872
|
+
message: global ? "Hooks installed globally (all projects)" : "Hooks installed for current project",
|
|
1873
|
+
installed: true,
|
|
1874
|
+
hookPaths: {
|
|
1875
|
+
prePrompt: prePromptPath,
|
|
1876
|
+
postToolResult: postToolResultPath
|
|
1459
1877
|
}
|
|
1460
1878
|
};
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
return {
|
|
1881
|
+
success: false,
|
|
1882
|
+
message: "Failed to install hooks",
|
|
1883
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function uninstallHooks(settingsPath, global) {
|
|
1888
|
+
try {
|
|
1889
|
+
if (!existsSync3(settingsPath)) {
|
|
1890
|
+
return {
|
|
1891
|
+
success: true,
|
|
1892
|
+
message: "No hooks installed (settings.json not found)",
|
|
1893
|
+
installed: false
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const settingsJson = readFileSync4(settingsPath, "utf-8");
|
|
1897
|
+
const settings = JSON.parse(settingsJson);
|
|
1898
|
+
if (settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null) {
|
|
1899
|
+
const hooks = settings["hooks"];
|
|
1900
|
+
delete hooks["pre-prompt"];
|
|
1901
|
+
delete hooks["post-tool-result"];
|
|
1902
|
+
if (Object.keys(hooks).length === 0) {
|
|
1903
|
+
delete settings["hooks"];
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1907
|
+
return {
|
|
1908
|
+
success: true,
|
|
1909
|
+
message: global ? "Hooks uninstalled globally" : "Hooks uninstalled from current project",
|
|
1910
|
+
installed: false
|
|
1911
|
+
};
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
return {
|
|
1914
|
+
success: false,
|
|
1915
|
+
message: "Failed to uninstall hooks",
|
|
1916
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
async function hooksStatus(settingsPath, global) {
|
|
1921
|
+
try {
|
|
1922
|
+
if (!existsSync3(settingsPath)) {
|
|
1923
|
+
return {
|
|
1924
|
+
success: true,
|
|
1925
|
+
message: global ? "No global hooks installed (settings.json not found)" : "No project hooks installed (settings.json not found)",
|
|
1926
|
+
installed: false
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
const settingsJson = readFileSync4(settingsPath, "utf-8");
|
|
1930
|
+
const settings = JSON.parse(settingsJson);
|
|
1931
|
+
const hasHooks = settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null && "pre-prompt" in settings["hooks"] && "post-tool-result" in settings["hooks"];
|
|
1932
|
+
if (!hasHooks) {
|
|
1933
|
+
return {
|
|
1934
|
+
success: true,
|
|
1935
|
+
message: global ? "No global hooks installed" : "No project hooks installed",
|
|
1936
|
+
installed: false
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
const hooks = settings["hooks"];
|
|
1940
|
+
return {
|
|
1941
|
+
success: true,
|
|
1942
|
+
message: global ? "Global hooks active" : "Project hooks active",
|
|
1943
|
+
installed: true,
|
|
1944
|
+
hookPaths: {
|
|
1945
|
+
prePrompt: hooks["pre-prompt"] || "",
|
|
1946
|
+
postToolResult: hooks["post-tool-result"] || ""
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
return {
|
|
1951
|
+
success: false,
|
|
1952
|
+
message: "Failed to check hooks status",
|
|
1953
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
var init_hooks = __esm({
|
|
1958
|
+
"src/cli/commands/hooks.ts"() {
|
|
1959
|
+
"use strict";
|
|
1960
|
+
init_esm_shims();
|
|
1461
1961
|
}
|
|
1462
1962
|
});
|
|
1463
1963
|
|
|
@@ -1465,18 +1965,18 @@ var init_config2 = __esm({
|
|
|
1465
1965
|
init_esm_shims();
|
|
1466
1966
|
init_banner();
|
|
1467
1967
|
import { spawn as spawn2 } from "child_process";
|
|
1468
|
-
import { readFileSync as
|
|
1469
|
-
import { dirname as
|
|
1968
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1969
|
+
import { dirname as dirname4, join as join4, resolve } from "path";
|
|
1470
1970
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1471
1971
|
import { Command } from "commander";
|
|
1472
1972
|
function getVersion2() {
|
|
1473
1973
|
try {
|
|
1474
|
-
const pkg = JSON.parse(
|
|
1974
|
+
const pkg = JSON.parse(readFileSync5(join4(process.cwd(), "package.json"), "utf-8"));
|
|
1475
1975
|
return pkg.version;
|
|
1476
1976
|
} catch {
|
|
1477
1977
|
const __filename2 = fileURLToPath3(import.meta.url);
|
|
1478
|
-
const __dirname2 =
|
|
1479
|
-
const pkg = JSON.parse(
|
|
1978
|
+
const __dirname2 = dirname4(__filename2);
|
|
1979
|
+
const pkg = JSON.parse(readFileSync5(join4(__dirname2, "../../package.json"), "utf-8"));
|
|
1480
1980
|
return pkg.version;
|
|
1481
1981
|
}
|
|
1482
1982
|
}
|
|
@@ -1860,6 +2360,139 @@ The config file is located at .sparn/config.yaml
|
|
|
1860
2360
|
process.exit(1);
|
|
1861
2361
|
}
|
|
1862
2362
|
});
|
|
2363
|
+
program.command("daemon <subcommand>").description("Manage real-time optimization daemon").addHelpText(
|
|
2364
|
+
"after",
|
|
2365
|
+
`
|
|
2366
|
+
Subcommands:
|
|
2367
|
+
start # Start daemon
|
|
2368
|
+
stop # Stop daemon
|
|
2369
|
+
status # Check daemon status
|
|
2370
|
+
|
|
2371
|
+
Examples:
|
|
2372
|
+
$ sparn daemon start # Start watching Claude Code sessions
|
|
2373
|
+
$ sparn daemon stop # Stop daemon
|
|
2374
|
+
$ sparn daemon status # Check if daemon is running
|
|
2375
|
+
|
|
2376
|
+
The daemon watches ~/.claude/projects/**/*.jsonl and automatically
|
|
2377
|
+
optimizes contexts when they exceed the configured threshold.
|
|
2378
|
+
`
|
|
2379
|
+
).action(async (subcommand) => {
|
|
2380
|
+
const { load: parseYAML2 } = await import("js-yaml");
|
|
2381
|
+
const { createDaemonCommand: createDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon_process(), daemon_process_exports));
|
|
2382
|
+
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
2383
|
+
try {
|
|
2384
|
+
const configPath = resolve(process.cwd(), ".sparn/config.yaml");
|
|
2385
|
+
const configYAML = readFileSync5(configPath, "utf-8");
|
|
2386
|
+
const config = parseYAML2(configYAML);
|
|
2387
|
+
const daemon = createDaemonCommand2();
|
|
2388
|
+
switch (subcommand) {
|
|
2389
|
+
case "start": {
|
|
2390
|
+
const result = await daemon.start(config);
|
|
2391
|
+
if (result.success) {
|
|
2392
|
+
console.log(neuralCyan2(`
|
|
2393
|
+
\u2713 ${result.message}
|
|
2394
|
+
`));
|
|
2395
|
+
} else {
|
|
2396
|
+
console.error(errorRed2(`
|
|
2397
|
+
\u2717 ${result.message}
|
|
2398
|
+
`));
|
|
2399
|
+
process.exit(1);
|
|
2400
|
+
}
|
|
2401
|
+
break;
|
|
2402
|
+
}
|
|
2403
|
+
case "stop": {
|
|
2404
|
+
const result = await daemon.stop(config);
|
|
2405
|
+
if (result.success) {
|
|
2406
|
+
console.log(neuralCyan2(`
|
|
2407
|
+
\u2713 ${result.message}
|
|
2408
|
+
`));
|
|
2409
|
+
} else {
|
|
2410
|
+
console.error(errorRed2(`
|
|
2411
|
+
\u2717 ${result.message}
|
|
2412
|
+
`));
|
|
2413
|
+
process.exit(1);
|
|
2414
|
+
}
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
case "status": {
|
|
2418
|
+
const result = await daemon.status(config);
|
|
2419
|
+
if (result.running) {
|
|
2420
|
+
console.log(neuralCyan2(`
|
|
2421
|
+
\u2713 ${result.message}`));
|
|
2422
|
+
if (result.sessionsWatched !== void 0) {
|
|
2423
|
+
console.log(` Sessions watched: ${result.sessionsWatched}`);
|
|
2424
|
+
}
|
|
2425
|
+
if (result.tokensSaved !== void 0) {
|
|
2426
|
+
console.log(` Tokens saved: ${result.tokensSaved.toLocaleString()}`);
|
|
2427
|
+
}
|
|
2428
|
+
console.log();
|
|
2429
|
+
} else {
|
|
2430
|
+
console.log(errorRed2(`
|
|
2431
|
+
\u2717 ${result.message}
|
|
2432
|
+
`));
|
|
2433
|
+
}
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
default:
|
|
2437
|
+
console.error(errorRed2(`
|
|
2438
|
+
Unknown subcommand: ${subcommand}
|
|
2439
|
+
`));
|
|
2440
|
+
console.error("Valid subcommands: start, stop, status\n");
|
|
2441
|
+
process.exit(1);
|
|
2442
|
+
}
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
|
|
2445
|
+
process.exit(1);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
program.command("hooks <subcommand>").description("Manage Claude Code hook integration").option("--global", "Install hooks globally (for all projects)").addHelpText(
|
|
2449
|
+
"after",
|
|
2450
|
+
`
|
|
2451
|
+
Subcommands:
|
|
2452
|
+
install # Install hooks
|
|
2453
|
+
uninstall # Uninstall hooks
|
|
2454
|
+
status # Check hook status
|
|
2455
|
+
|
|
2456
|
+
Examples:
|
|
2457
|
+
$ sparn hooks install # Install hooks for current project
|
|
2458
|
+
$ sparn hooks install --global # Install hooks globally
|
|
2459
|
+
$ sparn hooks uninstall # Uninstall hooks
|
|
2460
|
+
$ sparn hooks status # Check if hooks are active
|
|
2461
|
+
|
|
2462
|
+
Hooks automatically optimize context before each Claude Code prompt
|
|
2463
|
+
and compress verbose tool results after execution.
|
|
2464
|
+
`
|
|
2465
|
+
).action(async (subcommand, options) => {
|
|
2466
|
+
const { hooksCommand: hooksCommand2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
2467
|
+
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
2468
|
+
try {
|
|
2469
|
+
const result = await hooksCommand2({
|
|
2470
|
+
subcommand,
|
|
2471
|
+
global: options.global || false
|
|
2472
|
+
});
|
|
2473
|
+
if (result.success) {
|
|
2474
|
+
console.log(neuralCyan2(`
|
|
2475
|
+
\u2713 ${result.message}`));
|
|
2476
|
+
if (result.hookPaths) {
|
|
2477
|
+
console.log("\nHook paths:");
|
|
2478
|
+
console.log(` pre-prompt: ${result.hookPaths.prePrompt}`);
|
|
2479
|
+
console.log(` post-tool-result: ${result.hookPaths.postToolResult}`);
|
|
2480
|
+
}
|
|
2481
|
+
console.log();
|
|
2482
|
+
} else {
|
|
2483
|
+
console.error(errorRed2(`
|
|
2484
|
+
\u2717 ${result.message}`));
|
|
2485
|
+
if (result.error) {
|
|
2486
|
+
console.error(` ${result.error}`);
|
|
2487
|
+
}
|
|
2488
|
+
console.log();
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
|
|
2493
|
+
process.exit(1);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
1863
2496
|
program.on("option:version", () => {
|
|
1864
2497
|
console.log(getBanner(VERSION2));
|
|
1865
2498
|
process.exit(0);
|