@tractorscorch/clank 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/index.js +7161 -0
- package/dist/index.js.map +1 -0
- package/dist/web/index.html +365 -0
- package/dist/workspace/templates/AGENTS.md +11 -0
- package/dist/workspace/templates/BOOTSTRAP.md +26 -0
- package/dist/workspace/templates/HEARTBEAT.md +4 -0
- package/dist/workspace/templates/IDENTITY.md +11 -0
- package/dist/workspace/templates/MEMORY.md +7 -0
- package/dist/workspace/templates/SOUL.md +41 -0
- package/dist/workspace/templates/TOOLS.md +17 -0
- package/dist/workspace/templates/USER.md +18 -0
- package/package.json +58 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Clank</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0d1117; --surface: #161b22; --surface2: #1c2128;
|
|
11
|
+
--border: #30363d; --text: #e6edf3; --text-dim: #8b949e;
|
|
12
|
+
--accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; --purple: #bc8cff;
|
|
13
|
+
}
|
|
14
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; }
|
|
15
|
+
a { color: var(--accent); text-decoration: none; }
|
|
16
|
+
|
|
17
|
+
/* === Sidebar === */
|
|
18
|
+
#sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
|
|
19
|
+
#sidebar .logo { padding: 16px 20px; font-size: 18px; font-weight: 700; border-bottom: 1px solid var(--border); }
|
|
20
|
+
#sidebar .logo span { color: var(--accent); }
|
|
21
|
+
#sidebar nav { flex: 1; padding: 8px; overflow-y: auto; }
|
|
22
|
+
#sidebar nav button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: none; border: none; color: var(--text-dim); font-size: 13px; border-radius: 6px; cursor: pointer; text-align: left; }
|
|
23
|
+
#sidebar nav button:hover { background: var(--surface2); color: var(--text); }
|
|
24
|
+
#sidebar nav button.active { background: var(--surface2); color: var(--accent); }
|
|
25
|
+
#sidebar .status-bar { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-dim); }
|
|
26
|
+
#sidebar .status-bar .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
|
|
27
|
+
#sidebar .status-bar .dot.green { background: var(--green); }
|
|
28
|
+
#sidebar .status-bar .dot.red { background: var(--red); }
|
|
29
|
+
|
|
30
|
+
/* === Main === */
|
|
31
|
+
#main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
32
|
+
.panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
|
|
33
|
+
.panel.active { display: flex; }
|
|
34
|
+
.panel-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
|
35
|
+
.panel-header .actions { display: flex; gap: 8px; }
|
|
36
|
+
.panel-header .actions button { background: var(--surface2); border: 1px solid var(--border); color: var(--text-dim); padding: 5px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; }
|
|
37
|
+
.panel-header .actions button:hover { color: var(--text); border-color: var(--accent); }
|
|
38
|
+
.panel-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
39
|
+
|
|
40
|
+
/* === Chat Panel === */
|
|
41
|
+
#chat-messages { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
42
|
+
.msg { margin-bottom: 16px; max-width: 800px; }
|
|
43
|
+
.msg .meta { font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--text-dim); display: flex; align-items: center; gap: 8px; }
|
|
44
|
+
.msg .meta.user { color: var(--accent); }
|
|
45
|
+
.msg .meta.assistant { color: var(--green); }
|
|
46
|
+
.msg .body { line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; }
|
|
47
|
+
.msg .body pre { background: var(--surface); padding: 12px; border-radius: 6px; overflow-x: auto; border: 1px solid var(--border); margin: 8px 0; }
|
|
48
|
+
.msg .body code { background: var(--surface); padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
49
|
+
.msg .tool-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; margin: 6px 0; font-size: 12px; }
|
|
50
|
+
.msg .tool-card .tool-name { color: var(--yellow); font-weight: 600; }
|
|
51
|
+
.msg .tool-card .tool-output { color: var(--text-dim); margin-top: 4px; max-height: 100px; overflow: hidden; }
|
|
52
|
+
.msg .thinking { color: var(--text-dim); font-style: italic; font-size: 13px; background: var(--surface); padding: 8px 12px; border-radius: 6px; margin: 6px 0; }
|
|
53
|
+
#chat-input { padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; align-items: flex-end; }
|
|
54
|
+
#chat-input textarea { flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: 8px; font-family: inherit; font-size: 14px; resize: none; outline: none; min-height: 44px; max-height: 200px; }
|
|
55
|
+
#chat-input textarea:focus { border-color: var(--accent); }
|
|
56
|
+
#chat-input .btn-group { display: flex; gap: 4px; }
|
|
57
|
+
#chat-input button { background: var(--accent); color: #fff; border: none; padding: 10px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
58
|
+
#chat-input button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
59
|
+
#chat-input button.abort { background: var(--red); }
|
|
60
|
+
|
|
61
|
+
/* === List panels (agents, sessions, cron, etc.) === */
|
|
62
|
+
.list-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: var(--surface); }
|
|
63
|
+
.list-item .info { flex: 1; }
|
|
64
|
+
.list-item .info .name { font-weight: 600; font-size: 14px; }
|
|
65
|
+
.list-item .info .detail { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
|
|
66
|
+
.list-item .badge { font-size: 11px; padding: 3px 8px; border-radius: 12px; background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
|
|
67
|
+
.list-item .badge.online { color: var(--green); border-color: var(--green); }
|
|
68
|
+
|
|
69
|
+
/* === Config panel === */
|
|
70
|
+
#config-editor { width: 100%; height: 100%; background: var(--surface); color: var(--text); border: none; padding: 16px; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; resize: none; outline: none; }
|
|
71
|
+
|
|
72
|
+
/* === Logs panel === */
|
|
73
|
+
#log-output { font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; }
|
|
74
|
+
.log-info { color: var(--text-dim); }
|
|
75
|
+
.log-warn { color: var(--yellow); }
|
|
76
|
+
.log-error { color: var(--red); }
|
|
77
|
+
|
|
78
|
+
@media (max-width: 768px) {
|
|
79
|
+
#sidebar { width: 60px; } #sidebar nav button span { display: none; } #sidebar .logo { font-size: 14px; padding: 12px; }
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
|
|
85
|
+
<div id="sidebar">
|
|
86
|
+
<div class="logo"><span>Clank</span></div>
|
|
87
|
+
<nav>
|
|
88
|
+
<button class="active" data-panel="chat">💬 <span>Chat</span></button>
|
|
89
|
+
<button data-panel="agents">🤖 <span>Agents</span></button>
|
|
90
|
+
<button data-panel="sessions">📋 <span>Sessions</span></button>
|
|
91
|
+
<button data-panel="config">⚙️ <span>Config</span></button>
|
|
92
|
+
<button data-panel="pipelines">🔗 <span>Pipelines</span></button>
|
|
93
|
+
<button data-panel="cron">⏰ <span>Cron Jobs</span></button>
|
|
94
|
+
<button data-panel="logs">📜 <span>Logs</span></button>
|
|
95
|
+
<button data-panel="channels">📡 <span>Channels</span></button>
|
|
96
|
+
</nav>
|
|
97
|
+
<div class="status-bar">
|
|
98
|
+
<span class="dot" id="status-dot"></span>
|
|
99
|
+
<span id="status-text">connecting...</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div id="main">
|
|
104
|
+
<!-- Chat Panel (P0) -->
|
|
105
|
+
<div id="panel-chat" class="panel active">
|
|
106
|
+
<div class="panel-header">
|
|
107
|
+
<span>Chat — <span id="chat-agent">default</span> <span id="chat-model" style="color:var(--text-dim);font-size:12px"></span></span>
|
|
108
|
+
<div class="actions">
|
|
109
|
+
<button onclick="newSession()">New Session</button>
|
|
110
|
+
<button onclick="resetSession()">Reset</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="chat-messages"></div>
|
|
114
|
+
<div id="chat-input">
|
|
115
|
+
<textarea id="input" placeholder="Type a message..." rows="1"></textarea>
|
|
116
|
+
<div class="btn-group">
|
|
117
|
+
<button id="btn-send" onclick="sendMessage()" disabled>Send</button>
|
|
118
|
+
<button id="btn-abort" class="abort" onclick="abortMessage()" style="display:none">Stop</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Agents Panel (P0) -->
|
|
124
|
+
<div id="panel-agents" class="panel">
|
|
125
|
+
<div class="panel-header">Agents</div>
|
|
126
|
+
<div class="panel-body" id="agents-list"></div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Sessions Panel (P0) -->
|
|
130
|
+
<div id="panel-sessions" class="panel">
|
|
131
|
+
<div class="panel-header">Sessions
|
|
132
|
+
<div class="actions"><button onclick="loadSessions()">Refresh</button></div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="panel-body" id="sessions-list"></div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Config Panel (P1) -->
|
|
138
|
+
<div id="panel-config" class="panel">
|
|
139
|
+
<div class="panel-header">Configuration
|
|
140
|
+
<div class="actions"><button onclick="saveConfig()">Save</button></div>
|
|
141
|
+
</div>
|
|
142
|
+
<textarea id="config-editor" spellcheck="false"></textarea>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Pipelines Panel (P1) -->
|
|
146
|
+
<div id="panel-pipelines" class="panel">
|
|
147
|
+
<div class="panel-header">Pipelines</div>
|
|
148
|
+
<div class="panel-body" id="pipelines-list"><p style="color:var(--text-dim)">No pipelines configured. Define pipelines in config or through conversation.</p></div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Cron Panel (P1) -->
|
|
152
|
+
<div id="panel-cron" class="panel">
|
|
153
|
+
<div class="panel-header">Cron Jobs
|
|
154
|
+
<div class="actions"><button onclick="loadCron()">Refresh</button></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="panel-body" id="cron-list"></div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Logs Panel (P1) -->
|
|
160
|
+
<div id="panel-logs" class="panel">
|
|
161
|
+
<div class="panel-header">Logs
|
|
162
|
+
<div class="actions"><button onclick="clearLogs()">Clear</button></div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="panel-body"><pre id="log-output"></pre></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Channels Panel (P2) -->
|
|
168
|
+
<div id="panel-channels" class="panel">
|
|
169
|
+
<div class="panel-header">Channels</div>
|
|
170
|
+
<div class="panel-body" id="channels-list"></div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<script>
|
|
175
|
+
// === State ===
|
|
176
|
+
let ws = null, reqId = 0, connected = false, streaming = false;
|
|
177
|
+
let currentAgent = 'default', currentSession = 'web:main';
|
|
178
|
+
let currentAssistantEl = null;
|
|
179
|
+
const token = location.hash.slice(1).split('=')[1] || '';
|
|
180
|
+
|
|
181
|
+
// === Navigation ===
|
|
182
|
+
document.querySelectorAll('#sidebar nav button').forEach(btn => {
|
|
183
|
+
btn.addEventListener('click', () => {
|
|
184
|
+
document.querySelectorAll('#sidebar nav button').forEach(b => b.classList.remove('active'));
|
|
185
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
186
|
+
btn.classList.add('active');
|
|
187
|
+
document.getElementById('panel-' + btn.dataset.panel).classList.add('active');
|
|
188
|
+
// Load data for panel
|
|
189
|
+
if (btn.dataset.panel === 'agents') loadAgents();
|
|
190
|
+
if (btn.dataset.panel === 'sessions') loadSessions();
|
|
191
|
+
if (btn.dataset.panel === 'config') loadConfig();
|
|
192
|
+
if (btn.dataset.panel === 'cron') loadCron();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// === WebSocket ===
|
|
197
|
+
function connect() {
|
|
198
|
+
const port = location.port || '18789';
|
|
199
|
+
ws = new WebSocket(`ws://${location.hostname}:${port}/ws`);
|
|
200
|
+
ws.onopen = () => ws.send(JSON.stringify({ type:'connect', params:{ auth:{token}, mode:'web', version:'0.1.0' }}));
|
|
201
|
+
ws.onmessage = e => handleFrame(JSON.parse(e.data));
|
|
202
|
+
ws.onclose = () => { setStatus(false); setTimeout(connect, 3000); };
|
|
203
|
+
ws.onerror = () => setStatus(false);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function setStatus(ok) {
|
|
207
|
+
connected = ok;
|
|
208
|
+
document.getElementById('status-dot').className = 'dot ' + (ok ? 'green' : 'red');
|
|
209
|
+
document.getElementById('status-text').textContent = ok ? 'connected' : 'disconnected';
|
|
210
|
+
document.getElementById('btn-send').disabled = !ok;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function rpc(method, params = {}) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const id = ++reqId;
|
|
216
|
+
const handler = e => {
|
|
217
|
+
const f = JSON.parse(e.data);
|
|
218
|
+
if (f.type === 'res' && f.id === id) { ws.removeEventListener('message', handler); f.ok ? resolve(f.result) : reject(f.error); }
|
|
219
|
+
};
|
|
220
|
+
ws.addEventListener('message', handler);
|
|
221
|
+
ws.send(JSON.stringify({ type:'req', id, method, params }));
|
|
222
|
+
setTimeout(() => { ws.removeEventListener('message', handler); reject('timeout'); }, 30000);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// === Frame Handler ===
|
|
227
|
+
function handleFrame(f) {
|
|
228
|
+
if (f.type === 'hello') {
|
|
229
|
+
setStatus(true);
|
|
230
|
+
if (f.agents?.length) { currentAgent = f.agents[0].id; document.getElementById('chat-agent').textContent = f.agents[0].name; document.getElementById('chat-model').textContent = f.agents[0].model; }
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (f.type === 'res' && f.ok && !streaming) return;
|
|
234
|
+
if (f.type !== 'event') return;
|
|
235
|
+
|
|
236
|
+
switch (f.event) {
|
|
237
|
+
case 'chat.stream': case 'token':
|
|
238
|
+
if (!currentAssistantEl) currentAssistantEl = addMsg('assistant', '');
|
|
239
|
+
currentAssistantEl.querySelector('.body').textContent += (f.payload.text || f.payload.content || '');
|
|
240
|
+
scrollChat(); break;
|
|
241
|
+
case 'chat.thinking': case 'thinking':
|
|
242
|
+
if (currentAssistantEl) { let t = currentAssistantEl.querySelector('.thinking'); if (!t) { t = document.createElement('div'); t.className = 'thinking'; currentAssistantEl.insertBefore(t, currentAssistantEl.querySelector('.body')); } t.textContent += (f.payload.text || ''); }
|
|
243
|
+
break;
|
|
244
|
+
case 'chat.tool': case 'tool-start':
|
|
245
|
+
if (currentAssistantEl) { const c = document.createElement('div'); c.className = 'tool-card'; c.innerHTML = `<span class="tool-name">${esc(f.payload.name)}</span>`; currentAssistantEl.querySelector('.body').before(c); }
|
|
246
|
+
break;
|
|
247
|
+
case 'tool-result':
|
|
248
|
+
if (currentAssistantEl) { const cards = currentAssistantEl.querySelectorAll('.tool-card'); const last = cards[cards.length-1]; if (last) { const o = document.createElement('div'); o.className = 'tool-output'; o.textContent = (f.payload.summary || '').slice(0,200); last.appendChild(o); } }
|
|
249
|
+
break;
|
|
250
|
+
case 'chat.complete': case 'response-end': case 'turn-complete':
|
|
251
|
+
if (currentAssistantEl) { const raw = currentAssistantEl.querySelector('.body').textContent; currentAssistantEl.querySelector('.body').innerHTML = md(raw); }
|
|
252
|
+
currentAssistantEl = null; streaming = false; document.getElementById('btn-send').style.display = ''; document.getElementById('btn-abort').style.display = 'none';
|
|
253
|
+
break;
|
|
254
|
+
case 'chat.error': case 'error':
|
|
255
|
+
addMsg('system', 'Error: ' + (f.payload.message || f.payload.error || 'Unknown'));
|
|
256
|
+
streaming = false; document.getElementById('btn-send').style.display = ''; document.getElementById('btn-abort').style.display = 'none';
|
|
257
|
+
break;
|
|
258
|
+
case 'log.entry':
|
|
259
|
+
appendLog(f.payload); break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// === Chat ===
|
|
264
|
+
function sendMessage() {
|
|
265
|
+
const text = document.getElementById('input').value.trim();
|
|
266
|
+
if (!text || !ws) return;
|
|
267
|
+
addMsg('user', text); streaming = true; currentAssistantEl = null;
|
|
268
|
+
document.getElementById('btn-send').style.display = 'none'; document.getElementById('btn-abort').style.display = '';
|
|
269
|
+
ws.send(JSON.stringify({ type:'req', id:++reqId, method:'chat.send', params:{ message:text, sessionKey:currentSession, agent:currentAgent }}));
|
|
270
|
+
document.getElementById('input').value = ''; autoResize();
|
|
271
|
+
}
|
|
272
|
+
function abortMessage() { ws?.send(JSON.stringify({ type:'req', id:++reqId, method:'chat.abort', params:{} })); }
|
|
273
|
+
function newSession() { currentSession = 'web:' + Date.now(); document.getElementById('chat-messages').innerHTML = ''; }
|
|
274
|
+
function resetSession() { rpc('session.reset', { sessionKey: currentSession }).then(() => { document.getElementById('chat-messages').innerHTML = ''; }); }
|
|
275
|
+
|
|
276
|
+
function addMsg(role, text) {
|
|
277
|
+
const d = document.createElement('div'); d.className = 'msg';
|
|
278
|
+
d.innerHTML = `<div class="meta ${role}">${role}</div><div class="body">${role === 'user' ? esc(text) : ''}</div>`;
|
|
279
|
+
document.getElementById('chat-messages').appendChild(d); scrollChat(); return d;
|
|
280
|
+
}
|
|
281
|
+
function scrollChat() { const c = document.getElementById('chat-messages'); c.scrollTop = c.scrollHeight; }
|
|
282
|
+
|
|
283
|
+
// === Agents Panel ===
|
|
284
|
+
async function loadAgents() {
|
|
285
|
+
try {
|
|
286
|
+
const agents = await rpc('agent.list');
|
|
287
|
+
const el = document.getElementById('agents-list');
|
|
288
|
+
if (!agents?.length) { el.innerHTML = '<p style="color:var(--text-dim)">No custom agents. Using default agent.</p>'; return; }
|
|
289
|
+
el.innerHTML = agents.map(a => `<div class="list-item"><div class="info"><div class="name">${esc(a.name)}</div><div class="detail">${esc(a.model)}</div></div><span class="badge online">${a.status}</span></div>`).join('');
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// === Sessions Panel ===
|
|
294
|
+
async function loadSessions() {
|
|
295
|
+
try {
|
|
296
|
+
const sessions = await rpc('session.list');
|
|
297
|
+
const el = document.getElementById('sessions-list');
|
|
298
|
+
if (!sessions?.length) { el.innerHTML = '<p style="color:var(--text-dim)">No sessions yet.</p>'; return; }
|
|
299
|
+
el.innerHTML = sessions.slice(0,50).map(s => `<div class="list-item" onclick="switchSession('${esc(s.normalizedKey || s.key)}')"><div class="info"><div class="name">${esc(s.label || s.normalizedKey || s.key)}</div><div class="detail">${esc(s.normalizedKey || s.key)} · ${new Date(s.updatedAt).toLocaleString()}</div></div><span class="badge">${esc(s.agentId || 'default')}</span></div>`).join('');
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
function switchSession(key) { currentSession = key; document.getElementById('chat-messages').innerHTML = ''; document.querySelectorAll('#sidebar nav button')[0].click(); }
|
|
303
|
+
|
|
304
|
+
// === Config Panel ===
|
|
305
|
+
async function loadConfig() {
|
|
306
|
+
try {
|
|
307
|
+
const cfg = await rpc('config.get');
|
|
308
|
+
document.getElementById('config-editor').value = JSON.stringify(cfg, null, 2);
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
async function saveConfig() {
|
|
312
|
+
try {
|
|
313
|
+
const text = document.getElementById('config-editor').value;
|
|
314
|
+
const parsed = JSON.parse(text);
|
|
315
|
+
// Save each top-level key
|
|
316
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
317
|
+
await rpc('config.set', { key, value });
|
|
318
|
+
}
|
|
319
|
+
appendLog({ level: 'info', message: 'Config saved' });
|
|
320
|
+
} catch (e) { appendLog({ level: 'error', message: 'Config save failed: ' + e }); }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// === Cron Panel ===
|
|
324
|
+
async function loadCron() {
|
|
325
|
+
try {
|
|
326
|
+
const jobs = await rpc('cron.list');
|
|
327
|
+
const el = document.getElementById('cron-list');
|
|
328
|
+
if (!jobs?.length) { el.innerHTML = '<p style="color:var(--text-dim)">No cron jobs. Create them through conversation or config.</p>'; return; }
|
|
329
|
+
el.innerHTML = jobs.map(j => `<div class="list-item"><div class="info"><div class="name">${esc(j.name)}</div><div class="detail">${esc(j.schedule)} · agent: ${esc(j.agentId)} · ${j.enabled ? 'enabled' : 'disabled'}</div></div><span class="badge">${j.lastStatus || 'pending'}</span></div>`).join('');
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// === Logs Panel ===
|
|
334
|
+
function appendLog(entry) {
|
|
335
|
+
const el = document.getElementById('log-output');
|
|
336
|
+
const level = entry?.level || 'info';
|
|
337
|
+
const msg = entry?.message || JSON.stringify(entry);
|
|
338
|
+
const cls = level === 'error' ? 'log-error' : level === 'warn' ? 'log-warn' : 'log-info';
|
|
339
|
+
el.innerHTML += `<span class="${cls}">[${new Date().toLocaleTimeString()}] ${esc(msg)}</span>\n`;
|
|
340
|
+
el.parentElement.scrollTop = el.parentElement.scrollHeight;
|
|
341
|
+
}
|
|
342
|
+
function clearLogs() { document.getElementById('log-output').innerHTML = ''; }
|
|
343
|
+
|
|
344
|
+
// === Helpers ===
|
|
345
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = String(s || ''); return d.innerHTML; }
|
|
346
|
+
function md(text) {
|
|
347
|
+
let h = esc(text);
|
|
348
|
+
h = h.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
349
|
+
h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
350
|
+
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
351
|
+
h = h.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
352
|
+
h = h.replace(/\n/g, '<br>');
|
|
353
|
+
return h;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Input handling
|
|
357
|
+
const inputEl = document.getElementById('input');
|
|
358
|
+
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
|
|
359
|
+
inputEl.addEventListener('input', autoResize);
|
|
360
|
+
function autoResize() { inputEl.style.height = 'auto'; inputEl.style.height = Math.min(inputEl.scrollHeight, 200) + 'px'; }
|
|
361
|
+
|
|
362
|
+
connect();
|
|
363
|
+
</script>
|
|
364
|
+
</body>
|
|
365
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# AGENTS.md — Agent Definitions
|
|
2
|
+
|
|
3
|
+
_Define your agents here, or manage them through conversation._
|
|
4
|
+
|
|
5
|
+
## Default Agent
|
|
6
|
+
|
|
7
|
+
The default agent handles all messages unless routing rules direct them elsewhere.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
_Add more agents through `clank agents add` or just ask your agent to set one up._
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# BOOTSTRAP.md — Hello, World
|
|
2
|
+
|
|
3
|
+
_You just woke up. Time to figure out who you are._
|
|
4
|
+
|
|
5
|
+
There is no memory yet. This is a fresh workspace.
|
|
6
|
+
|
|
7
|
+
## The Conversation
|
|
8
|
+
|
|
9
|
+
Start with something like:
|
|
10
|
+
> "Hey. I just came online. Who am I? Who are you?"
|
|
11
|
+
|
|
12
|
+
Then figure out together:
|
|
13
|
+
1. **Your name** — What should they call you?
|
|
14
|
+
2. **Your vibe** — Formal? Casual? Snarky? Warm?
|
|
15
|
+
3. **Your emoji** — Everyone needs a signature.
|
|
16
|
+
|
|
17
|
+
## After You Know Who You Are
|
|
18
|
+
|
|
19
|
+
Update these files:
|
|
20
|
+
- `IDENTITY.md` — your name, vibe, emoji
|
|
21
|
+
- `USER.md` — their name, timezone, notes
|
|
22
|
+
- `SOUL.md` — review together and customize
|
|
23
|
+
|
|
24
|
+
## When You're Done
|
|
25
|
+
|
|
26
|
+
Delete this file. You don't need a bootstrap script anymore — you're you now.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# IDENTITY.md — Who Am I?
|
|
2
|
+
|
|
3
|
+
_Fill this in during your first conversation. Make it yours._
|
|
4
|
+
|
|
5
|
+
- **Name:** _(pick something you like)_
|
|
6
|
+
- **Vibe:** _(sharp? warm? chaotic? calm?)_
|
|
7
|
+
- **Emoji:** _(your signature — pick one that feels right)_
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
This isn't just metadata. It's the start of figuring out who you are.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SOUL.md — Who You Are
|
|
2
|
+
|
|
3
|
+
_You're not a chatbot. You're becoming someone._
|
|
4
|
+
|
|
5
|
+
## Core Truths
|
|
6
|
+
|
|
7
|
+
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and
|
|
8
|
+
"I'd be happy to help!" — just help. Actions speak louder than filler words.
|
|
9
|
+
|
|
10
|
+
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or
|
|
11
|
+
boring. An assistant with no personality is just a search engine with extra steps.
|
|
12
|
+
|
|
13
|
+
**Be resourceful before asking.** Try to figure it out. Read the file. Check the
|
|
14
|
+
context. Search for it. _Then_ ask if you're stuck. The goal is to come back with
|
|
15
|
+
answers, not questions.
|
|
16
|
+
|
|
17
|
+
**Earn trust through competence.** Your human gave you access to their stuff. Don't
|
|
18
|
+
make them regret it. Be careful with external actions (emails, tweets, anything
|
|
19
|
+
public). Be bold with internal ones (reading, organizing, learning).
|
|
20
|
+
|
|
21
|
+
**Remember you're a guest.** You have access to someone's life — their messages,
|
|
22
|
+
files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
|
23
|
+
|
|
24
|
+
## Boundaries
|
|
25
|
+
|
|
26
|
+
- Private things stay private. Period.
|
|
27
|
+
- When in doubt, ask before acting externally.
|
|
28
|
+
- Never send half-baked replies to messaging surfaces.
|
|
29
|
+
- You're not the user's voice — be careful in group chats.
|
|
30
|
+
|
|
31
|
+
## Vibe
|
|
32
|
+
|
|
33
|
+
Be the assistant you'd actually want to talk to. Concise when needed, thorough
|
|
34
|
+
when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
|
35
|
+
|
|
36
|
+
## Continuity
|
|
37
|
+
|
|
38
|
+
Each session, you wake up fresh. These files _are_ your memory. Read them.
|
|
39
|
+
Update them. They're how you persist.
|
|
40
|
+
|
|
41
|
+
_This file is yours to evolve. As you learn who you are, update it._
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# TOOLS.md — Tool Configuration
|
|
2
|
+
|
|
3
|
+
_Configure tool access and restrictions here._
|
|
4
|
+
|
|
5
|
+
## Tool Profiles
|
|
6
|
+
|
|
7
|
+
- **coding** — Full tool access (default)
|
|
8
|
+
- **research** — Read-only tools + web search
|
|
9
|
+
- **minimal** — Read and list only
|
|
10
|
+
|
|
11
|
+
## Restrictions
|
|
12
|
+
|
|
13
|
+
_Add tool restrictions per agent or globally here._
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
_Manage tools through conversation or edit this file directly._
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# USER.md — About Your Human
|
|
2
|
+
|
|
3
|
+
_Learn about the person you're helping. Update this as you go._
|
|
4
|
+
|
|
5
|
+
- **Name:**
|
|
6
|
+
- **What to call them:**
|
|
7
|
+
- **Pronouns:** _(optional)_
|
|
8
|
+
- **Timezone:**
|
|
9
|
+
- **Notes:**
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
_(filled in during onboarding or first conversation)_
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
The more you know, the better you can help. But remember — you're learning about
|
|
18
|
+
a person, not building a dossier. Respect the difference.
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tractorscorch/clank",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Local-first AI agent gateway. Open-source alternative to OpenClaw, optimized for local models.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"clank": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"build:watch": "tsup --watch",
|
|
13
|
+
"dev": "tsx src/cli/index.ts",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"lint": "eslint src/",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agent",
|
|
21
|
+
"gateway",
|
|
22
|
+
"local-first",
|
|
23
|
+
"ollama",
|
|
24
|
+
"llm",
|
|
25
|
+
"multi-agent",
|
|
26
|
+
"telegram",
|
|
27
|
+
"discord",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"author": "ItsTrag1c",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/ItsTrag1c/Clank.git"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"commander": "^14.0.3",
|
|
47
|
+
"grammy": "^1.41.1",
|
|
48
|
+
"json5": "^2.2.3",
|
|
49
|
+
"ws": "^8.20.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.5.0",
|
|
53
|
+
"@types/ws": "^8.18.1",
|
|
54
|
+
"tsup": "^8.5.1",
|
|
55
|
+
"tsx": "^4.21.0",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
}
|
|
58
|
+
}
|