@ulrichc1/sparn 1.0.1 → 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 +38 -6
- package/dist/cli/index.cjs +646 -15
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +639 -6
- 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.cjs
CHANGED
|
@@ -372,7 +372,17 @@ var init_config = __esm({
|
|
|
372
372
|
sounds: false,
|
|
373
373
|
verbose: false
|
|
374
374
|
},
|
|
375
|
-
autoConsolidate: null
|
|
375
|
+
autoConsolidate: null,
|
|
376
|
+
realtime: {
|
|
377
|
+
tokenBudget: 5e4,
|
|
378
|
+
autoOptimizeThreshold: 8e4,
|
|
379
|
+
watchPatterns: ["**/*.jsonl"],
|
|
380
|
+
pidFile: ".sparn/daemon.pid",
|
|
381
|
+
logFile: ".sparn/daemon.log",
|
|
382
|
+
debounceMs: 5e3,
|
|
383
|
+
incremental: true,
|
|
384
|
+
windowSize: 500
|
|
385
|
+
}
|
|
376
386
|
};
|
|
377
387
|
}
|
|
378
388
|
});
|
|
@@ -389,8 +399,8 @@ function getVersion() {
|
|
|
389
399
|
return pkg.version;
|
|
390
400
|
} catch {
|
|
391
401
|
const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
392
|
-
const
|
|
393
|
-
const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path.join)(
|
|
402
|
+
const __dirname2 = (0, import_node_path.dirname)(__filename2);
|
|
403
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path.join)(__dirname2, "../../package.json"), "utf-8"));
|
|
394
404
|
return pkg.version;
|
|
395
405
|
}
|
|
396
406
|
}
|
|
@@ -1485,27 +1495,515 @@ var init_config2 = __esm({
|
|
|
1485
1495
|
validate: (v) => v === null || typeof v === "number" && v > 0,
|
|
1486
1496
|
errorMessage: "autoConsolidate must be a positive number (hours) or null",
|
|
1487
1497
|
parse: (v) => v === "null" ? null : Number.parseFloat(v)
|
|
1498
|
+
},
|
|
1499
|
+
"realtime.tokenBudget": {
|
|
1500
|
+
path: ["realtime", "tokenBudget"],
|
|
1501
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1502
|
+
errorMessage: "tokenBudget must be a positive number",
|
|
1503
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1504
|
+
},
|
|
1505
|
+
"realtime.autoOptimizeThreshold": {
|
|
1506
|
+
path: ["realtime", "autoOptimizeThreshold"],
|
|
1507
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1508
|
+
errorMessage: "autoOptimizeThreshold must be a positive number",
|
|
1509
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1510
|
+
},
|
|
1511
|
+
"realtime.watchPatterns": {
|
|
1512
|
+
path: ["realtime", "watchPatterns"],
|
|
1513
|
+
validate: (v) => Array.isArray(v) && v.every((p) => typeof p === "string"),
|
|
1514
|
+
errorMessage: "watchPatterns must be an array of strings",
|
|
1515
|
+
parse: (v) => v.split(",").map((p) => p.trim())
|
|
1516
|
+
},
|
|
1517
|
+
"realtime.pidFile": {
|
|
1518
|
+
path: ["realtime", "pidFile"],
|
|
1519
|
+
validate: (v) => typeof v === "string" && v.length > 0,
|
|
1520
|
+
errorMessage: "pidFile must be a non-empty string",
|
|
1521
|
+
parse: (v) => v
|
|
1522
|
+
},
|
|
1523
|
+
"realtime.logFile": {
|
|
1524
|
+
path: ["realtime", "logFile"],
|
|
1525
|
+
validate: (v) => typeof v === "string" && v.length > 0,
|
|
1526
|
+
errorMessage: "logFile must be a non-empty string",
|
|
1527
|
+
parse: (v) => v
|
|
1528
|
+
},
|
|
1529
|
+
"realtime.debounceMs": {
|
|
1530
|
+
path: ["realtime", "debounceMs"],
|
|
1531
|
+
validate: (v) => typeof v === "number" && v >= 0,
|
|
1532
|
+
errorMessage: "debounceMs must be a non-negative number",
|
|
1533
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1534
|
+
},
|
|
1535
|
+
"realtime.incremental": {
|
|
1536
|
+
path: ["realtime", "incremental"],
|
|
1537
|
+
validate: (v) => typeof v === "boolean",
|
|
1538
|
+
errorMessage: "incremental must be true or false",
|
|
1539
|
+
parse: (v) => v === "true"
|
|
1540
|
+
},
|
|
1541
|
+
"realtime.windowSize": {
|
|
1542
|
+
path: ["realtime", "windowSize"],
|
|
1543
|
+
validate: (v) => typeof v === "number" && v > 0,
|
|
1544
|
+
errorMessage: "windowSize must be a positive number",
|
|
1545
|
+
parse: (v) => Number.parseInt(v, 10)
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// src/core/metrics.ts
|
|
1552
|
+
function createMetricsCollector() {
|
|
1553
|
+
const optimizations = [];
|
|
1554
|
+
let daemonMetrics = {
|
|
1555
|
+
startTime: Date.now(),
|
|
1556
|
+
sessionsWatched: 0,
|
|
1557
|
+
totalOptimizations: 0,
|
|
1558
|
+
totalTokensSaved: 0,
|
|
1559
|
+
averageLatency: 0,
|
|
1560
|
+
memoryUsage: 0
|
|
1561
|
+
};
|
|
1562
|
+
let cacheHits = 0;
|
|
1563
|
+
let cacheMisses = 0;
|
|
1564
|
+
function recordOptimization(metric) {
|
|
1565
|
+
optimizations.push(metric);
|
|
1566
|
+
daemonMetrics.totalOptimizations++;
|
|
1567
|
+
daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
|
|
1568
|
+
if (metric.cacheHitRate > 0) {
|
|
1569
|
+
const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
|
|
1570
|
+
cacheHits += hits;
|
|
1571
|
+
cacheMisses += metric.entriesProcessed - hits;
|
|
1572
|
+
}
|
|
1573
|
+
daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
|
|
1574
|
+
if (optimizations.length > 1e3) {
|
|
1575
|
+
optimizations.shift();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function updateDaemon(metric) {
|
|
1579
|
+
daemonMetrics = {
|
|
1580
|
+
...daemonMetrics,
|
|
1581
|
+
...metric
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function calculatePercentile(values, percentile) {
|
|
1585
|
+
if (values.length === 0) return 0;
|
|
1586
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1587
|
+
const index = Math.ceil(percentile / 100 * sorted.length) - 1;
|
|
1588
|
+
return sorted[index] || 0;
|
|
1589
|
+
}
|
|
1590
|
+
function getSnapshot() {
|
|
1591
|
+
const totalRuns = optimizations.length;
|
|
1592
|
+
const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
|
|
1593
|
+
const totalTokensSaved = optimizations.reduce(
|
|
1594
|
+
(sum, m) => sum + (m.tokensBefore - m.tokensAfter),
|
|
1595
|
+
0
|
|
1596
|
+
);
|
|
1597
|
+
const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
|
|
1598
|
+
const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
|
|
1599
|
+
const durations = optimizations.map((m) => m.duration);
|
|
1600
|
+
const totalCacheQueries = cacheHits + cacheMisses;
|
|
1601
|
+
const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
|
|
1602
|
+
return {
|
|
1603
|
+
timestamp: Date.now(),
|
|
1604
|
+
optimization: {
|
|
1605
|
+
totalRuns,
|
|
1606
|
+
totalDuration,
|
|
1607
|
+
totalTokensSaved,
|
|
1608
|
+
averageReduction,
|
|
1609
|
+
p50Latency: calculatePercentile(durations, 50),
|
|
1610
|
+
p95Latency: calculatePercentile(durations, 95),
|
|
1611
|
+
p99Latency: calculatePercentile(durations, 99)
|
|
1612
|
+
},
|
|
1613
|
+
cache: {
|
|
1614
|
+
hitRate,
|
|
1615
|
+
totalHits: cacheHits,
|
|
1616
|
+
totalMisses: cacheMisses,
|
|
1617
|
+
size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
|
|
1618
|
+
},
|
|
1619
|
+
daemon: {
|
|
1620
|
+
uptime: Date.now() - daemonMetrics.startTime,
|
|
1621
|
+
sessionsWatched: daemonMetrics.sessionsWatched,
|
|
1622
|
+
memoryUsage: daemonMetrics.memoryUsage
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function exportMetrics() {
|
|
1627
|
+
return JSON.stringify(getSnapshot(), null, 2);
|
|
1628
|
+
}
|
|
1629
|
+
function reset() {
|
|
1630
|
+
optimizations.length = 0;
|
|
1631
|
+
cacheHits = 0;
|
|
1632
|
+
cacheMisses = 0;
|
|
1633
|
+
daemonMetrics = {
|
|
1634
|
+
startTime: Date.now(),
|
|
1635
|
+
sessionsWatched: 0,
|
|
1636
|
+
totalOptimizations: 0,
|
|
1637
|
+
totalTokensSaved: 0,
|
|
1638
|
+
averageLatency: 0,
|
|
1639
|
+
memoryUsage: 0
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
recordOptimization,
|
|
1644
|
+
updateDaemon,
|
|
1645
|
+
getSnapshot,
|
|
1646
|
+
export: exportMetrics,
|
|
1647
|
+
reset
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function getMetrics() {
|
|
1651
|
+
if (!globalMetrics) {
|
|
1652
|
+
globalMetrics = createMetricsCollector();
|
|
1653
|
+
}
|
|
1654
|
+
return globalMetrics;
|
|
1655
|
+
}
|
|
1656
|
+
var globalMetrics;
|
|
1657
|
+
var init_metrics = __esm({
|
|
1658
|
+
"src/core/metrics.ts"() {
|
|
1659
|
+
"use strict";
|
|
1660
|
+
init_cjs_shims();
|
|
1661
|
+
globalMetrics = null;
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
// src/daemon/daemon-process.ts
|
|
1666
|
+
var daemon_process_exports = {};
|
|
1667
|
+
__export(daemon_process_exports, {
|
|
1668
|
+
createDaemonCommand: () => createDaemonCommand
|
|
1669
|
+
});
|
|
1670
|
+
function createDaemonCommand() {
|
|
1671
|
+
function isDaemonRunning(pidFile) {
|
|
1672
|
+
if (!(0, import_node_fs4.existsSync)(pidFile)) {
|
|
1673
|
+
return { running: false };
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const pidStr = (0, import_node_fs4.readFileSync)(pidFile, "utf-8").trim();
|
|
1677
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
1678
|
+
if (Number.isNaN(pid)) {
|
|
1679
|
+
return { running: false };
|
|
1680
|
+
}
|
|
1681
|
+
try {
|
|
1682
|
+
process.kill(pid, 0);
|
|
1683
|
+
return { running: true, pid };
|
|
1684
|
+
} catch {
|
|
1685
|
+
(0, import_node_fs4.unlinkSync)(pidFile);
|
|
1686
|
+
return { running: false };
|
|
1488
1687
|
}
|
|
1688
|
+
} catch {
|
|
1689
|
+
return { running: false };
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function writePidFile(pidFile, pid) {
|
|
1693
|
+
const dir = (0, import_node_path2.dirname)(pidFile);
|
|
1694
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
1695
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1696
|
+
}
|
|
1697
|
+
(0, import_node_fs4.writeFileSync)(pidFile, String(pid), "utf-8");
|
|
1698
|
+
}
|
|
1699
|
+
function removePidFile(pidFile) {
|
|
1700
|
+
if ((0, import_node_fs4.existsSync)(pidFile)) {
|
|
1701
|
+
(0, import_node_fs4.unlinkSync)(pidFile);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
async function start(config) {
|
|
1705
|
+
const { pidFile, logFile } = config.realtime;
|
|
1706
|
+
const status2 = isDaemonRunning(pidFile);
|
|
1707
|
+
if (status2.running) {
|
|
1708
|
+
return {
|
|
1709
|
+
success: false,
|
|
1710
|
+
pid: status2.pid,
|
|
1711
|
+
message: `Daemon already running (PID ${status2.pid})`,
|
|
1712
|
+
error: "Already running"
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const daemonPath = (0, import_node_path2.join)(__dirname, "index.js");
|
|
1717
|
+
const child = (0, import_node_child_process2.fork)(daemonPath, [], {
|
|
1718
|
+
detached: true,
|
|
1719
|
+
stdio: "ignore",
|
|
1720
|
+
env: {
|
|
1721
|
+
...process.env,
|
|
1722
|
+
SPARN_CONFIG: JSON.stringify(config),
|
|
1723
|
+
SPARN_PID_FILE: pidFile,
|
|
1724
|
+
SPARN_LOG_FILE: logFile
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
child.unref();
|
|
1728
|
+
if (child.pid) {
|
|
1729
|
+
writePidFile(pidFile, child.pid);
|
|
1730
|
+
return {
|
|
1731
|
+
success: true,
|
|
1732
|
+
pid: child.pid,
|
|
1733
|
+
message: `Daemon started (PID ${child.pid})`
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
success: false,
|
|
1738
|
+
message: "Failed to start daemon (no PID)",
|
|
1739
|
+
error: "No PID"
|
|
1740
|
+
};
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
return {
|
|
1743
|
+
success: false,
|
|
1744
|
+
message: "Failed to start daemon",
|
|
1745
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function stop(config) {
|
|
1750
|
+
const { pidFile } = config.realtime;
|
|
1751
|
+
const status2 = isDaemonRunning(pidFile);
|
|
1752
|
+
if (!status2.running || !status2.pid) {
|
|
1753
|
+
return {
|
|
1754
|
+
success: true,
|
|
1755
|
+
message: "Daemon not running"
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
process.kill(status2.pid, "SIGTERM");
|
|
1760
|
+
const maxWait = 5e3;
|
|
1761
|
+
const interval = 100;
|
|
1762
|
+
let waited = 0;
|
|
1763
|
+
while (waited < maxWait) {
|
|
1764
|
+
try {
|
|
1765
|
+
process.kill(status2.pid, 0);
|
|
1766
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
1767
|
+
waited += interval;
|
|
1768
|
+
} catch {
|
|
1769
|
+
removePidFile(pidFile);
|
|
1770
|
+
return {
|
|
1771
|
+
success: true,
|
|
1772
|
+
message: `Daemon stopped (PID ${status2.pid})`
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
process.kill(status2.pid, "SIGKILL");
|
|
1778
|
+
removePidFile(pidFile);
|
|
1779
|
+
return {
|
|
1780
|
+
success: true,
|
|
1781
|
+
message: `Daemon force killed (PID ${status2.pid})`
|
|
1782
|
+
};
|
|
1783
|
+
} catch {
|
|
1784
|
+
removePidFile(pidFile);
|
|
1785
|
+
return {
|
|
1786
|
+
success: true,
|
|
1787
|
+
message: `Daemon stopped (PID ${status2.pid})`
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
return {
|
|
1792
|
+
success: false,
|
|
1793
|
+
message: "Failed to stop daemon",
|
|
1794
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async function status(config) {
|
|
1799
|
+
const { pidFile } = config.realtime;
|
|
1800
|
+
const daemonStatus = isDaemonRunning(pidFile);
|
|
1801
|
+
if (!daemonStatus.running || !daemonStatus.pid) {
|
|
1802
|
+
return {
|
|
1803
|
+
running: false,
|
|
1804
|
+
message: "Daemon not running"
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
const metrics = getMetrics().getSnapshot();
|
|
1808
|
+
return {
|
|
1809
|
+
running: true,
|
|
1810
|
+
pid: daemonStatus.pid,
|
|
1811
|
+
uptime: metrics.daemon.uptime,
|
|
1812
|
+
sessionsWatched: metrics.daemon.sessionsWatched,
|
|
1813
|
+
tokensSaved: metrics.optimization.totalTokensSaved,
|
|
1814
|
+
message: `Daemon running (PID ${daemonStatus.pid})`
|
|
1489
1815
|
};
|
|
1490
1816
|
}
|
|
1817
|
+
return {
|
|
1818
|
+
start,
|
|
1819
|
+
stop,
|
|
1820
|
+
status
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
var import_node_child_process2, import_node_fs4, import_node_path2;
|
|
1824
|
+
var init_daemon_process = __esm({
|
|
1825
|
+
"src/daemon/daemon-process.ts"() {
|
|
1826
|
+
"use strict";
|
|
1827
|
+
init_cjs_shims();
|
|
1828
|
+
import_node_child_process2 = require("child_process");
|
|
1829
|
+
import_node_fs4 = require("fs");
|
|
1830
|
+
import_node_path2 = require("path");
|
|
1831
|
+
init_metrics();
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// src/cli/commands/hooks.ts
|
|
1836
|
+
var hooks_exports = {};
|
|
1837
|
+
__export(hooks_exports, {
|
|
1838
|
+
hooksCommand: () => hooksCommand
|
|
1839
|
+
});
|
|
1840
|
+
async function hooksCommand(options) {
|
|
1841
|
+
const { subcommand, global } = options;
|
|
1842
|
+
const settingsPath = global ? (0, import_node_path3.join)((0, import_node_os.homedir)(), ".claude", "settings.json") : (0, import_node_path3.join)(process.cwd(), ".claude", "settings.json");
|
|
1843
|
+
const hooksDir = (0, import_node_path3.join)((0, import_node_path3.dirname)((0, import_node_path3.dirname)((0, import_node_path3.dirname)(__dirname))), "dist", "hooks");
|
|
1844
|
+
const prePromptPath = (0, import_node_path3.join)(hooksDir, "pre-prompt.js");
|
|
1845
|
+
const postToolResultPath = (0, import_node_path3.join)(hooksDir, "post-tool-result.js");
|
|
1846
|
+
switch (subcommand) {
|
|
1847
|
+
case "install":
|
|
1848
|
+
return await installHooks(settingsPath, prePromptPath, postToolResultPath, global);
|
|
1849
|
+
case "uninstall":
|
|
1850
|
+
return await uninstallHooks(settingsPath, global);
|
|
1851
|
+
case "status":
|
|
1852
|
+
return await hooksStatus(settingsPath, global);
|
|
1853
|
+
default:
|
|
1854
|
+
return {
|
|
1855
|
+
success: false,
|
|
1856
|
+
message: `Unknown subcommand: ${subcommand}`,
|
|
1857
|
+
error: "Invalid subcommand"
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
async function installHooks(settingsPath, prePromptPath, postToolResultPath, global) {
|
|
1862
|
+
try {
|
|
1863
|
+
if (!(0, import_node_fs5.existsSync)(prePromptPath)) {
|
|
1864
|
+
return {
|
|
1865
|
+
success: false,
|
|
1866
|
+
message: `Hook script not found: ${prePromptPath}`,
|
|
1867
|
+
error: "Hook scripts not built. Run `npm run build` first."
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
if (!(0, import_node_fs5.existsSync)(postToolResultPath)) {
|
|
1871
|
+
return {
|
|
1872
|
+
success: false,
|
|
1873
|
+
message: `Hook script not found: ${postToolResultPath}`,
|
|
1874
|
+
error: "Hook scripts not built. Run `npm run build` first."
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
let settings = {};
|
|
1878
|
+
if ((0, import_node_fs5.existsSync)(settingsPath)) {
|
|
1879
|
+
const settingsJson = (0, import_node_fs5.readFileSync)(settingsPath, "utf-8");
|
|
1880
|
+
settings = JSON.parse(settingsJson);
|
|
1881
|
+
} else {
|
|
1882
|
+
const claudeDir = (0, import_node_path3.dirname)(settingsPath);
|
|
1883
|
+
if (!(0, import_node_fs5.existsSync)(claudeDir)) {
|
|
1884
|
+
(0, import_node_fs5.mkdirSync)(claudeDir, { recursive: true });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
settings["hooks"] = {
|
|
1888
|
+
...typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {},
|
|
1889
|
+
"pre-prompt": `node ${prePromptPath}`,
|
|
1890
|
+
"post-tool-result": `node ${postToolResultPath}`
|
|
1891
|
+
};
|
|
1892
|
+
(0, import_node_fs5.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1893
|
+
return {
|
|
1894
|
+
success: true,
|
|
1895
|
+
message: global ? "Hooks installed globally (all projects)" : "Hooks installed for current project",
|
|
1896
|
+
installed: true,
|
|
1897
|
+
hookPaths: {
|
|
1898
|
+
prePrompt: prePromptPath,
|
|
1899
|
+
postToolResult: postToolResultPath
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return {
|
|
1904
|
+
success: false,
|
|
1905
|
+
message: "Failed to install hooks",
|
|
1906
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
async function uninstallHooks(settingsPath, global) {
|
|
1911
|
+
try {
|
|
1912
|
+
if (!(0, import_node_fs5.existsSync)(settingsPath)) {
|
|
1913
|
+
return {
|
|
1914
|
+
success: true,
|
|
1915
|
+
message: "No hooks installed (settings.json not found)",
|
|
1916
|
+
installed: false
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
const settingsJson = (0, import_node_fs5.readFileSync)(settingsPath, "utf-8");
|
|
1920
|
+
const settings = JSON.parse(settingsJson);
|
|
1921
|
+
if (settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null) {
|
|
1922
|
+
const hooks = settings["hooks"];
|
|
1923
|
+
delete hooks["pre-prompt"];
|
|
1924
|
+
delete hooks["post-tool-result"];
|
|
1925
|
+
if (Object.keys(hooks).length === 0) {
|
|
1926
|
+
delete settings["hooks"];
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
(0, import_node_fs5.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1930
|
+
return {
|
|
1931
|
+
success: true,
|
|
1932
|
+
message: global ? "Hooks uninstalled globally" : "Hooks uninstalled from current project",
|
|
1933
|
+
installed: false
|
|
1934
|
+
};
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
return {
|
|
1937
|
+
success: false,
|
|
1938
|
+
message: "Failed to uninstall hooks",
|
|
1939
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
async function hooksStatus(settingsPath, global) {
|
|
1944
|
+
try {
|
|
1945
|
+
if (!(0, import_node_fs5.existsSync)(settingsPath)) {
|
|
1946
|
+
return {
|
|
1947
|
+
success: true,
|
|
1948
|
+
message: global ? "No global hooks installed (settings.json not found)" : "No project hooks installed (settings.json not found)",
|
|
1949
|
+
installed: false
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
const settingsJson = (0, import_node_fs5.readFileSync)(settingsPath, "utf-8");
|
|
1953
|
+
const settings = JSON.parse(settingsJson);
|
|
1954
|
+
const hasHooks = settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null && "pre-prompt" in settings["hooks"] && "post-tool-result" in settings["hooks"];
|
|
1955
|
+
if (!hasHooks) {
|
|
1956
|
+
return {
|
|
1957
|
+
success: true,
|
|
1958
|
+
message: global ? "No global hooks installed" : "No project hooks installed",
|
|
1959
|
+
installed: false
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
const hooks = settings["hooks"];
|
|
1963
|
+
return {
|
|
1964
|
+
success: true,
|
|
1965
|
+
message: global ? "Global hooks active" : "Project hooks active",
|
|
1966
|
+
installed: true,
|
|
1967
|
+
hookPaths: {
|
|
1968
|
+
prePrompt: hooks["pre-prompt"] || "",
|
|
1969
|
+
postToolResult: hooks["post-tool-result"] || ""
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
return {
|
|
1974
|
+
success: false,
|
|
1975
|
+
message: "Failed to check hooks status",
|
|
1976
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
var import_node_fs5, import_node_os, import_node_path3;
|
|
1981
|
+
var init_hooks = __esm({
|
|
1982
|
+
"src/cli/commands/hooks.ts"() {
|
|
1983
|
+
"use strict";
|
|
1984
|
+
init_cjs_shims();
|
|
1985
|
+
import_node_fs5 = require("fs");
|
|
1986
|
+
import_node_os = require("os");
|
|
1987
|
+
import_node_path3 = require("path");
|
|
1988
|
+
}
|
|
1491
1989
|
});
|
|
1492
1990
|
|
|
1493
1991
|
// src/cli/index.ts
|
|
1494
1992
|
init_cjs_shims();
|
|
1495
|
-
var
|
|
1496
|
-
var
|
|
1497
|
-
var
|
|
1993
|
+
var import_node_child_process3 = require("child_process");
|
|
1994
|
+
var import_node_fs6 = require("fs");
|
|
1995
|
+
var import_node_path4 = require("path");
|
|
1498
1996
|
var import_node_url2 = require("url");
|
|
1499
1997
|
var import_commander = require("commander");
|
|
1500
1998
|
init_banner();
|
|
1501
1999
|
function getVersion2() {
|
|
1502
2000
|
try {
|
|
1503
|
-
const pkg = JSON.parse((0,
|
|
2001
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path4.join)(process.cwd(), "package.json"), "utf-8"));
|
|
1504
2002
|
return pkg.version;
|
|
1505
2003
|
} catch {
|
|
1506
2004
|
const __filename2 = (0, import_node_url2.fileURLToPath)(importMetaUrl);
|
|
1507
|
-
const
|
|
1508
|
-
const pkg = JSON.parse((0,
|
|
2005
|
+
const __dirname2 = (0, import_node_path4.dirname)(__filename2);
|
|
2006
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path4.join)(__dirname2, "../../package.json"), "utf-8"));
|
|
1509
2007
|
return pkg.version;
|
|
1510
2008
|
}
|
|
1511
2009
|
}
|
|
@@ -1626,7 +2124,7 @@ Typical Results:
|
|
|
1626
2124
|
spinner.text = `\u{1F4D6} Reading context from ${options.input}...`;
|
|
1627
2125
|
}
|
|
1628
2126
|
spinner.text = "\u{1F4BE} Loading memory database...";
|
|
1629
|
-
const dbPath = (0,
|
|
2127
|
+
const dbPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/memory.db");
|
|
1630
2128
|
const memory = await createKVMemory2(dbPath);
|
|
1631
2129
|
spinner.text = "\u26A1 Applying neuroscience principles...";
|
|
1632
2130
|
const result = await optimizeCommand2({
|
|
@@ -1690,7 +2188,7 @@ Tracked Metrics:
|
|
|
1690
2188
|
try {
|
|
1691
2189
|
if (spinner) spinner.start();
|
|
1692
2190
|
if (spinner) spinner.text = "\u{1F4BE} Loading optimization history...";
|
|
1693
|
-
const dbPath = (0,
|
|
2191
|
+
const dbPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/memory.db");
|
|
1694
2192
|
const memory = await createKVMemory2(dbPath);
|
|
1695
2193
|
let confirmReset = false;
|
|
1696
2194
|
if (options.reset) {
|
|
@@ -1750,7 +2248,7 @@ The relay command passes the exit code from the wrapped command.
|
|
|
1750
2248
|
const { relayCommand: relayCommand2 } = await Promise.resolve().then(() => (init_relay(), relay_exports));
|
|
1751
2249
|
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
1752
2250
|
try {
|
|
1753
|
-
const dbPath = (0,
|
|
2251
|
+
const dbPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/memory.db");
|
|
1754
2252
|
const memory = await createKVMemory2(dbPath);
|
|
1755
2253
|
const result = await relayCommand2({
|
|
1756
2254
|
command,
|
|
@@ -1803,7 +2301,7 @@ Typical Results:
|
|
|
1803
2301
|
try {
|
|
1804
2302
|
spinner.start();
|
|
1805
2303
|
spinner.text = "\u{1F4BE} Loading memory database...";
|
|
1806
|
-
const dbPath = (0,
|
|
2304
|
+
const dbPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/memory.db");
|
|
1807
2305
|
const memory = await createKVMemory2(dbPath);
|
|
1808
2306
|
spinner.text = "\u{1F50D} Identifying decayed entries...";
|
|
1809
2307
|
const result = await consolidateCommand2({ memory });
|
|
@@ -1851,7 +2349,7 @@ The config file is located at .sparn/config.yaml
|
|
|
1851
2349
|
const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
1852
2350
|
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
1853
2351
|
try {
|
|
1854
|
-
const configPath = (0,
|
|
2352
|
+
const configPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/config.yaml");
|
|
1855
2353
|
const result = await configCommand2({
|
|
1856
2354
|
configPath,
|
|
1857
2355
|
subcommand,
|
|
@@ -1868,7 +2366,7 @@ The config file is located at .sparn/config.yaml
|
|
|
1868
2366
|
console.log(neuralCyan2(`
|
|
1869
2367
|
\u{1F4DD} Opening config in ${editor}...
|
|
1870
2368
|
`));
|
|
1871
|
-
const child = (0,
|
|
2369
|
+
const child = (0, import_node_child_process3.spawn)(editor, [result.editorPath], {
|
|
1872
2370
|
stdio: "inherit"
|
|
1873
2371
|
});
|
|
1874
2372
|
child.on("close", (code) => {
|
|
@@ -1889,6 +2387,139 @@ The config file is located at .sparn/config.yaml
|
|
|
1889
2387
|
process.exit(1);
|
|
1890
2388
|
}
|
|
1891
2389
|
});
|
|
2390
|
+
program.command("daemon <subcommand>").description("Manage real-time optimization daemon").addHelpText(
|
|
2391
|
+
"after",
|
|
2392
|
+
`
|
|
2393
|
+
Subcommands:
|
|
2394
|
+
start # Start daemon
|
|
2395
|
+
stop # Stop daemon
|
|
2396
|
+
status # Check daemon status
|
|
2397
|
+
|
|
2398
|
+
Examples:
|
|
2399
|
+
$ sparn daemon start # Start watching Claude Code sessions
|
|
2400
|
+
$ sparn daemon stop # Stop daemon
|
|
2401
|
+
$ sparn daemon status # Check if daemon is running
|
|
2402
|
+
|
|
2403
|
+
The daemon watches ~/.claude/projects/**/*.jsonl and automatically
|
|
2404
|
+
optimizes contexts when they exceed the configured threshold.
|
|
2405
|
+
`
|
|
2406
|
+
).action(async (subcommand) => {
|
|
2407
|
+
const { load: parseYAML2 } = await import("js-yaml");
|
|
2408
|
+
const { createDaemonCommand: createDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon_process(), daemon_process_exports));
|
|
2409
|
+
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
2410
|
+
try {
|
|
2411
|
+
const configPath = (0, import_node_path4.resolve)(process.cwd(), ".sparn/config.yaml");
|
|
2412
|
+
const configYAML = (0, import_node_fs6.readFileSync)(configPath, "utf-8");
|
|
2413
|
+
const config = parseYAML2(configYAML);
|
|
2414
|
+
const daemon = createDaemonCommand2();
|
|
2415
|
+
switch (subcommand) {
|
|
2416
|
+
case "start": {
|
|
2417
|
+
const result = await daemon.start(config);
|
|
2418
|
+
if (result.success) {
|
|
2419
|
+
console.log(neuralCyan2(`
|
|
2420
|
+
\u2713 ${result.message}
|
|
2421
|
+
`));
|
|
2422
|
+
} else {
|
|
2423
|
+
console.error(errorRed2(`
|
|
2424
|
+
\u2717 ${result.message}
|
|
2425
|
+
`));
|
|
2426
|
+
process.exit(1);
|
|
2427
|
+
}
|
|
2428
|
+
break;
|
|
2429
|
+
}
|
|
2430
|
+
case "stop": {
|
|
2431
|
+
const result = await daemon.stop(config);
|
|
2432
|
+
if (result.success) {
|
|
2433
|
+
console.log(neuralCyan2(`
|
|
2434
|
+
\u2713 ${result.message}
|
|
2435
|
+
`));
|
|
2436
|
+
} else {
|
|
2437
|
+
console.error(errorRed2(`
|
|
2438
|
+
\u2717 ${result.message}
|
|
2439
|
+
`));
|
|
2440
|
+
process.exit(1);
|
|
2441
|
+
}
|
|
2442
|
+
break;
|
|
2443
|
+
}
|
|
2444
|
+
case "status": {
|
|
2445
|
+
const result = await daemon.status(config);
|
|
2446
|
+
if (result.running) {
|
|
2447
|
+
console.log(neuralCyan2(`
|
|
2448
|
+
\u2713 ${result.message}`));
|
|
2449
|
+
if (result.sessionsWatched !== void 0) {
|
|
2450
|
+
console.log(` Sessions watched: ${result.sessionsWatched}`);
|
|
2451
|
+
}
|
|
2452
|
+
if (result.tokensSaved !== void 0) {
|
|
2453
|
+
console.log(` Tokens saved: ${result.tokensSaved.toLocaleString()}`);
|
|
2454
|
+
}
|
|
2455
|
+
console.log();
|
|
2456
|
+
} else {
|
|
2457
|
+
console.log(errorRed2(`
|
|
2458
|
+
\u2717 ${result.message}
|
|
2459
|
+
`));
|
|
2460
|
+
}
|
|
2461
|
+
break;
|
|
2462
|
+
}
|
|
2463
|
+
default:
|
|
2464
|
+
console.error(errorRed2(`
|
|
2465
|
+
Unknown subcommand: ${subcommand}
|
|
2466
|
+
`));
|
|
2467
|
+
console.error("Valid subcommands: start, stop, status\n");
|
|
2468
|
+
process.exit(1);
|
|
2469
|
+
}
|
|
2470
|
+
} catch (error) {
|
|
2471
|
+
console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
|
|
2472
|
+
process.exit(1);
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
program.command("hooks <subcommand>").description("Manage Claude Code hook integration").option("--global", "Install hooks globally (for all projects)").addHelpText(
|
|
2476
|
+
"after",
|
|
2477
|
+
`
|
|
2478
|
+
Subcommands:
|
|
2479
|
+
install # Install hooks
|
|
2480
|
+
uninstall # Uninstall hooks
|
|
2481
|
+
status # Check hook status
|
|
2482
|
+
|
|
2483
|
+
Examples:
|
|
2484
|
+
$ sparn hooks install # Install hooks for current project
|
|
2485
|
+
$ sparn hooks install --global # Install hooks globally
|
|
2486
|
+
$ sparn hooks uninstall # Uninstall hooks
|
|
2487
|
+
$ sparn hooks status # Check if hooks are active
|
|
2488
|
+
|
|
2489
|
+
Hooks automatically optimize context before each Claude Code prompt
|
|
2490
|
+
and compress verbose tool results after execution.
|
|
2491
|
+
`
|
|
2492
|
+
).action(async (subcommand, options) => {
|
|
2493
|
+
const { hooksCommand: hooksCommand2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
2494
|
+
const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
|
|
2495
|
+
try {
|
|
2496
|
+
const result = await hooksCommand2({
|
|
2497
|
+
subcommand,
|
|
2498
|
+
global: options.global || false
|
|
2499
|
+
});
|
|
2500
|
+
if (result.success) {
|
|
2501
|
+
console.log(neuralCyan2(`
|
|
2502
|
+
\u2713 ${result.message}`));
|
|
2503
|
+
if (result.hookPaths) {
|
|
2504
|
+
console.log("\nHook paths:");
|
|
2505
|
+
console.log(` pre-prompt: ${result.hookPaths.prePrompt}`);
|
|
2506
|
+
console.log(` post-tool-result: ${result.hookPaths.postToolResult}`);
|
|
2507
|
+
}
|
|
2508
|
+
console.log();
|
|
2509
|
+
} else {
|
|
2510
|
+
console.error(errorRed2(`
|
|
2511
|
+
\u2717 ${result.message}`));
|
|
2512
|
+
if (result.error) {
|
|
2513
|
+
console.error(` ${result.error}`);
|
|
2514
|
+
}
|
|
2515
|
+
console.log();
|
|
2516
|
+
process.exit(1);
|
|
2517
|
+
}
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
|
|
2520
|
+
process.exit(1);
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
1892
2523
|
program.on("option:version", () => {
|
|
1893
2524
|
console.log(getBanner(VERSION2));
|
|
1894
2525
|
process.exit(0);
|