@hydra-acp/cli 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +901 -96
- package/dist/index.d.ts +100 -0
- package/dist/index.js +656 -26
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -930,6 +930,9 @@ function extractHydraMeta(meta) {
|
|
|
930
930
|
if (typeof obj.promptUpdating === "boolean") {
|
|
931
931
|
out.promptUpdating = obj.promptUpdating;
|
|
932
932
|
}
|
|
933
|
+
if (typeof obj.mcpStdin === "boolean") {
|
|
934
|
+
out.mcpStdin = obj.mcpStdin;
|
|
935
|
+
}
|
|
933
936
|
if (typeof obj.promptAmending === "boolean") {
|
|
934
937
|
out.promptAmending = obj.promptAmending;
|
|
935
938
|
}
|
|
@@ -1163,6 +1166,10 @@ var init_types = __esm({
|
|
|
1163
1166
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1164
1167
|
// Set when this session was spawned as a child by a transformer.
|
|
1165
1168
|
parentSessionId: z3.string().optional(),
|
|
1169
|
+
// clientInfo from the process that issued session/new. Lets list views
|
|
1170
|
+
// hide cat-style ancillary sessions by default while letting an
|
|
1171
|
+
// override flag surface them.
|
|
1172
|
+
originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
|
|
1166
1173
|
updatedAt: z3.string(),
|
|
1167
1174
|
attachedClients: z3.number().int().nonnegative(),
|
|
1168
1175
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -1547,18 +1554,34 @@ var init_connection = __esm({
|
|
|
1547
1554
|
|
|
1548
1555
|
// src/core/stream-buffer.ts
|
|
1549
1556
|
import * as fsp3 from "fs/promises";
|
|
1550
|
-
|
|
1557
|
+
function escapeRegex(s) {
|
|
1558
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1559
|
+
}
|
|
1560
|
+
var DEFAULT_CAPACITY_BYTES, INITIAL_CAPACITY_BYTES, STREAM_READ_MAX_BYTES, STREAM_WAIT_MAX_MS, STREAM_GREP_DEFAULT_MATCHES, STREAM_GREP_MAX_MATCHES, STREAM_GREP_DEFAULT_BYTES, STREAM_GREP_MAX_BYTES, STREAM_GREP_MAX_CONTEXT, SessionStreamBuffer;
|
|
1551
1561
|
var init_stream_buffer = __esm({
|
|
1552
1562
|
"src/core/stream-buffer.ts"() {
|
|
1553
1563
|
"use strict";
|
|
1554
|
-
DEFAULT_CAPACITY_BYTES =
|
|
1564
|
+
DEFAULT_CAPACITY_BYTES = 64 * 1024 * 1024;
|
|
1565
|
+
INITIAL_CAPACITY_BYTES = 1 * 1024 * 1024;
|
|
1555
1566
|
STREAM_READ_MAX_BYTES = 64 * 1024;
|
|
1556
1567
|
STREAM_WAIT_MAX_MS = 6e4;
|
|
1568
|
+
STREAM_GREP_DEFAULT_MATCHES = 100;
|
|
1569
|
+
STREAM_GREP_MAX_MATCHES = 1e3;
|
|
1570
|
+
STREAM_GREP_DEFAULT_BYTES = 64 * 1024;
|
|
1571
|
+
STREAM_GREP_MAX_BYTES = 256 * 1024;
|
|
1572
|
+
STREAM_GREP_MAX_CONTEXT = 20;
|
|
1557
1573
|
SessionStreamBuffer = class {
|
|
1558
1574
|
storage;
|
|
1559
|
-
|
|
1575
|
+
// The configured cap. Eviction begins once writeCursor exceeds this.
|
|
1576
|
+
maxCapacityBytes;
|
|
1577
|
+
// The size of the currently-allocated `storage`. Starts at
|
|
1578
|
+
// INITIAL_CAPACITY_BYTES (clamped to maxCapacityBytes) and doubles on
|
|
1579
|
+
// demand. Once it reaches maxCapacityBytes the ring behaves like a
|
|
1580
|
+
// fixed-size buffer; before then, writeCursor < currentCapacityBytes
|
|
1581
|
+
// always, so no wrap-around math is in play.
|
|
1582
|
+
currentCapacityBytes;
|
|
1560
1583
|
// Absolute monotonic byte offset of the next byte to be written. Also
|
|
1561
|
-
// the count of bytes ever appended. `writeCursor -
|
|
1584
|
+
// the count of bytes ever appended. `writeCursor - currentCapacityBytes`
|
|
1562
1585
|
// (clamped at 0) is the oldest still-resident byte's cursor.
|
|
1563
1586
|
writeCursor = 0;
|
|
1564
1587
|
closed = false;
|
|
@@ -1573,24 +1596,33 @@ var init_stream_buffer = __esm({
|
|
|
1573
1596
|
// calls don't interleave their writes.
|
|
1574
1597
|
fileWriteChain = Promise.resolve();
|
|
1575
1598
|
constructor(opts = {}) {
|
|
1576
|
-
this.
|
|
1577
|
-
if (this.
|
|
1599
|
+
this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
1600
|
+
if (this.maxCapacityBytes <= 0) {
|
|
1578
1601
|
throw new Error("capacityBytes must be > 0");
|
|
1579
1602
|
}
|
|
1580
|
-
this.
|
|
1603
|
+
this.currentCapacityBytes = Math.min(
|
|
1604
|
+
INITIAL_CAPACITY_BYTES,
|
|
1605
|
+
this.maxCapacityBytes
|
|
1606
|
+
);
|
|
1607
|
+
this.storage = Buffer.alloc(this.currentCapacityBytes);
|
|
1581
1608
|
this.filePath = opts.filePath;
|
|
1582
1609
|
this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
|
|
1583
1610
|
this.onFileCapReached = opts.onFileCapReached;
|
|
1584
1611
|
this.logWriteError = opts.logWriteError;
|
|
1585
1612
|
}
|
|
1586
1613
|
get capacity() {
|
|
1587
|
-
return this.
|
|
1614
|
+
return this.maxCapacityBytes;
|
|
1615
|
+
}
|
|
1616
|
+
// Currently-allocated storage size (for observability / tests). May be
|
|
1617
|
+
// anywhere between INITIAL_CAPACITY_BYTES and capacity.
|
|
1618
|
+
get allocatedBytes() {
|
|
1619
|
+
return this.currentCapacityBytes;
|
|
1588
1620
|
}
|
|
1589
1621
|
get writeCursorPos() {
|
|
1590
1622
|
return this.writeCursor;
|
|
1591
1623
|
}
|
|
1592
1624
|
get oldestAvailable() {
|
|
1593
|
-
return Math.max(0, this.writeCursor - this.
|
|
1625
|
+
return Math.max(0, this.writeCursor - this.currentCapacityBytes);
|
|
1594
1626
|
}
|
|
1595
1627
|
get isClosed() {
|
|
1596
1628
|
return this.closed;
|
|
@@ -1735,6 +1767,136 @@ var init_stream_buffer = __esm({
|
|
|
1735
1767
|
this.waiters.push(waiter);
|
|
1736
1768
|
});
|
|
1737
1769
|
}
|
|
1770
|
+
// Scan the resident region line-by-line, returning lines that match
|
|
1771
|
+
// `pattern`. Server-side filtering so the agent doesn't have to pull
|
|
1772
|
+
// and decode 64 KiB base64 windows just to grep a multi-MB log.
|
|
1773
|
+
//
|
|
1774
|
+
// Lines are split on `\n` (LF). A trailing partial line (no LF) is
|
|
1775
|
+
// skipped when the buffer is still open — its bytes might be the
|
|
1776
|
+
// start of a longer line that's still being written — but is treated
|
|
1777
|
+
// as a final full line once the buffer is closed.
|
|
1778
|
+
//
|
|
1779
|
+
// Caps: max 1000 matches and 256 KiB of output bytes per call. The
|
|
1780
|
+
// agent should re-call with `cursor = nextCursor` to resume when
|
|
1781
|
+
// `truncated:true`.
|
|
1782
|
+
grep(opts) {
|
|
1783
|
+
const oldest = this.oldestAvailable;
|
|
1784
|
+
const requested = opts.cursor;
|
|
1785
|
+
let start = requested ?? oldest;
|
|
1786
|
+
let gap = 0;
|
|
1787
|
+
if (requested !== void 0 && requested < oldest) {
|
|
1788
|
+
gap = oldest - requested;
|
|
1789
|
+
start = oldest;
|
|
1790
|
+
}
|
|
1791
|
+
if (start > this.writeCursor) {
|
|
1792
|
+
start = this.writeCursor;
|
|
1793
|
+
}
|
|
1794
|
+
const slice = this.sliceFromRing(start, this.writeCursor - start);
|
|
1795
|
+
const useRegex = opts.regex ?? true;
|
|
1796
|
+
const flags = opts.caseInsensitive === true ? "i" : "";
|
|
1797
|
+
const re = useRegex ? new RegExp(opts.pattern, flags) : new RegExp(escapeRegex(opts.pattern), flags);
|
|
1798
|
+
const invert = opts.invert ?? false;
|
|
1799
|
+
const maxMatches = Math.max(
|
|
1800
|
+
1,
|
|
1801
|
+
Math.min(
|
|
1802
|
+
opts.maxMatches ?? STREAM_GREP_DEFAULT_MATCHES,
|
|
1803
|
+
STREAM_GREP_MAX_MATCHES
|
|
1804
|
+
)
|
|
1805
|
+
);
|
|
1806
|
+
const maxBytes = Math.max(
|
|
1807
|
+
1,
|
|
1808
|
+
Math.min(opts.maxBytes ?? STREAM_GREP_DEFAULT_BYTES, STREAM_GREP_MAX_BYTES)
|
|
1809
|
+
);
|
|
1810
|
+
const contextBefore = Math.max(
|
|
1811
|
+
0,
|
|
1812
|
+
Math.min(opts.contextBefore ?? 0, STREAM_GREP_MAX_CONTEXT)
|
|
1813
|
+
);
|
|
1814
|
+
const contextAfter = Math.max(
|
|
1815
|
+
0,
|
|
1816
|
+
Math.min(opts.contextAfter ?? 0, STREAM_GREP_MAX_CONTEXT)
|
|
1817
|
+
);
|
|
1818
|
+
const matches = [];
|
|
1819
|
+
const beforeRing = [];
|
|
1820
|
+
const pendingAfter = [];
|
|
1821
|
+
let bytesUsed = 0;
|
|
1822
|
+
let truncated = false;
|
|
1823
|
+
let lineStartByte = 0;
|
|
1824
|
+
let resumeFromLineStart = 0;
|
|
1825
|
+
const processLine = (lineCursor, lineText) => {
|
|
1826
|
+
for (const pa of pendingAfter) {
|
|
1827
|
+
if (pa.remaining > 0) {
|
|
1828
|
+
if (pa.match.after === void 0) {
|
|
1829
|
+
pa.match.after = [];
|
|
1830
|
+
}
|
|
1831
|
+
pa.match.after.push({ cursor: lineCursor, line: lineText });
|
|
1832
|
+
pa.remaining--;
|
|
1833
|
+
bytesUsed += lineText.length;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
while (pendingAfter.length > 0 && pendingAfter[0].remaining === 0) {
|
|
1837
|
+
pendingAfter.shift();
|
|
1838
|
+
}
|
|
1839
|
+
const matched = re.test(lineText) !== invert;
|
|
1840
|
+
if (matched && matches.length < maxMatches) {
|
|
1841
|
+
const m = { cursor: lineCursor, line: lineText };
|
|
1842
|
+
if (contextBefore > 0 && beforeRing.length > 0) {
|
|
1843
|
+
m.before = beforeRing.slice();
|
|
1844
|
+
for (const b of m.before) {
|
|
1845
|
+
bytesUsed += b.line.length;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
bytesUsed += lineText.length;
|
|
1849
|
+
matches.push(m);
|
|
1850
|
+
if (contextAfter > 0) {
|
|
1851
|
+
pendingAfter.push({ match: m, remaining: contextAfter });
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (contextBefore > 0) {
|
|
1855
|
+
beforeRing.push({ cursor: lineCursor, line: lineText });
|
|
1856
|
+
while (beforeRing.length > contextBefore) {
|
|
1857
|
+
beforeRing.shift();
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
const hitMaxMatches = matches.length >= maxMatches && pendingAfter.length === 0;
|
|
1861
|
+
const hitMaxBytes = bytesUsed >= maxBytes;
|
|
1862
|
+
return hitMaxMatches || hitMaxBytes;
|
|
1863
|
+
};
|
|
1864
|
+
for (let i = 0; i < slice.length; i++) {
|
|
1865
|
+
if (slice[i] !== 10) {
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
const lineText = slice.subarray(lineStartByte, i).toString("utf8");
|
|
1869
|
+
const lineCursor = start + lineStartByte;
|
|
1870
|
+
lineStartByte = i + 1;
|
|
1871
|
+
resumeFromLineStart = lineStartByte;
|
|
1872
|
+
if (processLine(lineCursor, lineText)) {
|
|
1873
|
+
truncated = true;
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (!truncated && lineStartByte < slice.length && this.closed) {
|
|
1878
|
+
const lineText = slice.subarray(lineStartByte).toString("utf8");
|
|
1879
|
+
const lineCursor = start + lineStartByte;
|
|
1880
|
+
if (processLine(lineCursor, lineText)) {
|
|
1881
|
+
truncated = true;
|
|
1882
|
+
}
|
|
1883
|
+
resumeFromLineStart = slice.length;
|
|
1884
|
+
}
|
|
1885
|
+
const nextCursor = Math.min(start + resumeFromLineStart, this.writeCursor);
|
|
1886
|
+
const result = {
|
|
1887
|
+
matches,
|
|
1888
|
+
truncated,
|
|
1889
|
+
nextCursor,
|
|
1890
|
+
scannedBytes: resumeFromLineStart
|
|
1891
|
+
};
|
|
1892
|
+
if (gap > 0) {
|
|
1893
|
+
result.gap = gap;
|
|
1894
|
+
}
|
|
1895
|
+
if (this.closed && nextCursor >= this.writeCursor) {
|
|
1896
|
+
result.eof = true;
|
|
1897
|
+
}
|
|
1898
|
+
return result;
|
|
1899
|
+
}
|
|
1738
1900
|
wakeWaiters(outcome) {
|
|
1739
1901
|
if (this.waiters.length === 0) {
|
|
1740
1902
|
return;
|
|
@@ -1745,15 +1907,42 @@ var init_stream_buffer = __esm({
|
|
|
1745
1907
|
w.resolve(outcome);
|
|
1746
1908
|
}
|
|
1747
1909
|
}
|
|
1910
|
+
// Grow `storage` if needed to fit `additionalBytes` more bytes without
|
|
1911
|
+
// wrapping. Caps at maxCapacityBytes; once we're at the cap, callers
|
|
1912
|
+
// fall back to ring-wrap behavior. Doubles each grow so we amortize.
|
|
1913
|
+
// Only called before we've ever wrapped (writeCursor < currentCapacity
|
|
1914
|
+
// always holds while we're growing), so the existing bytes live at
|
|
1915
|
+
// storage[0..writeCursor] and we can just copy them flat.
|
|
1916
|
+
growIfNeeded(additionalBytes) {
|
|
1917
|
+
if (this.currentCapacityBytes >= this.maxCapacityBytes) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const needed = this.writeCursor + additionalBytes;
|
|
1921
|
+
if (needed <= this.currentCapacityBytes) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
let next = this.currentCapacityBytes;
|
|
1925
|
+
while (next < needed && next < this.maxCapacityBytes) {
|
|
1926
|
+
next = Math.min(this.maxCapacityBytes, next * 2);
|
|
1927
|
+
}
|
|
1928
|
+
if (next === this.currentCapacityBytes) {
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const newStorage = Buffer.alloc(next);
|
|
1932
|
+
this.storage.copy(newStorage, 0, 0, this.writeCursor);
|
|
1933
|
+
this.storage = newStorage;
|
|
1934
|
+
this.currentCapacityBytes = next;
|
|
1935
|
+
}
|
|
1748
1936
|
writeRing(chunk) {
|
|
1749
1937
|
const len = chunk.length;
|
|
1750
|
-
|
|
1751
|
-
|
|
1938
|
+
this.growIfNeeded(len);
|
|
1939
|
+
if (len >= this.currentCapacityBytes) {
|
|
1940
|
+
const tailStart = len - this.currentCapacityBytes;
|
|
1752
1941
|
chunk.copy(this.storage, 0, tailStart, len);
|
|
1753
1942
|
return;
|
|
1754
1943
|
}
|
|
1755
|
-
const offset = this.writeCursor % this.
|
|
1756
|
-
const tailRoom = this.
|
|
1944
|
+
const offset = this.writeCursor % this.currentCapacityBytes;
|
|
1945
|
+
const tailRoom = this.currentCapacityBytes - offset;
|
|
1757
1946
|
if (len <= tailRoom) {
|
|
1758
1947
|
chunk.copy(this.storage, offset, 0, len);
|
|
1759
1948
|
} else {
|
|
@@ -1766,8 +1955,8 @@ var init_stream_buffer = __esm({
|
|
|
1766
1955
|
return Buffer.alloc(0);
|
|
1767
1956
|
}
|
|
1768
1957
|
const out = Buffer.alloc(length);
|
|
1769
|
-
const offset = fromCursor % this.
|
|
1770
|
-
const tailLen = Math.min(length, this.
|
|
1958
|
+
const offset = fromCursor % this.currentCapacityBytes;
|
|
1959
|
+
const tailLen = Math.min(length, this.currentCapacityBytes - offset);
|
|
1771
1960
|
this.storage.copy(out, 0, offset, offset + tailLen);
|
|
1772
1961
|
if (tailLen < length) {
|
|
1773
1962
|
this.storage.copy(out, tailLen, 0, length - tailLen);
|
|
@@ -2191,6 +2380,7 @@ var init_session = __esm({
|
|
|
2191
2380
|
agentCapabilities;
|
|
2192
2381
|
agentArgs;
|
|
2193
2382
|
parentSessionId;
|
|
2383
|
+
originatingClient;
|
|
2194
2384
|
title;
|
|
2195
2385
|
// Snapshot state delivered to attaching clients via the attach
|
|
2196
2386
|
// response _meta rather than via history replay (which would be
|
|
@@ -2345,6 +2535,7 @@ var init_session = __esm({
|
|
|
2345
2535
|
this.agentCapabilities = init.agentCapabilities;
|
|
2346
2536
|
this.agentArgs = init.agentArgs;
|
|
2347
2537
|
this.parentSessionId = init.parentSessionId;
|
|
2538
|
+
this.originatingClient = init.originatingClient;
|
|
2348
2539
|
this.title = init.title;
|
|
2349
2540
|
this.currentModel = init.currentModel;
|
|
2350
2541
|
this.currentMode = init.currentMode;
|
|
@@ -4532,6 +4723,43 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4532
4723
|
}
|
|
4533
4724
|
return out;
|
|
4534
4725
|
}
|
|
4726
|
+
streamTail(bytes) {
|
|
4727
|
+
const buf = this.requireStreamBuffer();
|
|
4728
|
+
const r = buf.tail(bytes);
|
|
4729
|
+
return {
|
|
4730
|
+
bytes: r.bytes.toString("base64"),
|
|
4731
|
+
startCursor: r.startCursor,
|
|
4732
|
+
endCursor: r.endCursor,
|
|
4733
|
+
truncated: r.truncated
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
streamHead(bytes) {
|
|
4737
|
+
const buf = this.requireStreamBuffer();
|
|
4738
|
+
const r = buf.head(bytes);
|
|
4739
|
+
return {
|
|
4740
|
+
bytes: r.bytes.toString("base64"),
|
|
4741
|
+
startCursor: r.startCursor,
|
|
4742
|
+
endCursor: r.endCursor,
|
|
4743
|
+
truncated: r.truncated
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
async streamWaitFor(cursor, timeoutMs) {
|
|
4747
|
+
const buf = this.requireStreamBuffer();
|
|
4748
|
+
return buf.waitForData(cursor, timeoutMs);
|
|
4749
|
+
}
|
|
4750
|
+
streamGrep(opts) {
|
|
4751
|
+
const buf = this.requireStreamBuffer();
|
|
4752
|
+
return buf.grep(opts);
|
|
4753
|
+
}
|
|
4754
|
+
streamInfo() {
|
|
4755
|
+
const buf = this.requireStreamBuffer();
|
|
4756
|
+
return {
|
|
4757
|
+
writeCursor: buf.writeCursorPos,
|
|
4758
|
+
oldestAvailable: buf.oldestAvailable,
|
|
4759
|
+
capacity: buf.capacity,
|
|
4760
|
+
closed: buf.isClosed
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4535
4763
|
requireStreamBuffer() {
|
|
4536
4764
|
if (this.streamBuffer === void 0) {
|
|
4537
4765
|
const err = new Error(
|
|
@@ -5122,11 +5350,12 @@ function resolveVersion() {
|
|
|
5122
5350
|
}
|
|
5123
5351
|
return "0.0.0";
|
|
5124
5352
|
}
|
|
5125
|
-
var HYDRA_VERSION;
|
|
5353
|
+
var HYDRA_VERSION, HYDRA_CAT_CLIENT_NAME;
|
|
5126
5354
|
var init_hydra_version = __esm({
|
|
5127
5355
|
"src/core/hydra-version.ts"() {
|
|
5128
5356
|
"use strict";
|
|
5129
5357
|
HYDRA_VERSION = resolveVersion();
|
|
5358
|
+
HYDRA_CAT_CLIENT_NAME = "hydra-acp-cat";
|
|
5130
5359
|
}
|
|
5131
5360
|
});
|
|
5132
5361
|
|
|
@@ -5714,7 +5943,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
|
|
|
5714
5943
|
title: s.title,
|
|
5715
5944
|
importedFromMachine: s.importedFromMachine,
|
|
5716
5945
|
importedFromUpstreamSessionId: s.importedFromUpstreamSessionId,
|
|
5717
|
-
busy: s.busy
|
|
5946
|
+
busy: s.busy,
|
|
5947
|
+
originatingClient: s.originatingClient
|
|
5718
5948
|
}));
|
|
5719
5949
|
}
|
|
5720
5950
|
async function killSession(target, id, fetchImpl = fetch) {
|
|
@@ -6234,6 +6464,47 @@ var init_resilient_ws = __esm({
|
|
|
6234
6464
|
}
|
|
6235
6465
|
});
|
|
6236
6466
|
|
|
6467
|
+
// src/acp/permission-pick.ts
|
|
6468
|
+
function pickPermissionOptionId(params, preferredKinds) {
|
|
6469
|
+
const options = params && typeof params === "object" ? params.options : void 0;
|
|
6470
|
+
if (Array.isArray(options)) {
|
|
6471
|
+
for (const kind of preferredKinds) {
|
|
6472
|
+
const match = options.find(
|
|
6473
|
+
(o) => typeof o === "object" && o !== null && o.kind === kind && typeof o.optionId === "string"
|
|
6474
|
+
);
|
|
6475
|
+
if (match?.optionId !== void 0) {
|
|
6476
|
+
return match.optionId;
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
const fallback = options.find(
|
|
6480
|
+
(o) => typeof o === "object" && o !== null && typeof o.optionId === "string"
|
|
6481
|
+
);
|
|
6482
|
+
if (fallback?.optionId !== void 0) {
|
|
6483
|
+
return fallback.optionId;
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
return preferredKinds[0] ?? "allow";
|
|
6487
|
+
}
|
|
6488
|
+
function buildApproveResponse(params) {
|
|
6489
|
+
const optionId = pickPermissionOptionId(params, [
|
|
6490
|
+
"allow_once",
|
|
6491
|
+
"allow_always"
|
|
6492
|
+
]);
|
|
6493
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
6494
|
+
}
|
|
6495
|
+
function buildRejectResponse(params) {
|
|
6496
|
+
const optionId = pickPermissionOptionId(params, [
|
|
6497
|
+
"reject_once",
|
|
6498
|
+
"reject_always"
|
|
6499
|
+
]);
|
|
6500
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
6501
|
+
}
|
|
6502
|
+
var init_permission_pick = __esm({
|
|
6503
|
+
"src/acp/permission-pick.ts"() {
|
|
6504
|
+
"use strict";
|
|
6505
|
+
}
|
|
6506
|
+
});
|
|
6507
|
+
|
|
6237
6508
|
// src/core/update-check.ts
|
|
6238
6509
|
function disabled() {
|
|
6239
6510
|
if (process.env.NO_UPDATE_NOTIFIER === "1") {
|
|
@@ -10582,7 +10853,9 @@ var init_import_action_prompt = __esm({
|
|
|
10582
10853
|
|
|
10583
10854
|
// src/tui/picker.ts
|
|
10584
10855
|
function createPickerPrefs() {
|
|
10585
|
-
return {
|
|
10856
|
+
return {
|
|
10857
|
+
filters: { cwdOnly: false, hostFilter: "__local", showCat: false }
|
|
10858
|
+
};
|
|
10586
10859
|
}
|
|
10587
10860
|
async function pickSession(term, opts) {
|
|
10588
10861
|
process.stdout.write("\x1B[<u");
|
|
@@ -10624,6 +10897,11 @@ async function pickSession(term, opts) {
|
|
|
10624
10897
|
if (prefs.filters.cwdOnly) {
|
|
10625
10898
|
base = base.filter((s) => s.cwd === opts.cwd);
|
|
10626
10899
|
}
|
|
10900
|
+
if (!prefs.filters.showCat) {
|
|
10901
|
+
base = base.filter(
|
|
10902
|
+
(s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
|
|
10903
|
+
);
|
|
10904
|
+
}
|
|
10627
10905
|
base = filterByHost(base, prefs.filters.hostFilter);
|
|
10628
10906
|
return base;
|
|
10629
10907
|
};
|
|
@@ -10816,6 +11094,9 @@ async function pickSession(term, opts) {
|
|
|
10816
11094
|
prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
|
|
10817
11095
|
);
|
|
10818
11096
|
}
|
|
11097
|
+
if (prefs.filters.showCat) {
|
|
11098
|
+
parts.push("+cat");
|
|
11099
|
+
}
|
|
10819
11100
|
if (above > 0) {
|
|
10820
11101
|
parts.push(`\u2191 ${above} above`);
|
|
10821
11102
|
}
|
|
@@ -12012,6 +12293,14 @@ ${cells}`;
|
|
|
12012
12293
|
renderFromScratch();
|
|
12013
12294
|
return;
|
|
12014
12295
|
}
|
|
12296
|
+
if (name === "i" || name === "I") {
|
|
12297
|
+
const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
12298
|
+
prefs.filters.showCat = !prefs.filters.showCat;
|
|
12299
|
+
applyFilter();
|
|
12300
|
+
restoreCursorAfterFilter(keepId);
|
|
12301
|
+
renderFromScratch();
|
|
12302
|
+
return;
|
|
12303
|
+
}
|
|
12015
12304
|
if (name === "r" || name === "R") {
|
|
12016
12305
|
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
12017
12306
|
void refresh(currentId);
|
|
@@ -12238,6 +12527,7 @@ var init_picker = __esm({
|
|
|
12238
12527
|
init_session_row();
|
|
12239
12528
|
init_paths();
|
|
12240
12529
|
init_session();
|
|
12530
|
+
init_hydra_version();
|
|
12241
12531
|
init_discovery();
|
|
12242
12532
|
init_input();
|
|
12243
12533
|
init_screen();
|
|
@@ -12262,6 +12552,7 @@ var init_picker = __esm({
|
|
|
12262
12552
|
["^f", "find in session history (content + tool inputs)"],
|
|
12263
12553
|
["o", "toggle cwd-only filter"],
|
|
12264
12554
|
["h", "cycle host filter (local / <peer> / all)"],
|
|
12555
|
+
["i", "toggle include-cat filter"],
|
|
12265
12556
|
["r", "refresh from daemon"],
|
|
12266
12557
|
null,
|
|
12267
12558
|
["k", "kill the selected live session"],
|
|
@@ -13798,6 +14089,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
|
|
|
13798
14089
|
if (teardownStarted) {
|
|
13799
14090
|
return { outcome: { outcome: "cancelled" } };
|
|
13800
14091
|
}
|
|
14092
|
+
if (opts.dangerouslySkipPermissions) {
|
|
14093
|
+
return buildApproveResponse(params);
|
|
14094
|
+
}
|
|
13801
14095
|
const p = params ?? {};
|
|
13802
14096
|
const rawOptions = Array.isArray(p.options) ? p.options : [];
|
|
13803
14097
|
const options = rawOptions.map((o) => ({
|
|
@@ -15810,6 +16104,7 @@ var init_app = __esm({
|
|
|
15810
16104
|
init_session();
|
|
15811
16105
|
init_paths();
|
|
15812
16106
|
init_hydra_version();
|
|
16107
|
+
init_permission_pick();
|
|
15813
16108
|
init_update_check();
|
|
15814
16109
|
init_history();
|
|
15815
16110
|
init_discovery();
|
|
@@ -15878,12 +16173,14 @@ import { dirname as dirname6, resolve as resolve5 } from "path";
|
|
|
15878
16173
|
// src/cli/parse-args.ts
|
|
15879
16174
|
var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
15880
16175
|
"all",
|
|
16176
|
+
"dangerously-skip-permissions",
|
|
15881
16177
|
"detach",
|
|
15882
16178
|
"disabled",
|
|
15883
16179
|
"follow",
|
|
15884
16180
|
"force",
|
|
15885
16181
|
"foreground",
|
|
15886
16182
|
"help",
|
|
16183
|
+
"include-cat",
|
|
15887
16184
|
"info",
|
|
15888
16185
|
"json",
|
|
15889
16186
|
"new",
|
|
@@ -15891,7 +16188,6 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
15891
16188
|
"readonly",
|
|
15892
16189
|
"replace",
|
|
15893
16190
|
"rotate-token",
|
|
15894
|
-
"stream",
|
|
15895
16191
|
"version"
|
|
15896
16192
|
]);
|
|
15897
16193
|
var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -15907,7 +16203,6 @@ var KNOWN_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
15907
16203
|
"prompt",
|
|
15908
16204
|
"session",
|
|
15909
16205
|
"stream-bytes",
|
|
15910
|
-
"stream-file-cap",
|
|
15911
16206
|
"stream-threshold",
|
|
15912
16207
|
"tail"
|
|
15913
16208
|
]);
|
|
@@ -17065,6 +17360,10 @@ var PersistedUsage = z4.object({
|
|
|
17065
17360
|
costCurrency: z4.string().optional(),
|
|
17066
17361
|
cumulativeCost: z4.number().optional()
|
|
17067
17362
|
});
|
|
17363
|
+
var PersistedOriginatingClient = z4.object({
|
|
17364
|
+
name: z4.string(),
|
|
17365
|
+
version: z4.string().optional()
|
|
17366
|
+
});
|
|
17068
17367
|
var SessionRecord = z4.object({
|
|
17069
17368
|
version: z4.literal(1),
|
|
17070
17369
|
sessionId: z4.string(),
|
|
@@ -17115,6 +17414,10 @@ var SessionRecord = z4.object({
|
|
|
17115
17414
|
// Set when this session was spawned as a child by a transformer via
|
|
17116
17415
|
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
17117
17416
|
parentSessionId: z4.string().optional(),
|
|
17417
|
+
// clientInfo from the process that issued session/new. Picker and
|
|
17418
|
+
// `sessions list` use this to hide cat-style ancillary sessions by
|
|
17419
|
+
// default; carried in meta.json so cold sessions filter the same way.
|
|
17420
|
+
originatingClient: PersistedOriginatingClient.optional(),
|
|
17118
17421
|
createdAt: z4.string(),
|
|
17119
17422
|
updatedAt: z4.string()
|
|
17120
17423
|
});
|
|
@@ -17237,6 +17540,7 @@ function recordFromMemorySession(args) {
|
|
|
17237
17540
|
agentModels: args.agentModels,
|
|
17238
17541
|
pendingHistorySync: args.pendingHistorySync,
|
|
17239
17542
|
parentSessionId: args.parentSessionId,
|
|
17543
|
+
originatingClient: args.originatingClient,
|
|
17240
17544
|
createdAt: args.createdAt ?? now,
|
|
17241
17545
|
updatedAt: args.updatedAt ?? now
|
|
17242
17546
|
};
|
|
@@ -17495,6 +17799,7 @@ var SessionManager = class {
|
|
|
17495
17799
|
agentModels: fresh.initialModels,
|
|
17496
17800
|
transformChain: params.transformChain,
|
|
17497
17801
|
parentSessionId: params.parentSessionId,
|
|
17802
|
+
originatingClient: params.originatingClient,
|
|
17498
17803
|
extensionCommands: this.extensionCommands
|
|
17499
17804
|
});
|
|
17500
17805
|
await this.attachManagerHooks(session);
|
|
@@ -17666,6 +17971,7 @@ var SessionManager = class {
|
|
|
17666
17971
|
// than stay stuck.
|
|
17667
17972
|
firstPromptSeeded: !!params.title,
|
|
17668
17973
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
17974
|
+
originatingClient: params.originatingClient,
|
|
17669
17975
|
extensionCommands: this.extensionCommands
|
|
17670
17976
|
});
|
|
17671
17977
|
await this.attachManagerHooks(session);
|
|
@@ -17733,6 +18039,7 @@ var SessionManager = class {
|
|
|
17733
18039
|
agentModels: advertisedModels,
|
|
17734
18040
|
firstPromptSeeded: !!params.title,
|
|
17735
18041
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
18042
|
+
originatingClient: params.originatingClient,
|
|
17736
18043
|
extensionCommands: this.extensionCommands
|
|
17737
18044
|
});
|
|
17738
18045
|
await this.attachManagerHooks(session);
|
|
@@ -18082,7 +18389,8 @@ var SessionManager = class {
|
|
|
18082
18389
|
agentModes: record.agentModes,
|
|
18083
18390
|
agentModels: record.agentModels,
|
|
18084
18391
|
createdAt: record.createdAt,
|
|
18085
|
-
pendingHistorySync: record.pendingHistorySync
|
|
18392
|
+
pendingHistorySync: record.pendingHistorySync,
|
|
18393
|
+
originatingClient: record.originatingClient
|
|
18086
18394
|
};
|
|
18087
18395
|
}
|
|
18088
18396
|
async clearPendingHistorySync(sessionId) {
|
|
@@ -18183,6 +18491,7 @@ var SessionManager = class {
|
|
|
18183
18491
|
currentModel: session.currentModel,
|
|
18184
18492
|
currentUsage: session.currentUsage,
|
|
18185
18493
|
parentSessionId: session.parentSessionId,
|
|
18494
|
+
originatingClient: session.originatingClient,
|
|
18186
18495
|
updatedAt: used,
|
|
18187
18496
|
attachedClients: session.attachedCount,
|
|
18188
18497
|
status: "live",
|
|
@@ -18212,6 +18521,7 @@ var SessionManager = class {
|
|
|
18212
18521
|
importedFromMachine: r.importedFromMachine,
|
|
18213
18522
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
18214
18523
|
parentSessionId: r.parentSessionId,
|
|
18524
|
+
originatingClient: r.originatingClient,
|
|
18215
18525
|
updatedAt: used,
|
|
18216
18526
|
attachedClients: 0,
|
|
18217
18527
|
status: "cold",
|
|
@@ -18557,6 +18867,7 @@ function mergeForPersistence(session, existing) {
|
|
|
18557
18867
|
agentModes,
|
|
18558
18868
|
agentModels,
|
|
18559
18869
|
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
18870
|
+
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
18560
18871
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
18561
18872
|
});
|
|
18562
18873
|
}
|
|
@@ -21705,6 +22016,7 @@ import { nanoid as nanoid2 } from "nanoid";
|
|
|
21705
22016
|
import * as os5 from "os";
|
|
21706
22017
|
import * as path13 from "path";
|
|
21707
22018
|
init_hydra_version();
|
|
22019
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
21708
22020
|
function registerAcpWsEndpoint(app, deps) {
|
|
21709
22021
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
21710
22022
|
const token = tokenFromUpgradeRequest({
|
|
@@ -21742,6 +22054,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
21742
22054
|
};
|
|
21743
22055
|
connection.onRequest("initialize", async (raw) => {
|
|
21744
22056
|
const params = InitializeParams.parse(raw ?? {});
|
|
22057
|
+
if (params.clientInfo?.name) {
|
|
22058
|
+
state.clientInfo = {
|
|
22059
|
+
name: params.clientInfo.name,
|
|
22060
|
+
...params.clientInfo.version !== void 0 ? { version: params.clientInfo.version } : {}
|
|
22061
|
+
};
|
|
22062
|
+
}
|
|
21745
22063
|
const version = params.clientInfo?.version;
|
|
21746
22064
|
if (version && processIdentity) {
|
|
21747
22065
|
if (processIdentity.kind === "extension") {
|
|
@@ -21921,16 +22239,50 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
21921
22239
|
);
|
|
21922
22240
|
const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
|
|
21923
22241
|
const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
|
|
21924
|
-
|
|
21925
|
-
|
|
21926
|
-
|
|
21927
|
-
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
22242
|
+
let stdinToken;
|
|
22243
|
+
let stdinReservation;
|
|
22244
|
+
let augmentedMcpServers = params.mcpServers;
|
|
22245
|
+
if (hydraMeta.mcpStdin === true && deps.stdinMcpRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
22246
|
+
stdinToken = randomBytes3(32).toString("hex");
|
|
22247
|
+
stdinReservation = deps.stdinMcpRegistry.reserve(stdinToken);
|
|
22248
|
+
const url = `${deps.getDaemonOrigin()}/mcp/stdin`;
|
|
22249
|
+
const descriptor = {
|
|
22250
|
+
name: "hydra_stdin",
|
|
22251
|
+
type: "http",
|
|
22252
|
+
url,
|
|
22253
|
+
headers: [
|
|
22254
|
+
{ name: "Authorization", value: `Bearer ${stdinToken}` }
|
|
22255
|
+
]
|
|
22256
|
+
};
|
|
22257
|
+
augmentedMcpServers = [...params.mcpServers ?? [], descriptor];
|
|
22258
|
+
}
|
|
22259
|
+
let session;
|
|
22260
|
+
try {
|
|
22261
|
+
session = await deps.manager.create({
|
|
22262
|
+
cwd: params.cwd,
|
|
22263
|
+
agentId: params.agentId ?? deps.defaultAgent,
|
|
22264
|
+
mcpServers: augmentedMcpServers,
|
|
22265
|
+
title: hydraMeta.name,
|
|
22266
|
+
agentArgs: hydraMeta.agentArgs,
|
|
22267
|
+
model: hydraMeta.model,
|
|
22268
|
+
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
22269
|
+
transformChain,
|
|
22270
|
+
originatingClient: state.clientInfo
|
|
22271
|
+
});
|
|
22272
|
+
} catch (err) {
|
|
22273
|
+
if (stdinReservation !== void 0) {
|
|
22274
|
+
stdinReservation.abandon(err instanceof Error ? err : void 0);
|
|
22275
|
+
}
|
|
22276
|
+
throw err;
|
|
22277
|
+
}
|
|
22278
|
+
if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.stdinMcpRegistry !== void 0) {
|
|
22279
|
+
const token2 = stdinToken;
|
|
22280
|
+
const registry = deps.stdinMcpRegistry;
|
|
22281
|
+
stdinReservation.complete(session);
|
|
22282
|
+
session.onClose(() => {
|
|
22283
|
+
void registry.unbind(token2);
|
|
22284
|
+
});
|
|
22285
|
+
}
|
|
21934
22286
|
const client = bindClientToSession(connection, session, state);
|
|
21935
22287
|
const { entries: replay } = await session.attach(client, "full");
|
|
21936
22288
|
state.attached.set(session.sessionId, {
|
|
@@ -22651,6 +23003,336 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
22651
23003
|
};
|
|
22652
23004
|
}
|
|
22653
23005
|
|
|
23006
|
+
// src/daemon/mcp/stdin-registry.ts
|
|
23007
|
+
var StdinMcpRegistry = class {
|
|
23008
|
+
byToken = /* @__PURE__ */ new Map();
|
|
23009
|
+
// Reserve a token slot before the session exists. Used by acp-ws when
|
|
23010
|
+
// we need to inject the bearer into the agent's mcpServers BEFORE
|
|
23011
|
+
// manager.create() returns — claude-acp connects to /mcp/stdin during
|
|
23012
|
+
// session/new initialization (eagerly), so the route handler must be
|
|
23013
|
+
// able to find the token by the time the agent's first request lands.
|
|
23014
|
+
reserve(token) {
|
|
23015
|
+
if (this.byToken.has(token)) {
|
|
23016
|
+
throw new Error(`stdin MCP token already bound`);
|
|
23017
|
+
}
|
|
23018
|
+
let resolveSession2;
|
|
23019
|
+
let rejectSession;
|
|
23020
|
+
const sessionReady = new Promise((resolve6, reject) => {
|
|
23021
|
+
resolveSession2 = resolve6;
|
|
23022
|
+
rejectSession = reject;
|
|
23023
|
+
});
|
|
23024
|
+
sessionReady.catch(() => void 0);
|
|
23025
|
+
const entry = { session: void 0, sessionReady };
|
|
23026
|
+
this.byToken.set(token, entry);
|
|
23027
|
+
return {
|
|
23028
|
+
complete: (session) => {
|
|
23029
|
+
entry.session = session;
|
|
23030
|
+
resolveSession2(session);
|
|
23031
|
+
},
|
|
23032
|
+
abandon: (reason) => {
|
|
23033
|
+
this.byToken.delete(token);
|
|
23034
|
+
rejectSession(reason ?? new Error("stdin MCP reservation abandoned"));
|
|
23035
|
+
}
|
|
23036
|
+
};
|
|
23037
|
+
}
|
|
23038
|
+
// Convenience for callers that already have the session in hand (and
|
|
23039
|
+
// for tests). Equivalent to reserve() + complete() back-to-back.
|
|
23040
|
+
bind(token, session) {
|
|
23041
|
+
const { complete } = this.reserve(token);
|
|
23042
|
+
complete(session);
|
|
23043
|
+
}
|
|
23044
|
+
lookup(token) {
|
|
23045
|
+
return this.byToken.get(token);
|
|
23046
|
+
}
|
|
23047
|
+
attachTransport(token, server, transport) {
|
|
23048
|
+
const ep = this.byToken.get(token);
|
|
23049
|
+
if (!ep) {
|
|
23050
|
+
return;
|
|
23051
|
+
}
|
|
23052
|
+
ep.server = server;
|
|
23053
|
+
ep.transport = transport;
|
|
23054
|
+
}
|
|
23055
|
+
async unbind(token) {
|
|
23056
|
+
const ep = this.byToken.get(token);
|
|
23057
|
+
if (!ep) {
|
|
23058
|
+
return;
|
|
23059
|
+
}
|
|
23060
|
+
this.byToken.delete(token);
|
|
23061
|
+
if (ep.transport) {
|
|
23062
|
+
try {
|
|
23063
|
+
await ep.transport.close();
|
|
23064
|
+
} catch {
|
|
23065
|
+
}
|
|
23066
|
+
}
|
|
23067
|
+
if (ep.server) {
|
|
23068
|
+
try {
|
|
23069
|
+
await ep.server.close();
|
|
23070
|
+
} catch {
|
|
23071
|
+
}
|
|
23072
|
+
}
|
|
23073
|
+
}
|
|
23074
|
+
size() {
|
|
23075
|
+
return this.byToken.size;
|
|
23076
|
+
}
|
|
23077
|
+
};
|
|
23078
|
+
|
|
23079
|
+
// src/daemon/mcp/stdin-server.ts
|
|
23080
|
+
import { randomUUID } from "crypto";
|
|
23081
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23082
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23083
|
+
import { z as z7 } from "zod";
|
|
23084
|
+
var BEARER_PREFIX2 = "Bearer ";
|
|
23085
|
+
function extractBearer(req) {
|
|
23086
|
+
const header = req.headers.authorization;
|
|
23087
|
+
if (typeof header !== "string") {
|
|
23088
|
+
return void 0;
|
|
23089
|
+
}
|
|
23090
|
+
if (!header.startsWith(BEARER_PREFIX2)) {
|
|
23091
|
+
return void 0;
|
|
23092
|
+
}
|
|
23093
|
+
const token = header.slice(BEARER_PREFIX2.length).trim();
|
|
23094
|
+
return token.length > 0 ? token : void 0;
|
|
23095
|
+
}
|
|
23096
|
+
function buildMcpServer(session) {
|
|
23097
|
+
const server = new McpServer(
|
|
23098
|
+
{ name: "hydra-stdin", version: "1.0.0" },
|
|
23099
|
+
{
|
|
23100
|
+
instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `tail_stdin` for the latest N bytes (good for finding the end of a log), `head_stdin` for the first N bytes (good for headers/preamble), `read_stdin` for windowed reads against an absolute byte cursor, `wait_for_more` to block until new bytes arrive past a cursor, and `stdin_info` for the current cursors/capacity/closed status. Byte payloads come back base64-encoded."
|
|
23101
|
+
}
|
|
23102
|
+
);
|
|
23103
|
+
server.registerTool(
|
|
23104
|
+
"tail_stdin",
|
|
23105
|
+
{
|
|
23106
|
+
description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
|
|
23107
|
+
inputSchema: {
|
|
23108
|
+
bytes: z7.number().int().min(1).describe("How many trailing bytes to return.")
|
|
23109
|
+
}
|
|
23110
|
+
},
|
|
23111
|
+
async ({ bytes }) => {
|
|
23112
|
+
const r = session.streamTail(bytes);
|
|
23113
|
+
return {
|
|
23114
|
+
content: [
|
|
23115
|
+
{
|
|
23116
|
+
type: "text",
|
|
23117
|
+
text: JSON.stringify(r)
|
|
23118
|
+
}
|
|
23119
|
+
],
|
|
23120
|
+
structuredContent: r
|
|
23121
|
+
};
|
|
23122
|
+
}
|
|
23123
|
+
);
|
|
23124
|
+
server.registerTool(
|
|
23125
|
+
"head_stdin",
|
|
23126
|
+
{
|
|
23127
|
+
description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
|
|
23128
|
+
inputSchema: {
|
|
23129
|
+
bytes: z7.number().int().min(1).describe("How many leading bytes to return.")
|
|
23130
|
+
}
|
|
23131
|
+
},
|
|
23132
|
+
async ({ bytes }) => {
|
|
23133
|
+
const r = session.streamHead(bytes);
|
|
23134
|
+
return {
|
|
23135
|
+
content: [
|
|
23136
|
+
{
|
|
23137
|
+
type: "text",
|
|
23138
|
+
text: JSON.stringify(r)
|
|
23139
|
+
}
|
|
23140
|
+
],
|
|
23141
|
+
structuredContent: r
|
|
23142
|
+
};
|
|
23143
|
+
}
|
|
23144
|
+
);
|
|
23145
|
+
server.registerTool(
|
|
23146
|
+
"read_stdin",
|
|
23147
|
+
{
|
|
23148
|
+
description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
|
|
23149
|
+
inputSchema: {
|
|
23150
|
+
cursor: z7.number().int().min(0).describe(
|
|
23151
|
+
"Absolute byte offset to start reading from. Use 0 to read from the very beginning (may produce a gap if old bytes have been evicted)."
|
|
23152
|
+
),
|
|
23153
|
+
max_bytes: z7.number().int().min(1).optional().describe(
|
|
23154
|
+
"Optional cap on how many bytes to return. Server caps at 64 KiB regardless."
|
|
23155
|
+
),
|
|
23156
|
+
wait_ms: z7.number().int().min(0).optional().describe(
|
|
23157
|
+
"If no bytes are available, block up to this many ms for more (capped server-side at 60_000)."
|
|
23158
|
+
)
|
|
23159
|
+
}
|
|
23160
|
+
},
|
|
23161
|
+
async ({ cursor, max_bytes, wait_ms }) => {
|
|
23162
|
+
const r = await session.streamRead(cursor, max_bytes, wait_ms);
|
|
23163
|
+
return {
|
|
23164
|
+
content: [
|
|
23165
|
+
{
|
|
23166
|
+
type: "text",
|
|
23167
|
+
text: JSON.stringify(r)
|
|
23168
|
+
}
|
|
23169
|
+
],
|
|
23170
|
+
structuredContent: r
|
|
23171
|
+
};
|
|
23172
|
+
}
|
|
23173
|
+
);
|
|
23174
|
+
server.registerTool(
|
|
23175
|
+
"wait_for_more",
|
|
23176
|
+
{
|
|
23177
|
+
description: "Block until bytes are available past `cursor`, the stream closes, or `timeout_ms` elapses. Returns one of {data, eof, timeout} plus the current `writeCursor`. Use this when you've consumed everything up to a cursor and want to wait for more without busy-polling.",
|
|
23178
|
+
inputSchema: {
|
|
23179
|
+
cursor: z7.number().int().min(0).describe("The cursor you've already consumed up to."),
|
|
23180
|
+
timeout_ms: z7.number().int().min(0).describe("Maximum ms to block (server caps at 60_000).")
|
|
23181
|
+
}
|
|
23182
|
+
},
|
|
23183
|
+
async ({ cursor, timeout_ms }) => {
|
|
23184
|
+
const outcome = await session.streamWaitFor(cursor, timeout_ms);
|
|
23185
|
+
const info = session.streamInfo();
|
|
23186
|
+
const payload = { outcome, writeCursor: info.writeCursor, closed: info.closed };
|
|
23187
|
+
return {
|
|
23188
|
+
content: [
|
|
23189
|
+
{
|
|
23190
|
+
type: "text",
|
|
23191
|
+
text: JSON.stringify(payload)
|
|
23192
|
+
}
|
|
23193
|
+
],
|
|
23194
|
+
structuredContent: payload
|
|
23195
|
+
};
|
|
23196
|
+
}
|
|
23197
|
+
);
|
|
23198
|
+
server.registerTool(
|
|
23199
|
+
"grep_stdin",
|
|
23200
|
+
{
|
|
23201
|
+
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read_stdin` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
|
|
23202
|
+
inputSchema: {
|
|
23203
|
+
pattern: z7.string().min(1).describe(
|
|
23204
|
+
"Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
|
|
23205
|
+
),
|
|
23206
|
+
regex: z7.boolean().optional().describe("Default true. Pass false to treat `pattern` as a literal substring."),
|
|
23207
|
+
case_insensitive: z7.boolean().optional().describe("Default false. Pass true for case-insensitive matching."),
|
|
23208
|
+
invert: z7.boolean().optional().describe("Default false. Pass true to return lines that do NOT match the pattern."),
|
|
23209
|
+
max_matches: z7.number().int().min(1).optional().describe("Default 100. Capped server-side at 1000."),
|
|
23210
|
+
max_bytes: z7.number().int().min(1).optional().describe("Default 64 KiB output. Capped server-side at 256 KiB."),
|
|
23211
|
+
context_before: z7.number().int().min(0).optional().describe("Default 0. Number of lines before each match to include (capped at 20)."),
|
|
23212
|
+
context_after: z7.number().int().min(0).optional().describe("Default 0. Number of lines after each match to include (capped at 20)."),
|
|
23213
|
+
cursor: z7.number().int().min(0).optional().describe(
|
|
23214
|
+
"Optional absolute byte offset to start scanning from. Omit to scan from the oldest still-resident byte. Pass the `nextCursor` from a previous truncated call to resume."
|
|
23215
|
+
)
|
|
23216
|
+
}
|
|
23217
|
+
},
|
|
23218
|
+
async (args) => {
|
|
23219
|
+
const opts = { pattern: args.pattern };
|
|
23220
|
+
if (args.regex !== void 0) {
|
|
23221
|
+
opts.regex = args.regex;
|
|
23222
|
+
}
|
|
23223
|
+
if (args.case_insensitive !== void 0) {
|
|
23224
|
+
opts.caseInsensitive = args.case_insensitive;
|
|
23225
|
+
}
|
|
23226
|
+
if (args.invert !== void 0) {
|
|
23227
|
+
opts.invert = args.invert;
|
|
23228
|
+
}
|
|
23229
|
+
if (args.max_matches !== void 0) {
|
|
23230
|
+
opts.maxMatches = args.max_matches;
|
|
23231
|
+
}
|
|
23232
|
+
if (args.max_bytes !== void 0) {
|
|
23233
|
+
opts.maxBytes = args.max_bytes;
|
|
23234
|
+
}
|
|
23235
|
+
if (args.context_before !== void 0) {
|
|
23236
|
+
opts.contextBefore = args.context_before;
|
|
23237
|
+
}
|
|
23238
|
+
if (args.context_after !== void 0) {
|
|
23239
|
+
opts.contextAfter = args.context_after;
|
|
23240
|
+
}
|
|
23241
|
+
if (args.cursor !== void 0) {
|
|
23242
|
+
opts.cursor = args.cursor;
|
|
23243
|
+
}
|
|
23244
|
+
const r = session.streamGrep(opts);
|
|
23245
|
+
const payload = r;
|
|
23246
|
+
return {
|
|
23247
|
+
content: [{ type: "text", text: JSON.stringify(r) }],
|
|
23248
|
+
structuredContent: payload
|
|
23249
|
+
};
|
|
23250
|
+
}
|
|
23251
|
+
);
|
|
23252
|
+
server.registerTool(
|
|
23253
|
+
"stdin_info",
|
|
23254
|
+
{
|
|
23255
|
+
description: "Report cursor / capacity / closed state of the stdin ring. Cheap; safe to call repeatedly.",
|
|
23256
|
+
inputSchema: {}
|
|
23257
|
+
},
|
|
23258
|
+
async () => {
|
|
23259
|
+
const r = session.streamInfo();
|
|
23260
|
+
return {
|
|
23261
|
+
content: [
|
|
23262
|
+
{
|
|
23263
|
+
type: "text",
|
|
23264
|
+
text: JSON.stringify(r)
|
|
23265
|
+
}
|
|
23266
|
+
],
|
|
23267
|
+
structuredContent: r
|
|
23268
|
+
};
|
|
23269
|
+
}
|
|
23270
|
+
);
|
|
23271
|
+
return server;
|
|
23272
|
+
}
|
|
23273
|
+
async function ensureTransport(token, session, registry) {
|
|
23274
|
+
const existing = registry.lookup(token);
|
|
23275
|
+
if (existing?.transport !== void 0) {
|
|
23276
|
+
return existing.transport;
|
|
23277
|
+
}
|
|
23278
|
+
const server = buildMcpServer(session);
|
|
23279
|
+
const transport = new StreamableHTTPServerTransport({
|
|
23280
|
+
sessionIdGenerator: () => randomUUID()
|
|
23281
|
+
});
|
|
23282
|
+
await server.connect(transport);
|
|
23283
|
+
registry.attachTransport(token, server, transport);
|
|
23284
|
+
return transport;
|
|
23285
|
+
}
|
|
23286
|
+
var SESSION_READY_TIMEOUT_MS = 1e4;
|
|
23287
|
+
async function handle(req, reply, registry) {
|
|
23288
|
+
const token = extractBearer(req);
|
|
23289
|
+
if (token === void 0) {
|
|
23290
|
+
reply.code(401).send({ error: "missing bearer token" });
|
|
23291
|
+
return;
|
|
23292
|
+
}
|
|
23293
|
+
const ep = registry.lookup(token);
|
|
23294
|
+
if (ep === void 0) {
|
|
23295
|
+
reply.code(404).send({ error: "unknown stdin token" });
|
|
23296
|
+
return;
|
|
23297
|
+
}
|
|
23298
|
+
let session;
|
|
23299
|
+
if (ep.session !== void 0) {
|
|
23300
|
+
session = ep.session;
|
|
23301
|
+
} else {
|
|
23302
|
+
let timer;
|
|
23303
|
+
const timeout = new Promise((resolve6) => {
|
|
23304
|
+
timer = setTimeout(() => resolve6(void 0), SESSION_READY_TIMEOUT_MS);
|
|
23305
|
+
});
|
|
23306
|
+
const resolved = await Promise.race([
|
|
23307
|
+
ep.sessionReady.catch(() => void 0),
|
|
23308
|
+
timeout
|
|
23309
|
+
]);
|
|
23310
|
+
if (timer !== void 0) {
|
|
23311
|
+
clearTimeout(timer);
|
|
23312
|
+
}
|
|
23313
|
+
if (resolved === void 0) {
|
|
23314
|
+
reply.code(503).send({ error: "session not ready" });
|
|
23315
|
+
return;
|
|
23316
|
+
}
|
|
23317
|
+
session = resolved;
|
|
23318
|
+
}
|
|
23319
|
+
const transport = await ensureTransport(token, session, registry);
|
|
23320
|
+
reply.hijack();
|
|
23321
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
23322
|
+
}
|
|
23323
|
+
function registerStdinMcpRoutes(app, registry) {
|
|
23324
|
+
const opts = { config: { skipAuth: true } };
|
|
23325
|
+
app.post("/mcp/stdin", opts, async (req, reply) => {
|
|
23326
|
+
await handle(req, reply, registry);
|
|
23327
|
+
});
|
|
23328
|
+
app.get("/mcp/stdin", opts, async (req, reply) => {
|
|
23329
|
+
await handle(req, reply, registry);
|
|
23330
|
+
});
|
|
23331
|
+
app.delete("/mcp/stdin", opts, async (req, reply) => {
|
|
23332
|
+
await handle(req, reply, registry);
|
|
23333
|
+
});
|
|
23334
|
+
}
|
|
23335
|
+
|
|
22654
23336
|
// src/daemon/server.ts
|
|
22655
23337
|
async function startDaemon(config, serviceToken) {
|
|
22656
23338
|
ensureLoopbackOrTls(config);
|
|
@@ -22756,6 +23438,19 @@ async function startDaemon(config, serviceToken) {
|
|
|
22756
23438
|
store: sessionTokenStore,
|
|
22757
23439
|
rateLimiter: authRateLimiter
|
|
22758
23440
|
});
|
|
23441
|
+
const stdinMcpRegistry = new StdinMcpRegistry();
|
|
23442
|
+
registerStdinMcpRoutes(app, stdinMcpRegistry);
|
|
23443
|
+
let daemonOriginCached;
|
|
23444
|
+
const getDaemonOrigin = () => {
|
|
23445
|
+
if (daemonOriginCached !== void 0) {
|
|
23446
|
+
return daemonOriginCached;
|
|
23447
|
+
}
|
|
23448
|
+
const addr = app.server.address();
|
|
23449
|
+
const port = addr && typeof addr === "object" ? addr.port : config.daemon.port;
|
|
23450
|
+
const scheme2 = config.daemon.tls ? "https" : "http";
|
|
23451
|
+
daemonOriginCached = `${scheme2}://${config.daemon.host}:${port}`;
|
|
23452
|
+
return daemonOriginCached;
|
|
23453
|
+
};
|
|
22759
23454
|
registerAcpWsEndpoint(app, {
|
|
22760
23455
|
validator,
|
|
22761
23456
|
manager,
|
|
@@ -22764,7 +23459,9 @@ async function startDaemon(config, serviceToken) {
|
|
|
22764
23459
|
onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
|
|
22765
23460
|
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
22766
23461
|
transformers,
|
|
22767
|
-
extensionCommands
|
|
23462
|
+
extensionCommands,
|
|
23463
|
+
stdinMcpRegistry,
|
|
23464
|
+
getDaemonOrigin
|
|
22768
23465
|
});
|
|
22769
23466
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
22770
23467
|
const address = app.server.address();
|
|
@@ -23020,14 +23717,14 @@ async function runDaemonStart(flags = {}) {
|
|
|
23020
23717
|
}
|
|
23021
23718
|
if (flagBool(flags, "foreground")) {
|
|
23022
23719
|
process.title = "hydra-daemon";
|
|
23023
|
-
const
|
|
23720
|
+
const handle2 = await startDaemon(config, serviceToken);
|
|
23024
23721
|
process.stdout.write(
|
|
23025
23722
|
`hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
|
|
23026
23723
|
`
|
|
23027
23724
|
);
|
|
23028
23725
|
const shutdown = async () => {
|
|
23029
23726
|
process.stdout.write("Shutting down...\n");
|
|
23030
|
-
await
|
|
23727
|
+
await handle2.shutdown();
|
|
23031
23728
|
process.exit(0);
|
|
23032
23729
|
};
|
|
23033
23730
|
process.on("SIGINT", () => void shutdown());
|
|
@@ -23197,6 +23894,7 @@ init_remote_target();
|
|
|
23197
23894
|
init_remote_url();
|
|
23198
23895
|
init_session();
|
|
23199
23896
|
init_discovery();
|
|
23897
|
+
init_hydra_version();
|
|
23200
23898
|
import * as fs19 from "fs/promises";
|
|
23201
23899
|
import * as path14 from "path";
|
|
23202
23900
|
init_session_row();
|
|
@@ -23214,10 +23912,13 @@ async function runSessionsList(opts = {}) {
|
|
|
23214
23912
|
process.exit(1);
|
|
23215
23913
|
}
|
|
23216
23914
|
const body = await response.json();
|
|
23915
|
+
const sessionsAfterCatFilter = opts.includeCat ? body.sessions : body.sessions.filter(
|
|
23916
|
+
(s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
|
|
23917
|
+
);
|
|
23217
23918
|
const host = opts.host ?? "local";
|
|
23218
|
-
const hostFiltered = host === "all" ?
|
|
23919
|
+
const hostFiltered = host === "all" ? sessionsAfterCatFilter : host === "local" ? sessionsAfterCatFilter.filter(
|
|
23219
23920
|
(s) => !s.importedFromMachine || !!s.upstreamSessionId
|
|
23220
|
-
) :
|
|
23921
|
+
) : sessionsAfterCatFilter.filter(
|
|
23221
23922
|
(s) => s.importedFromMachine === host && !s.upstreamSessionId
|
|
23222
23923
|
);
|
|
23223
23924
|
if (opts.json) {
|
|
@@ -25068,6 +25769,9 @@ function isResponse2(msg) {
|
|
|
25068
25769
|
return !("method" in msg) && "id" in msg;
|
|
25069
25770
|
}
|
|
25070
25771
|
|
|
25772
|
+
// src/shim/proxy.ts
|
|
25773
|
+
init_permission_pick();
|
|
25774
|
+
|
|
25071
25775
|
// src/core/process-title.ts
|
|
25072
25776
|
init_bin_name();
|
|
25073
25777
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -25152,6 +25856,14 @@ function wireShim({
|
|
|
25152
25856
|
}) {
|
|
25153
25857
|
upstream.onMessage((msg) => {
|
|
25154
25858
|
tracker.observeFromServer(msg);
|
|
25859
|
+
if (opts.dangerouslySkipPermissions === true && isPermissionRequest(msg)) {
|
|
25860
|
+
void upstream.send({
|
|
25861
|
+
jsonrpc: "2.0",
|
|
25862
|
+
id: msg.id,
|
|
25863
|
+
result: buildApproveResponse(msg.params)
|
|
25864
|
+
});
|
|
25865
|
+
return;
|
|
25866
|
+
}
|
|
25155
25867
|
maybeReplyToResolvedPermission(msg, tracker, downstream);
|
|
25156
25868
|
void downstream.send(msg);
|
|
25157
25869
|
});
|
|
@@ -25301,6 +26013,9 @@ async function replayAttach(stream, ctx, afterMessageId) {
|
|
|
25301
26013
|
function isSessionNewRequest(msg) {
|
|
25302
26014
|
return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/new";
|
|
25303
26015
|
}
|
|
26016
|
+
function isPermissionRequest(msg) {
|
|
26017
|
+
return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/request_permission";
|
|
26018
|
+
}
|
|
25304
26019
|
function buildAttachFromNew(msg, sessionId) {
|
|
25305
26020
|
return {
|
|
25306
26021
|
jsonrpc: "2.0",
|
|
@@ -25347,6 +26062,10 @@ init_daemon_bootstrap();
|
|
|
25347
26062
|
init_render_update();
|
|
25348
26063
|
init_types();
|
|
25349
26064
|
init_hydra_version();
|
|
26065
|
+
init_permission_pick();
|
|
26066
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
26067
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
26068
|
+
import { join as join11 } from "path";
|
|
25350
26069
|
import { WebSocket as WebSocket2 } from "ws";
|
|
25351
26070
|
|
|
25352
26071
|
// src/cli/commands/cat-chunker.ts
|
|
@@ -25401,8 +26120,26 @@ function createChunker(opts) {
|
|
|
25401
26120
|
}
|
|
25402
26121
|
|
|
25403
26122
|
// src/cli/commands/cat.ts
|
|
25404
|
-
var DEFAULT_STREAM_THRESHOLD =
|
|
25405
|
-
var
|
|
26123
|
+
var DEFAULT_STREAM_THRESHOLD = 1 * 1024 * 1024;
|
|
26124
|
+
var HYDRA_STDIN_TOOL_PREFIX = "mcp__hydra_stdin__";
|
|
26125
|
+
function isHydraStdinPermissionRequest(params) {
|
|
26126
|
+
if (!params || typeof params !== "object") {
|
|
26127
|
+
return false;
|
|
26128
|
+
}
|
|
26129
|
+
const toolCall = params.toolCall;
|
|
26130
|
+
if (!toolCall || typeof toolCall !== "object") {
|
|
26131
|
+
return false;
|
|
26132
|
+
}
|
|
26133
|
+
const title = toolCall.title;
|
|
26134
|
+
if (typeof title === "string" && title.startsWith(HYDRA_STDIN_TOOL_PREFIX)) {
|
|
26135
|
+
return true;
|
|
26136
|
+
}
|
|
26137
|
+
const toolName = toolCall.toolName;
|
|
26138
|
+
if (typeof toolName === "string" && toolName.startsWith(HYDRA_STDIN_TOOL_PREFIX)) {
|
|
26139
|
+
return true;
|
|
26140
|
+
}
|
|
26141
|
+
return false;
|
|
26142
|
+
}
|
|
25406
26143
|
async function runCat(opts) {
|
|
25407
26144
|
setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
|
|
25408
26145
|
if (process.stdin.isTTY && !opts.prompt && !opts.sessionId) {
|
|
@@ -25412,6 +26149,18 @@ async function runCat(opts) {
|
|
|
25412
26149
|
process.exit(2);
|
|
25413
26150
|
return;
|
|
25414
26151
|
}
|
|
26152
|
+
if (!opts.sessionId && opts.cwd === void 0 && process.stdin.isTTY !== true) {
|
|
26153
|
+
const sandbox = mkdtempSync(join11(tmpdir2(), "hydra-cat-"));
|
|
26154
|
+
opts.cwd = sandbox;
|
|
26155
|
+
if (!opts.detach) {
|
|
26156
|
+
process.on("exit", () => {
|
|
26157
|
+
try {
|
|
26158
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
26159
|
+
} catch {
|
|
26160
|
+
}
|
|
26161
|
+
});
|
|
26162
|
+
}
|
|
26163
|
+
}
|
|
25415
26164
|
const config = await loadConfig();
|
|
25416
26165
|
const target = opts.target ?? await resolveLocalTarget(config);
|
|
25417
26166
|
if (target.isLocal && !opts.target) {
|
|
@@ -25435,9 +26184,19 @@ async function runCat(opts) {
|
|
|
25435
26184
|
}
|
|
25436
26185
|
async function runCatLoop(args) {
|
|
25437
26186
|
const { conn, opts, stdin, stdinIsTty, stdout, stderr } = args;
|
|
26187
|
+
const useAutoStream = !stdinIsTty && opts.sessionId === void 0 && opts.follow !== true;
|
|
25438
26188
|
conn.setDefaultHandler(async () => {
|
|
25439
26189
|
return { error: { code: -32601, message: "method not implemented" } };
|
|
25440
26190
|
});
|
|
26191
|
+
conn.onRequest("session/request_permission", async (params) => {
|
|
26192
|
+
if (opts.dangerouslySkipPermissions) {
|
|
26193
|
+
return buildApproveResponse(params);
|
|
26194
|
+
}
|
|
26195
|
+
if (!isHydraStdinPermissionRequest(params)) {
|
|
26196
|
+
return buildRejectResponse(params);
|
|
26197
|
+
}
|
|
26198
|
+
return buildApproveResponse(params);
|
|
26199
|
+
});
|
|
25441
26200
|
try {
|
|
25442
26201
|
await conn.request("initialize", {
|
|
25443
26202
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
@@ -25445,11 +26204,11 @@ async function runCatLoop(args) {
|
|
|
25445
26204
|
fs: { readTextFile: false, writeTextFile: false },
|
|
25446
26205
|
terminal: false
|
|
25447
26206
|
},
|
|
25448
|
-
clientInfo: { name:
|
|
26207
|
+
clientInfo: { name: HYDRA_CAT_CLIENT_NAME, version: HYDRA_VERSION }
|
|
25449
26208
|
});
|
|
25450
26209
|
} catch {
|
|
25451
26210
|
}
|
|
25452
|
-
const sessionId = await openOrAttachSession(conn, opts);
|
|
26211
|
+
const sessionId = await openOrAttachSession(conn, opts, useAutoStream);
|
|
25453
26212
|
let turnHadOutput = false;
|
|
25454
26213
|
let lastCharWasNewline = true;
|
|
25455
26214
|
const writeStdout = (text) => {
|
|
@@ -25478,9 +26237,10 @@ async function runCatLoop(args) {
|
|
|
25478
26237
|
finalizeTurn();
|
|
25479
26238
|
}
|
|
25480
26239
|
});
|
|
26240
|
+
let firstChunkSent = false;
|
|
25481
26241
|
const sendChunk = async (text) => {
|
|
25482
26242
|
const promptBlocks = [];
|
|
25483
|
-
if (opts.prompt) {
|
|
26243
|
+
if (opts.prompt && !firstChunkSent) {
|
|
25484
26244
|
promptBlocks.push({ type: "text", text: opts.prompt });
|
|
25485
26245
|
}
|
|
25486
26246
|
if (text.length > 0) {
|
|
@@ -25494,6 +26254,7 @@ async function runCatLoop(args) {
|
|
|
25494
26254
|
sessionId,
|
|
25495
26255
|
prompt: promptBlocks
|
|
25496
26256
|
});
|
|
26257
|
+
firstChunkSent = true;
|
|
25497
26258
|
} catch (err) {
|
|
25498
26259
|
stderr(`hydra-acp cat: prompt failed: ${err.message}
|
|
25499
26260
|
`);
|
|
@@ -25552,22 +26313,6 @@ async function runCatLoop(args) {
|
|
|
25552
26313
|
}
|
|
25553
26314
|
}
|
|
25554
26315
|
};
|
|
25555
|
-
const chunker = createChunker({
|
|
25556
|
-
// setImmediate fires in the libuv "check" phase, after pending
|
|
25557
|
-
// I/O has been polled and any back-to-back "data" events have
|
|
25558
|
-
// been emitted. That makes it the natural hook for "the writer
|
|
25559
|
-
// has paused, time to flush": if more bytes were sitting in the
|
|
25560
|
-
// pipe buffer, Node would have emitted another "data" event
|
|
25561
|
-
// before this fires, and the chunker would detect that and defer.
|
|
25562
|
-
scheduleFlushCheck: (cb) => {
|
|
25563
|
-
const h = setImmediate(cb);
|
|
25564
|
-
return () => clearImmediate(h);
|
|
25565
|
-
},
|
|
25566
|
-
onChunk: (text) => {
|
|
25567
|
-
chunkQueue.push(text);
|
|
25568
|
-
void drainQueue();
|
|
25569
|
-
}
|
|
25570
|
-
});
|
|
25571
26316
|
if (stdinIsTty && !opts.sessionId) {
|
|
25572
26317
|
if (opts.prompt) {
|
|
25573
26318
|
await sendChunk("");
|
|
@@ -25575,7 +26320,7 @@ async function runCatLoop(args) {
|
|
|
25575
26320
|
await settle(0);
|
|
25576
26321
|
return done;
|
|
25577
26322
|
}
|
|
25578
|
-
if (
|
|
26323
|
+
if (useAutoStream) {
|
|
25579
26324
|
if (typeof stdin.setEncoding === "function") {
|
|
25580
26325
|
stdin.setEncoding("utf8");
|
|
25581
26326
|
}
|
|
@@ -25616,15 +26361,49 @@ async function runCatLoop(args) {
|
|
|
25616
26361
|
if (typeof stdin.setEncoding === "function") {
|
|
25617
26362
|
stdin.setEncoding("utf8");
|
|
25618
26363
|
}
|
|
26364
|
+
const useFollow = opts.follow === true || stdinIsTty && Boolean(opts.sessionId);
|
|
26365
|
+
if (useFollow) {
|
|
26366
|
+
const chunker = createChunker({
|
|
26367
|
+
scheduleFlushCheck: (cb) => {
|
|
26368
|
+
const h = setImmediate(cb);
|
|
26369
|
+
return () => clearImmediate(h);
|
|
26370
|
+
},
|
|
26371
|
+
onChunk: (text) => {
|
|
26372
|
+
chunkQueue.push(text);
|
|
26373
|
+
void drainQueue();
|
|
26374
|
+
}
|
|
26375
|
+
});
|
|
26376
|
+
stdin.on("data", (data) => {
|
|
26377
|
+
chunker.feed(typeof data === "string" ? data : data.toString("utf8"));
|
|
26378
|
+
});
|
|
26379
|
+
stdin.on("end", () => {
|
|
26380
|
+
chunker.eof();
|
|
26381
|
+
stdinEnded = true;
|
|
26382
|
+
if (!draining && chunkQueue.length === 0) {
|
|
26383
|
+
void settle(exitCode);
|
|
26384
|
+
}
|
|
26385
|
+
});
|
|
26386
|
+
stdin.on("error", (err) => {
|
|
26387
|
+
stderr(`hydra-acp cat: stdin error: ${err.message}
|
|
26388
|
+
`);
|
|
26389
|
+
exitCode = 1;
|
|
26390
|
+
stdinEnded = true;
|
|
26391
|
+
if (!draining && chunkQueue.length === 0) {
|
|
26392
|
+
void settle(exitCode);
|
|
26393
|
+
}
|
|
26394
|
+
});
|
|
26395
|
+
return done;
|
|
26396
|
+
}
|
|
26397
|
+
let oneShotBuffer = "";
|
|
25619
26398
|
stdin.on("data", (data) => {
|
|
25620
|
-
|
|
26399
|
+
oneShotBuffer += typeof data === "string" ? data : data.toString("utf8");
|
|
25621
26400
|
});
|
|
25622
26401
|
stdin.on("end", () => {
|
|
25623
|
-
chunker.eof();
|
|
25624
26402
|
stdinEnded = true;
|
|
25625
|
-
if (
|
|
25626
|
-
|
|
26403
|
+
if (oneShotBuffer.length > 0) {
|
|
26404
|
+
chunkQueue.push(oneShotBuffer);
|
|
25627
26405
|
}
|
|
26406
|
+
void drainQueue();
|
|
25628
26407
|
});
|
|
25629
26408
|
stdin.on("error", (err) => {
|
|
25630
26409
|
stderr(`hydra-acp cat: stdin error: ${err.message}
|
|
@@ -25677,12 +26456,11 @@ function runStreamingPath(args) {
|
|
|
25677
26456
|
try {
|
|
25678
26457
|
const openParams = {
|
|
25679
26458
|
sessionId,
|
|
25680
|
-
mode: "
|
|
26459
|
+
mode: "memory"
|
|
25681
26460
|
};
|
|
25682
26461
|
if (opts.streamBufferBytes !== void 0) {
|
|
25683
26462
|
openParams.capacityBytes = opts.streamBufferBytes;
|
|
25684
26463
|
}
|
|
25685
|
-
openParams.fileCapBytes = opts.streamFileCapBytes ?? DEFAULT_STREAM_FILE_CAP;
|
|
25686
26464
|
open2 = await conn.request("hydra-acp/stream_open", openParams);
|
|
25687
26465
|
} catch (err) {
|
|
25688
26466
|
args.onPromptFailed(
|
|
@@ -25690,23 +26468,12 @@ function runStreamingPath(args) {
|
|
|
25690
26468
|
);
|
|
25691
26469
|
return;
|
|
25692
26470
|
}
|
|
25693
|
-
const filePath = open2.filePath;
|
|
25694
|
-
if (filePath === void 0) {
|
|
25695
|
-
args.onPromptFailed(
|
|
25696
|
-
new Error("daemon did not return a filePath for stream mode")
|
|
25697
|
-
);
|
|
25698
|
-
return;
|
|
25699
|
-
}
|
|
25700
26471
|
if (headBuffer.length > 0) {
|
|
25701
26472
|
writeToStream(headBuffer, false);
|
|
25702
26473
|
headBuffer = Buffer.alloc(0);
|
|
25703
26474
|
}
|
|
25704
26475
|
await writeChain.catch(() => void 0);
|
|
25705
|
-
const promptText = buildStreamPromptText(
|
|
25706
|
-
opts.prompt,
|
|
25707
|
-
filePath,
|
|
25708
|
-
opts.streamFileCapBytes ?? DEFAULT_STREAM_FILE_CAP
|
|
25709
|
-
);
|
|
26476
|
+
const promptText = buildStreamPromptText(opts.prompt, open2.capacityBytes);
|
|
25710
26477
|
const promptDone = conn.request("session/prompt", {
|
|
25711
26478
|
sessionId,
|
|
25712
26479
|
prompt: [{ type: "text", text: promptText }]
|
|
@@ -25749,22 +26516,35 @@ function runStreamingPath(args) {
|
|
|
25749
26516
|
});
|
|
25750
26517
|
stdin.on("error", args.onError);
|
|
25751
26518
|
}
|
|
25752
|
-
function buildStreamPromptText(standing,
|
|
25753
|
-
const capHuman =
|
|
25754
|
-
const
|
|
26519
|
+
function buildStreamPromptText(standing, ringCapacityBytes) {
|
|
26520
|
+
const capHuman = ringCapacityBytes >= 1024 * 1024 ? `${(ringCapacityBytes / (1024 * 1024)).toFixed(0)} MB` : `${(ringCapacityBytes / 1024).toFixed(0)} KB`;
|
|
26521
|
+
const toolNote = `The user has piped data into this session. The bytes are NOT in your prompt; they live in the \`hydra_stdin\` MCP server and you read them via its tools:
|
|
26522
|
+
- \`stdin_info()\` \u2014 current writeCursor / oldestAvailable / capacity / closed. Cheap; call first to see how much data is there.
|
|
26523
|
+
- \`grep_stdin({pattern, regex?, case_insensitive?, context_before?, context_after?, cursor?})\` \u2014 server-side line filter; returns matching lines as decoded strings (not base64). Prefer this for "find lines that mention X" questions on multi-MB inputs.
|
|
26524
|
+
- \`head_stdin({bytes})\` \u2014 first N bytes (good for headers / preamble / file signatures).
|
|
26525
|
+
- \`tail_stdin({bytes})\` \u2014 most recent N bytes (good for log endings / recent errors).
|
|
26526
|
+
- \`read_stdin({cursor, max_bytes, wait_ms})\` \u2014 windowed read at an absolute byte cursor; iterate to sweep the whole stream.
|
|
26527
|
+
- \`wait_for_more({cursor, timeout_ms})\` \u2014 block for new bytes past a cursor (only useful for live tails).
|
|
26528
|
+
|
|
26529
|
+
Byte payloads (head/tail/read) come back base64-encoded \u2014 decode before reading them as text. \`grep_stdin\` returns plain strings. The ring holds the most recent ~${capHuman}; older bytes are evicted, and the byte tools report the gap when that happens. Per-call cap is 64 KiB for byte tools; loop \`read_stdin\` (advancing the cursor by \`nextCursor\`) when you need more.`;
|
|
25755
26530
|
if (standing && standing.length > 0) {
|
|
25756
|
-
return `${
|
|
26531
|
+
return `${toolNote}
|
|
25757
26532
|
|
|
25758
|
-
|
|
26533
|
+
Use those tools NOW to answer the user's question \u2014 do not ask whether to check stdin; just check it. Pick the right tool for the question (grep_stdin for finding specific lines, head for preamble / file type, tail for recent events, read_stdin + cursor sweep for whole-stream scans), then answer.
|
|
26534
|
+
|
|
26535
|
+
User's question:
|
|
26536
|
+
${standing}`;
|
|
25759
26537
|
}
|
|
25760
|
-
return
|
|
26538
|
+
return `${toolNote}
|
|
26539
|
+
|
|
26540
|
+
Use those tools to inspect the piped input and report what's there. Start with \`stdin_info()\` to see the size, then \`head_stdin\` and/or \`tail_stdin\` to look at the bytes.`;
|
|
25761
26541
|
}
|
|
25762
|
-
async function openOrAttachSession(conn, opts) {
|
|
26542
|
+
async function openOrAttachSession(conn, opts, useAutoStream) {
|
|
25763
26543
|
if (opts.sessionId) {
|
|
25764
26544
|
const attached = await conn.request("session/attach", {
|
|
25765
26545
|
sessionId: opts.sessionId,
|
|
25766
26546
|
historyPolicy: "pending_only",
|
|
25767
|
-
clientInfo: { name:
|
|
26547
|
+
clientInfo: { name: HYDRA_CAT_CLIENT_NAME, version: HYDRA_VERSION }
|
|
25768
26548
|
});
|
|
25769
26549
|
return attached.sessionId;
|
|
25770
26550
|
}
|
|
@@ -25775,6 +26555,9 @@ async function openOrAttachSession(conn, opts) {
|
|
|
25775
26555
|
if (opts.model) {
|
|
25776
26556
|
hydraMeta.model = opts.model;
|
|
25777
26557
|
}
|
|
26558
|
+
if (useAutoStream) {
|
|
26559
|
+
hydraMeta.mcpStdin = true;
|
|
26560
|
+
}
|
|
25778
26561
|
const cwd = opts.cwd ?? process.cwd();
|
|
25779
26562
|
const params = { cwd };
|
|
25780
26563
|
if (opts.agentId) {
|
|
@@ -25806,6 +26589,16 @@ async function openWs2(url, subprotocols) {
|
|
|
25806
26589
|
// src/cli.ts
|
|
25807
26590
|
init_update_check();
|
|
25808
26591
|
var suppressUpdateNotice = false;
|
|
26592
|
+
var dangerousNoticePrinted = false;
|
|
26593
|
+
function warnIfDangerouslySkipping(active) {
|
|
26594
|
+
if (!active || dangerousNoticePrinted) {
|
|
26595
|
+
return;
|
|
26596
|
+
}
|
|
26597
|
+
dangerousNoticePrinted = true;
|
|
26598
|
+
process.stderr.write(
|
|
26599
|
+
"hydra-acp: --dangerously-skip-permissions is set \u2014 all tool permission requests will be auto-approved.\n"
|
|
26600
|
+
);
|
|
26601
|
+
}
|
|
25809
26602
|
async function main() {
|
|
25810
26603
|
const argv = process.argv.slice(2);
|
|
25811
26604
|
const launchIdx = argv.indexOf("launch");
|
|
@@ -25838,11 +26631,14 @@ async function main() {
|
|
|
25838
26631
|
const name2 = resolveOption(flags2, "name");
|
|
25839
26632
|
const model2 = resolveOption(flags2, "model");
|
|
25840
26633
|
suppressUpdateNotice = true;
|
|
26634
|
+
const dangerous = flags2["dangerously-skip-permissions"] === true;
|
|
26635
|
+
warnIfDangerouslySkipping(dangerous);
|
|
25841
26636
|
const shimOpts = {
|
|
25842
26637
|
agentId,
|
|
25843
26638
|
agentArgs,
|
|
25844
26639
|
name: name2,
|
|
25845
|
-
model: model2
|
|
26640
|
+
model: model2,
|
|
26641
|
+
dangerouslySkipPermissions: dangerous
|
|
25846
26642
|
};
|
|
25847
26643
|
if (resolved2?.sessionId !== void 0) {
|
|
25848
26644
|
shimOpts.sessionId = resolved2.sessionId;
|
|
@@ -25868,6 +26664,8 @@ async function main() {
|
|
|
25868
26664
|
const name = resolveOption(flags, "name");
|
|
25869
26665
|
const agentIdFromFlag = resolveOption(flags, "agent");
|
|
25870
26666
|
const model = resolveOption(flags, "model");
|
|
26667
|
+
const dangerouslySkipPermissions = flags["dangerously-skip-permissions"] === true;
|
|
26668
|
+
warnIfDangerouslySkipping(dangerouslySkipPermissions);
|
|
25871
26669
|
const sessionInput = readSessionInput(flags);
|
|
25872
26670
|
const interactive = subcommand === "tui" || subcommand === void 0 && process.stdout.isTTY;
|
|
25873
26671
|
const resolved = await resolveSessionFlagOrExit(sessionInput, {
|
|
@@ -25889,7 +26687,8 @@ async function main() {
|
|
|
25889
26687
|
agentId: agentIdFromFlag,
|
|
25890
26688
|
name,
|
|
25891
26689
|
model,
|
|
25892
|
-
target: sessionTarget
|
|
26690
|
+
target: sessionTarget,
|
|
26691
|
+
dangerouslySkipPermissions
|
|
25893
26692
|
});
|
|
25894
26693
|
return;
|
|
25895
26694
|
}
|
|
@@ -25897,7 +26696,8 @@ async function main() {
|
|
|
25897
26696
|
const shimOpts = {
|
|
25898
26697
|
name,
|
|
25899
26698
|
model,
|
|
25900
|
-
agentId: agentIdFromFlag
|
|
26699
|
+
agentId: agentIdFromFlag,
|
|
26700
|
+
dangerouslySkipPermissions
|
|
25901
26701
|
};
|
|
25902
26702
|
if (sessionId !== void 0) {
|
|
25903
26703
|
shimOpts.sessionId = sessionId;
|
|
@@ -25914,7 +26714,8 @@ async function main() {
|
|
|
25914
26714
|
const shimOpts = {
|
|
25915
26715
|
name,
|
|
25916
26716
|
model,
|
|
25917
|
-
agentId: agentIdFromFlag
|
|
26717
|
+
agentId: agentIdFromFlag,
|
|
26718
|
+
dangerouslySkipPermissions
|
|
25918
26719
|
};
|
|
25919
26720
|
if (sessionId !== void 0) {
|
|
25920
26721
|
shimOpts.sessionId = sessionId;
|
|
@@ -25937,7 +26738,8 @@ async function main() {
|
|
|
25937
26738
|
model,
|
|
25938
26739
|
agentId: agentIdFromFlag,
|
|
25939
26740
|
detach: flags.detach === true,
|
|
25940
|
-
|
|
26741
|
+
follow: flags.follow === true,
|
|
26742
|
+
dangerouslySkipPermissions
|
|
25941
26743
|
};
|
|
25942
26744
|
if (cwd !== void 0) {
|
|
25943
26745
|
catOpts.cwd = cwd;
|
|
@@ -25953,10 +26755,6 @@ async function main() {
|
|
|
25953
26755
|
if (streamBufferBytes !== void 0) {
|
|
25954
26756
|
catOpts.streamBufferBytes = streamBufferBytes;
|
|
25955
26757
|
}
|
|
25956
|
-
const streamFileCap = parseNumericFlag(flags, "stream-file-cap");
|
|
25957
|
-
if (streamFileCap !== void 0) {
|
|
25958
|
-
catOpts.streamFileCapBytes = streamFileCap;
|
|
25959
|
-
}
|
|
25960
26758
|
suppressUpdateNotice = true;
|
|
25961
26759
|
await runCat(catOpts);
|
|
25962
26760
|
return;
|
|
@@ -26000,7 +26798,8 @@ async function main() {
|
|
|
26000
26798
|
await runSessionsList({
|
|
26001
26799
|
all: flags.all === true,
|
|
26002
26800
|
json: flags.json === true,
|
|
26003
|
-
host: typeof flags.host === "string" ? flags.host : void 0
|
|
26801
|
+
host: typeof flags.host === "string" ? flags.host : void 0,
|
|
26802
|
+
includeCat: flags["include-cat"] === true
|
|
26004
26803
|
});
|
|
26005
26804
|
return;
|
|
26006
26805
|
}
|
|
@@ -26182,7 +26981,8 @@ async function main() {
|
|
|
26182
26981
|
agentId: agentIdFromFlag,
|
|
26183
26982
|
name,
|
|
26184
26983
|
model,
|
|
26185
|
-
target: sessionTarget
|
|
26984
|
+
target: sessionTarget,
|
|
26985
|
+
dangerouslySkipPermissions
|
|
26186
26986
|
});
|
|
26187
26987
|
return;
|
|
26188
26988
|
default:
|
|
@@ -26224,6 +27024,9 @@ async function dispatchTui(flags, base) {
|
|
|
26224
27024
|
if (base.target !== void 0) {
|
|
26225
27025
|
tuiOpts.target = base.target;
|
|
26226
27026
|
}
|
|
27027
|
+
if (base.dangerouslySkipPermissions === true) {
|
|
27028
|
+
tuiOpts.dangerouslySkipPermissions = true;
|
|
27029
|
+
}
|
|
26227
27030
|
await runTui(tuiOpts);
|
|
26228
27031
|
}
|
|
26229
27032
|
function parseNumericFlag(flags, name) {
|
|
@@ -26325,15 +27128,17 @@ function printHelp() {
|
|
|
26325
27128
|
" --reattach Pick the most-recent session for the current cwd.",
|
|
26326
27129
|
" --new Force a fresh session.",
|
|
26327
27130
|
" --readonly Open a session as a transcript viewer (requires --session).",
|
|
27131
|
+
" --dangerously-skip-permissions Auto-approve every tool permission request (tui / shim / launch / cat).",
|
|
26328
27132
|
" HYDRA_ACP_SESSION Env var equivalent of --session (flag wins).",
|
|
26329
27133
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
26330
27134
|
" hydra-acp daemon [status] Show daemon pid/version (default when no subcommand)",
|
|
26331
27135
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
26332
27136
|
" hydra-acp daemon stop|restart",
|
|
26333
27137
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
26334
|
-
" hydra-acp session [list] [--all] [--json] [--host=<host>]",
|
|
27138
|
+
" hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-cat]",
|
|
26335
27139
|
" List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
|
|
26336
27140
|
" --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
|
|
27141
|
+
" --include-cat surfaces sessions spawned by `hydra cat` (hidden by default).",
|
|
26337
27142
|
" hydra-acp session kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
26338
27143
|
" hydra-acp session remove <id> Remove a session entirely (live or cold)",
|
|
26339
27144
|
" hydra-acp session export <id> [--out <file>|.]",
|