clay-server 2.17.0 → 2.18.0-beta.10
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/lib/daemon.js +2 -2
- package/lib/mates.js +85 -6
- package/lib/project.js +409 -4
- package/lib/public/app.js +307 -18
- package/lib/public/css/base.css +1 -0
- package/lib/public/css/input.css +5 -0
- package/lib/public/css/mates.css +5 -6
- package/lib/public/css/mention.css +226 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/index.html +1 -0
- package/lib/public/modules/input.js +41 -0
- package/lib/public/modules/mention.js +652 -0
- package/lib/public/modules/notifications.js +9 -3
- package/lib/public/modules/scheduler.js +47 -14
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +191 -0
- package/lib/sessions.js +13 -0
- package/package.json +1 -1
|
@@ -751,6 +751,10 @@ function renderDetailBody(tab, rec) {
|
|
|
751
751
|
html += '<span class="scheduler-detail-meta-value">' + esc(createdStr) + '</span>';
|
|
752
752
|
html += '<span class="scheduler-detail-meta-label">Last Run</span>';
|
|
753
753
|
html += '<span class="scheduler-detail-meta-value">' + esc(lastRunStr) + '</span>';
|
|
754
|
+
if (isScheduled && rec.nextRunAt) {
|
|
755
|
+
html += '<span class="scheduler-detail-meta-label">Next Run</span>';
|
|
756
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(formatDateTime(new Date(rec.nextRunAt))) + '</span>';
|
|
757
|
+
}
|
|
754
758
|
html += '</div>';
|
|
755
759
|
bodyEl2.innerHTML = html;
|
|
756
760
|
} else {
|
|
@@ -2681,6 +2685,18 @@ function closeDeleteDialog() {
|
|
|
2681
2685
|
}
|
|
2682
2686
|
}
|
|
2683
2687
|
|
|
2688
|
+
// Build an explicit list of values offset from a start value with a given step, wrapping at max
|
|
2689
|
+
function buildOffsetList(start, step, max) {
|
|
2690
|
+
var vals = [];
|
|
2691
|
+
var v = start % max;
|
|
2692
|
+
for (var i = 0; i < max; i += step) {
|
|
2693
|
+
vals.push(v);
|
|
2694
|
+
v = (v + step) % max;
|
|
2695
|
+
}
|
|
2696
|
+
vals.sort(function (a, b) { return a - b; });
|
|
2697
|
+
return vals.join(",");
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2684
2700
|
function buildCreateCron() {
|
|
2685
2701
|
if (!createSelectedDate) return null;
|
|
2686
2702
|
|
|
@@ -2707,9 +2723,9 @@ function buildCreateCron() {
|
|
|
2707
2723
|
|
|
2708
2724
|
// Interval only (no recurrence) = interval every day
|
|
2709
2725
|
if (intervalMins > 0 && createRecurrence === "none") {
|
|
2710
|
-
if (intervalMins < 60) return
|
|
2726
|
+
if (intervalMins < 60) return buildOffsetList(m, intervalMins, 60) + " * * * *";
|
|
2711
2727
|
var intHrs = Math.floor(intervalMins / 60);
|
|
2712
|
-
return "
|
|
2728
|
+
return String(m) + " " + buildOffsetList(h, intHrs, 24) + " * * *";
|
|
2713
2729
|
}
|
|
2714
2730
|
|
|
2715
2731
|
if (createRecurrence === "none" && intervalMins === 0) return null;
|
|
@@ -2718,12 +2734,12 @@ function buildCreateCron() {
|
|
|
2718
2734
|
var minField = String(m);
|
|
2719
2735
|
var hourField = String(h);
|
|
2720
2736
|
if (intervalMins > 0 && intervalMins < 60) {
|
|
2721
|
-
minField =
|
|
2737
|
+
minField = buildOffsetList(m, intervalMins, 60);
|
|
2722
2738
|
hourField = "*";
|
|
2723
2739
|
} else if (intervalMins >= 60) {
|
|
2724
2740
|
var intHrs2 = Math.floor(intervalMins / 60);
|
|
2725
2741
|
minField = String(m);
|
|
2726
|
-
hourField =
|
|
2742
|
+
hourField = buildOffsetList(h, intHrs2, 24);
|
|
2727
2743
|
}
|
|
2728
2744
|
|
|
2729
2745
|
if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
|
|
@@ -2837,10 +2853,10 @@ function buildCustomCron(h, m) {
|
|
|
2837
2853
|
var unit = document.getElementById("sched-custom-unit").value;
|
|
2838
2854
|
|
|
2839
2855
|
if (unit === "minute") {
|
|
2840
|
-
return interval === 1 ? "*/1 * * * *" :
|
|
2856
|
+
return interval === 1 ? "*/1 * * * *" : buildOffsetList(m, interval, 60) + " * * * *";
|
|
2841
2857
|
}
|
|
2842
2858
|
if (unit === "hour") {
|
|
2843
|
-
return interval === 1 ? "
|
|
2859
|
+
return interval === 1 ? m + " */1 * * *" : m + " " + buildOffsetList(h, interval, 24) + " * * *";
|
|
2844
2860
|
}
|
|
2845
2861
|
if (unit === "day") {
|
|
2846
2862
|
if (interval === 1) return m + " " + h + " * * *";
|
|
@@ -3022,15 +3038,15 @@ function cronToHuman(cron) {
|
|
|
3022
3038
|
if (!cron) return "";
|
|
3023
3039
|
var parts = cron.trim().split(/\s+/);
|
|
3024
3040
|
if (parts.length !== 5) return cron;
|
|
3025
|
-
// Minute interval patterns (e.g. */5 * * * *)
|
|
3026
|
-
if (parts[
|
|
3027
|
-
var minStep =
|
|
3028
|
-
return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
|
|
3041
|
+
// Minute interval patterns (e.g. */5 * * * * or 0,15,30,45 * * * *)
|
|
3042
|
+
if (parts[1] === "*" && parts[2] === "*") {
|
|
3043
|
+
var minStep = detectInterval(parts[0], 60);
|
|
3044
|
+
if (minStep) return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
|
|
3029
3045
|
}
|
|
3030
|
-
// Hour interval patterns (e.g. 0 */2 * * *)
|
|
3031
|
-
if (parts[
|
|
3032
|
-
var hrStep =
|
|
3033
|
-
return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
|
|
3046
|
+
// Hour interval patterns (e.g. 0 */2 * * * or 0 1,5,9,13,17,21 * * *)
|
|
3047
|
+
if (parts[2] === "*") {
|
|
3048
|
+
var hrStep = detectInterval(parts[1], 24);
|
|
3049
|
+
if (hrStep) return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
|
|
3034
3050
|
}
|
|
3035
3051
|
var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
|
|
3036
3052
|
var dow = parts[4], dom = parts[2];
|
|
@@ -3043,3 +3059,20 @@ function cronToHuman(cron) {
|
|
|
3043
3059
|
}
|
|
3044
3060
|
return cron;
|
|
3045
3061
|
}
|
|
3062
|
+
|
|
3063
|
+
// Detect if a cron field represents an evenly-spaced interval (*/N or comma-separated offset list)
|
|
3064
|
+
function detectInterval(field, max) {
|
|
3065
|
+
if (field.indexOf("/") !== -1) return parseInt(field.split("/")[1], 10) || null;
|
|
3066
|
+
if (field.indexOf(",") === -1) return null;
|
|
3067
|
+
var vals = field.split(",").map(function (v) { return parseInt(v, 10); }).sort(function (a, b) { return a - b; });
|
|
3068
|
+
if (vals.length < 2) return null;
|
|
3069
|
+
var step = vals[1] - vals[0];
|
|
3070
|
+
if (step <= 0) return null;
|
|
3071
|
+
// Verify all values are evenly spaced (wrapping around max)
|
|
3072
|
+
for (var i = 1; i < vals.length; i++) {
|
|
3073
|
+
if (vals[i] - vals[i - 1] !== step) return null;
|
|
3074
|
+
}
|
|
3075
|
+
// Check the wrap-around gap matches too
|
|
3076
|
+
if ((max - vals[vals.length - 1] + vals[0]) !== step) return null;
|
|
3077
|
+
return step;
|
|
3078
|
+
}
|
package/lib/public/style.css
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -56,6 +56,7 @@ function createSDKBridge(opts) {
|
|
|
56
56
|
var pushModule = opts.pushModule;
|
|
57
57
|
var getSDK = opts.getSDK;
|
|
58
58
|
var mateDisplayName = opts.mateDisplayName || "";
|
|
59
|
+
var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
|
|
59
60
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
60
61
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
61
62
|
|
|
@@ -1115,6 +1116,14 @@ function createSDKBridge(opts) {
|
|
|
1115
1116
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1116
1117
|
}
|
|
1117
1118
|
|
|
1119
|
+
// Mate sessions (DM, debate, mention): auto-approve read + fetch tools
|
|
1120
|
+
if (isMate || mateDisplayName) {
|
|
1121
|
+
var mateAutoTools = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
1122
|
+
if (mateAutoTools[toolName]) {
|
|
1123
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1118
1127
|
// AskUserQuestion: wait for user answers via WebSocket
|
|
1119
1128
|
if (toolName === "AskUserQuestion") {
|
|
1120
1129
|
return new Promise(function(resolve) {
|
|
@@ -1729,6 +1738,187 @@ function createSDKBridge(opts) {
|
|
|
1729
1738
|
}
|
|
1730
1739
|
}
|
|
1731
1740
|
|
|
1741
|
+
// --- @Mention: persistent read-only session for a mentioned Mate ---
|
|
1742
|
+
// Creates a mention session that can be reused across multiple mentions
|
|
1743
|
+
// within a conversation flow (session continuity).
|
|
1744
|
+
async function createMentionSession(opts) {
|
|
1745
|
+
// opts: { claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
|
|
1746
|
+
var sdk;
|
|
1747
|
+
try {
|
|
1748
|
+
sdk = await getSDK();
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
opts.onError("Failed to load Claude SDK: " + (e.message || e));
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
var mq = createMessageQueue();
|
|
1755
|
+
var abortController = new AbortController();
|
|
1756
|
+
|
|
1757
|
+
// Current response callbacks (swapped on each pushMessage)
|
|
1758
|
+
var currentOnDelta = opts.onDelta;
|
|
1759
|
+
var currentOnDone = opts.onDone;
|
|
1760
|
+
var currentOnError = opts.onError;
|
|
1761
|
+
var currentOnActivity = opts.onActivity || null;
|
|
1762
|
+
var responseFullText = "";
|
|
1763
|
+
var responseStreamedText = false;
|
|
1764
|
+
var mentionBlocks = {};
|
|
1765
|
+
var alive = true;
|
|
1766
|
+
|
|
1767
|
+
var query;
|
|
1768
|
+
try {
|
|
1769
|
+
query = sdk.query({
|
|
1770
|
+
prompt: mq,
|
|
1771
|
+
options: {
|
|
1772
|
+
cwd: cwd,
|
|
1773
|
+
systemPrompt: opts.claudeMd,
|
|
1774
|
+
settingSources: ["user"],
|
|
1775
|
+
includePartialMessages: true,
|
|
1776
|
+
abortController: abortController,
|
|
1777
|
+
canUseTool: opts.canUseTool || function (toolName, input) {
|
|
1778
|
+
var allowed = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
1779
|
+
if (allowed[toolName]) {
|
|
1780
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1781
|
+
}
|
|
1782
|
+
return Promise.resolve({
|
|
1783
|
+
behavior: "deny",
|
|
1784
|
+
message: "Read-only access. You cannot make changes via @mention.",
|
|
1785
|
+
});
|
|
1786
|
+
},
|
|
1787
|
+
},
|
|
1788
|
+
});
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
opts.onError("Failed to create mention query: " + (e.message || e));
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Push the initial message (context + question)
|
|
1795
|
+
var initialPrompt = opts.initialContext + "\n\n" + opts.initialMessage;
|
|
1796
|
+
mq.push({
|
|
1797
|
+
type: "user",
|
|
1798
|
+
message: { role: "user", content: [{ type: "text", text: initialPrompt }] },
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
// Background stream processing loop
|
|
1802
|
+
(async function () {
|
|
1803
|
+
try {
|
|
1804
|
+
for await (var sdkMsg of query) {
|
|
1805
|
+
if (sdkMsg.type === "stream_event" && sdkMsg.event) {
|
|
1806
|
+
var evt = sdkMsg.event;
|
|
1807
|
+
|
|
1808
|
+
// Track content blocks for activity reporting
|
|
1809
|
+
if (evt.type === "content_block_start") {
|
|
1810
|
+
var block = evt.content_block;
|
|
1811
|
+
var idx = evt.index;
|
|
1812
|
+
if (block.type === "thinking") {
|
|
1813
|
+
mentionBlocks[idx] = { type: "thinking" };
|
|
1814
|
+
if (currentOnActivity) currentOnActivity("thinking");
|
|
1815
|
+
} else if (block.type === "tool_use") {
|
|
1816
|
+
mentionBlocks[idx] = { type: "tool_use", name: block.name, inputJson: "" };
|
|
1817
|
+
var toolLabel = block.name;
|
|
1818
|
+
if (toolLabel === "Read") toolLabel = "Reading file...";
|
|
1819
|
+
else if (toolLabel === "Grep") toolLabel = "Searching code...";
|
|
1820
|
+
else if (toolLabel === "Glob") toolLabel = "Finding files...";
|
|
1821
|
+
if (currentOnActivity) currentOnActivity(toolLabel);
|
|
1822
|
+
} else if (block.type === "text") {
|
|
1823
|
+
mentionBlocks[idx] = { type: "text" };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (evt.type === "content_block_delta" && evt.delta) {
|
|
1828
|
+
if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
|
|
1829
|
+
responseStreamedText = true;
|
|
1830
|
+
responseFullText += evt.delta.text;
|
|
1831
|
+
if (currentOnActivity) currentOnActivity(null); // clear activity on text
|
|
1832
|
+
if (currentOnDelta) currentOnDelta(evt.delta.text);
|
|
1833
|
+
} else if (evt.delta.type === "input_json_delta" && mentionBlocks[evt.index]) {
|
|
1834
|
+
mentionBlocks[evt.index].inputJson += evt.delta.partial_json;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (evt.type === "content_block_stop") {
|
|
1839
|
+
var blk = mentionBlocks[evt.index];
|
|
1840
|
+
if (blk && blk.type === "tool_use") {
|
|
1841
|
+
// Show what file is being read
|
|
1842
|
+
var toolInput = {};
|
|
1843
|
+
try { toolInput = JSON.parse(blk.inputJson); } catch (e) {}
|
|
1844
|
+
if (blk.name === "Read" && toolInput.file_path) {
|
|
1845
|
+
var fname = toolInput.file_path.split(/[/\\]/).pop();
|
|
1846
|
+
if (currentOnActivity) currentOnActivity("Reading " + fname + "...");
|
|
1847
|
+
} else if (blk.name === "Grep" && toolInput.pattern) {
|
|
1848
|
+
if (currentOnActivity) currentOnActivity("Searching: " + toolInput.pattern.substring(0, 30) + "...");
|
|
1849
|
+
} else if (blk.name === "Glob" && toolInput.pattern) {
|
|
1850
|
+
if (currentOnActivity) currentOnActivity("Finding: " + toolInput.pattern.substring(0, 30) + "...");
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
delete mentionBlocks[evt.index];
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
} else if (sdkMsg.type === "assistant" && !responseStreamedText && sdkMsg.message && sdkMsg.message.content) {
|
|
1857
|
+
// Fallback: if text was not streamed via deltas, extract from assistant message
|
|
1858
|
+
var content = sdkMsg.message.content;
|
|
1859
|
+
if (Array.isArray(content)) {
|
|
1860
|
+
for (var ci = 0; ci < content.length; ci++) {
|
|
1861
|
+
if (content[ci].type === "text" && content[ci].text) {
|
|
1862
|
+
responseFullText += content[ci].text;
|
|
1863
|
+
if (currentOnDelta) currentOnDelta(content[ci].text);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
} else if (sdkMsg.type === "result") {
|
|
1868
|
+
// One response complete. Signal done and reset for next message.
|
|
1869
|
+
if (currentOnActivity) currentOnActivity(null);
|
|
1870
|
+
var doneRef = currentOnDone;
|
|
1871
|
+
if (doneRef) {
|
|
1872
|
+
doneRef(responseFullText);
|
|
1873
|
+
}
|
|
1874
|
+
// Only reset if pushMessage was not called during onDone
|
|
1875
|
+
// (pushMessage swaps callbacks and resets state itself)
|
|
1876
|
+
if (currentOnDone === doneRef) {
|
|
1877
|
+
currentOnDelta = null;
|
|
1878
|
+
currentOnDone = null;
|
|
1879
|
+
currentOnError = null;
|
|
1880
|
+
currentOnActivity = null;
|
|
1881
|
+
mentionBlocks = {};
|
|
1882
|
+
responseFullText = "";
|
|
1883
|
+
responseStreamedText = false;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
if (currentOnError) {
|
|
1889
|
+
if (err.name === "AbortError" || (abortController && abortController.signal.aborted)) {
|
|
1890
|
+
currentOnError("Mention query was cancelled.");
|
|
1891
|
+
} else {
|
|
1892
|
+
currentOnError(err.message || String(err));
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
alive = false;
|
|
1897
|
+
})();
|
|
1898
|
+
|
|
1899
|
+
return {
|
|
1900
|
+
// Push a follow-up message to the existing mention session
|
|
1901
|
+
pushMessage: function (text, callbacks) {
|
|
1902
|
+
currentOnDelta = callbacks.onDelta;
|
|
1903
|
+
currentOnDone = callbacks.onDone;
|
|
1904
|
+
currentOnError = callbacks.onError;
|
|
1905
|
+
currentOnActivity = callbacks.onActivity || null;
|
|
1906
|
+
mentionBlocks = {};
|
|
1907
|
+
responseFullText = "";
|
|
1908
|
+
responseStreamedText = false;
|
|
1909
|
+
mq.push({
|
|
1910
|
+
type: "user",
|
|
1911
|
+
message: { role: "user", content: [{ type: "text", text: text }] },
|
|
1912
|
+
});
|
|
1913
|
+
},
|
|
1914
|
+
close: function () {
|
|
1915
|
+
alive = false;
|
|
1916
|
+
try { mq.end(); } catch (e) {}
|
|
1917
|
+
},
|
|
1918
|
+
isAlive: function () { return alive; },
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1732
1922
|
return {
|
|
1733
1923
|
createMessageQueue: createMessageQueue,
|
|
1734
1924
|
processSDKMessage: processSDKMessage,
|
|
@@ -1746,6 +1936,7 @@ function createSDKBridge(opts) {
|
|
|
1746
1936
|
permissionPushBody: permissionPushBody,
|
|
1747
1937
|
warmup: warmup,
|
|
1748
1938
|
stopTask: stopTask,
|
|
1939
|
+
createMentionSession: createMentionSession,
|
|
1749
1940
|
};
|
|
1750
1941
|
}
|
|
1751
1942
|
|
package/lib/sessions.js
CHANGED
|
@@ -395,6 +395,16 @@ function createSessionManager(opts) {
|
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
function cleanupMentionSessions(session) {
|
|
399
|
+
if (session._mentionSessions) {
|
|
400
|
+
var mateIds = Object.keys(session._mentionSessions);
|
|
401
|
+
for (var mi = 0; mi < mateIds.length; mi++) {
|
|
402
|
+
try { session._mentionSessions[mateIds[mi]].close(); } catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
session._mentionSessions = {};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
398
408
|
function deleteSession(localId, targetWs) {
|
|
399
409
|
var session = sessions.get(localId);
|
|
400
410
|
if (!session) return;
|
|
@@ -402,6 +412,8 @@ function createSessionManager(opts) {
|
|
|
402
412
|
// Clean up unread tracking
|
|
403
413
|
delete singleUserUnread[localId];
|
|
404
414
|
|
|
415
|
+
cleanupMentionSessions(session);
|
|
416
|
+
|
|
405
417
|
if (session.abortController) {
|
|
406
418
|
try { session.abortController.abort(); } catch(e) {}
|
|
407
419
|
}
|
|
@@ -431,6 +443,7 @@ function createSessionManager(opts) {
|
|
|
431
443
|
var session = sessions.get(localId);
|
|
432
444
|
if (!session) return;
|
|
433
445
|
delete singleUserUnread[localId];
|
|
446
|
+
cleanupMentionSessions(session);
|
|
434
447
|
if (session.abortController) {
|
|
435
448
|
try { session.abortController.abort(); } catch(e) {}
|
|
436
449
|
}
|