create-walle 0.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/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,216 @@
|
|
|
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>Wall-E Setup</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #e6edf3; --dim: #8b949e; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; }
|
|
9
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
11
|
+
.container { max-width: 520px; width: 100%; padding: 24px; }
|
|
12
|
+
h1 { font-size: 28px; font-weight: 600; margin-bottom: 4px; }
|
|
13
|
+
.subtitle { color: var(--dim); margin-bottom: 32px; font-size: 15px; }
|
|
14
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 16px; }
|
|
15
|
+
.card h2 { font-size: 16px; font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
|
|
16
|
+
.card p { color: var(--dim); font-size: 13px; margin-bottom: 16px; }
|
|
17
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
18
|
+
.status-dot.ok { background: var(--green); }
|
|
19
|
+
.status-dot.missing { background: var(--yellow); }
|
|
20
|
+
.status-dot.error { background: var(--red); }
|
|
21
|
+
label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--dim); }
|
|
22
|
+
input[type="text"], input[type="password"] {
|
|
23
|
+
width: 100%; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border);
|
|
24
|
+
border-radius: 8px; color: var(--text); font-size: 14px; font-family: 'SF Mono', Menlo, monospace;
|
|
25
|
+
outline: none; transition: border-color 0.15s;
|
|
26
|
+
}
|
|
27
|
+
input:focus { border-color: var(--accent); }
|
|
28
|
+
.input-row { margin-bottom: 12px; }
|
|
29
|
+
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 20px; border-radius: 8px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
|
30
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
31
|
+
.btn-primary:hover { filter: brightness(1.15); }
|
|
32
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
33
|
+
.btn-secondary { background: var(--border); color: var(--text); }
|
|
34
|
+
.btn-secondary:hover { background: #3d444d; }
|
|
35
|
+
.btn-row { display: flex; gap: 12px; align-items: center; margin-top: 8px; }
|
|
36
|
+
.success-msg { color: var(--green); font-size: 13px; display: none; }
|
|
37
|
+
.error-msg { color: var(--red); font-size: 13px; display: none; }
|
|
38
|
+
.integration { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
|
39
|
+
.integration:last-child { border-bottom: none; }
|
|
40
|
+
.integration-info { display: flex; align-items: center; gap: 12px; }
|
|
41
|
+
.integration-icon { font-size: 20px; width: 32px; text-align: center; }
|
|
42
|
+
.integration-name { font-weight: 500; font-size: 14px; }
|
|
43
|
+
.integration-desc { color: var(--dim); font-size: 12px; }
|
|
44
|
+
.badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
|
|
45
|
+
.badge-connected { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
46
|
+
.badge-ready { background: rgba(88,166,255,0.15); color: var(--accent); }
|
|
47
|
+
.done-section { text-align: center; padding: 16px 0; }
|
|
48
|
+
.done-section a { color: var(--accent); text-decoration: none; font-weight: 500; }
|
|
49
|
+
.done-section a:hover { text-decoration: underline; }
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="container">
|
|
54
|
+
<h1>Welcome to Wall-E</h1>
|
|
55
|
+
<p class="subtitle">Let's get you set up. This takes about 30 seconds.</p>
|
|
56
|
+
|
|
57
|
+
<!-- Step 1: Owner -->
|
|
58
|
+
<div class="card">
|
|
59
|
+
<h2><span class="status-dot ok" id="owner-dot"></span> Owner</h2>
|
|
60
|
+
<p>Auto-detected from your system.</p>
|
|
61
|
+
<div class="input-row">
|
|
62
|
+
<label>Name</label>
|
|
63
|
+
<input type="text" id="owner-name" placeholder="Your Name">
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Step 2: API Key -->
|
|
68
|
+
<div class="card">
|
|
69
|
+
<h2><span class="status-dot missing" id="api-dot"></span> Anthropic API Key</h2>
|
|
70
|
+
<p>Required for AI features (chat, think, reflect). Get one at <a href="https://console.anthropic.com/settings/keys" target="_blank" style="color:var(--accent)">console.anthropic.com</a></p>
|
|
71
|
+
<div class="input-row">
|
|
72
|
+
<label>API Key</label>
|
|
73
|
+
<input type="password" id="api-key" placeholder="sk-ant-...">
|
|
74
|
+
</div>
|
|
75
|
+
<div class="btn-row">
|
|
76
|
+
<button class="btn btn-primary" id="save-btn" onclick="saveConfig()">Save & Continue</button>
|
|
77
|
+
<span class="success-msg" id="save-ok">Saved!</span>
|
|
78
|
+
<span class="error-msg" id="save-err"></span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Step 3: Integrations -->
|
|
83
|
+
<div class="card">
|
|
84
|
+
<h2>Integrations</h2>
|
|
85
|
+
<p>Optional — connect these anytime from the Wall-E settings.</p>
|
|
86
|
+
|
|
87
|
+
<div class="integration" id="int-slack">
|
|
88
|
+
<div class="integration-info">
|
|
89
|
+
<div class="integration-icon">💬</div>
|
|
90
|
+
<div>
|
|
91
|
+
<div class="integration-name">Slack</div>
|
|
92
|
+
<div class="integration-desc">Sync messages, search conversations</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<button class="btn btn-secondary" id="slack-btn" onclick="connectSlack()">Connect</button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="integration">
|
|
99
|
+
<div class="integration-info">
|
|
100
|
+
<div class="integration-icon">📧</div>
|
|
101
|
+
<div>
|
|
102
|
+
<div class="integration-name">Email (macOS Mail)</div>
|
|
103
|
+
<div class="integration-desc">Auto-detected — uses AppleScript</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<span class="badge badge-ready">Auto</span>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="integration">
|
|
110
|
+
<div class="integration-info">
|
|
111
|
+
<div class="integration-icon">📅</div>
|
|
112
|
+
<div>
|
|
113
|
+
<div class="integration-name">Calendar</div>
|
|
114
|
+
<div class="integration-desc">Syncs via EventKit on macOS</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<span class="badge badge-ready">Auto</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Done -->
|
|
122
|
+
<div class="done-section">
|
|
123
|
+
<a href="/" id="done-link">Go to Dashboard →</a>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
async function loadStatus() {
|
|
129
|
+
try {
|
|
130
|
+
const r = await fetch('/api/setup/status');
|
|
131
|
+
const d = await r.json();
|
|
132
|
+
if (d.owner_name) document.getElementById('owner-name').value = d.owner_name;
|
|
133
|
+
if (d.has_api_key) {
|
|
134
|
+
document.getElementById('api-dot').className = 'status-dot ok';
|
|
135
|
+
document.getElementById('api-key').placeholder = '••••••••••••••• (configured)';
|
|
136
|
+
}
|
|
137
|
+
if (d.slack_connected) {
|
|
138
|
+
document.getElementById('slack-btn').outerHTML = '<span class="badge badge-connected">Connected</span>';
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function saveConfig() {
|
|
144
|
+
const btn = document.getElementById('save-btn');
|
|
145
|
+
const okMsg = document.getElementById('save-ok');
|
|
146
|
+
const errMsg = document.getElementById('save-err');
|
|
147
|
+
okMsg.style.display = 'none';
|
|
148
|
+
errMsg.style.display = 'none';
|
|
149
|
+
btn.disabled = true;
|
|
150
|
+
|
|
151
|
+
const ownerVal = document.getElementById('owner-name').value.trim();
|
|
152
|
+
const apiVal = document.getElementById('api-key').value.trim();
|
|
153
|
+
|
|
154
|
+
if (!ownerVal) {
|
|
155
|
+
errMsg.textContent = 'Name is required';
|
|
156
|
+
errMsg.style.display = 'inline';
|
|
157
|
+
btn.disabled = false;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (apiVal && !apiVal.startsWith('sk-ant-')) {
|
|
161
|
+
errMsg.textContent = 'API key should start with sk-ant-';
|
|
162
|
+
errMsg.style.display = 'inline';
|
|
163
|
+
btn.disabled = false;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const body = { owner_name: ownerVal, api_key: apiVal };
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const r = await fetch('/api/setup/save', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify(body),
|
|
174
|
+
});
|
|
175
|
+
const d = await r.json();
|
|
176
|
+
if (d.ok) {
|
|
177
|
+
okMsg.style.display = 'inline';
|
|
178
|
+
document.getElementById('api-dot').className = 'status-dot ok';
|
|
179
|
+
document.getElementById('owner-dot').className = 'status-dot ok';
|
|
180
|
+
} else {
|
|
181
|
+
errMsg.textContent = d.error || 'Save failed';
|
|
182
|
+
errMsg.style.display = 'inline';
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
errMsg.textContent = e.message;
|
|
186
|
+
errMsg.style.display = 'inline';
|
|
187
|
+
}
|
|
188
|
+
btn.disabled = false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function connectSlack() {
|
|
192
|
+
const btn = document.getElementById('slack-btn');
|
|
193
|
+
try {
|
|
194
|
+
btn.disabled = true;
|
|
195
|
+
btn.textContent = 'Connecting...';
|
|
196
|
+
const r = await fetch('/api/wall-e/slack/auth', { method: 'POST' });
|
|
197
|
+
const d = await r.json();
|
|
198
|
+
if (d.url) {
|
|
199
|
+
window.open(d.url, '_blank');
|
|
200
|
+
btn.textContent = 'Check browser...';
|
|
201
|
+
} else if (d.ok) {
|
|
202
|
+
btn.outerHTML = '<span class="badge badge-connected">Connected</span>';
|
|
203
|
+
} else {
|
|
204
|
+
btn.textContent = d.error || 'Failed';
|
|
205
|
+
btn.disabled = false;
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
btn.textContent = 'Connect';
|
|
209
|
+
btn.disabled = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
loadStatus();
|
|
214
|
+
</script>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// --- Prompt Queue Engine ---
|
|
2
|
+
// In-memory, session-scoped queue state management with completion detection
|
|
3
|
+
// Persists to SQLite via db.js
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const db = require('./db');
|
|
6
|
+
|
|
7
|
+
// Map<sessionId, QueueState>
|
|
8
|
+
const queues = new Map();
|
|
9
|
+
|
|
10
|
+
// Hook called when a new queue is created (set by server.js)
|
|
11
|
+
let _onQueueCreated = null;
|
|
12
|
+
function setOnQueueCreated(fn) { _onQueueCreated = fn; }
|
|
13
|
+
|
|
14
|
+
// Default prompt patterns that indicate Claude Code is waiting for input
|
|
15
|
+
const DEFAULT_PROMPT_PATTERNS = [
|
|
16
|
+
/[❯>$#]\s*$/,
|
|
17
|
+
/^\s*>\s*$/m,
|
|
18
|
+
/waiting for input/i,
|
|
19
|
+
/\?\s*$/,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// --- Persistence (SQLite) ---
|
|
23
|
+
|
|
24
|
+
function saveToDb(q) {
|
|
25
|
+
try {
|
|
26
|
+
db.saveQueue(q.sessionId, {
|
|
27
|
+
mode: q.mode,
|
|
28
|
+
status: q.status,
|
|
29
|
+
currentIndex: q.currentIndex,
|
|
30
|
+
items: q.items.map(i => ({
|
|
31
|
+
id: i.id,
|
|
32
|
+
type: i.type,
|
|
33
|
+
promptId: i.promptId,
|
|
34
|
+
title: i.title,
|
|
35
|
+
text: i.text,
|
|
36
|
+
status: i.status,
|
|
37
|
+
})),
|
|
38
|
+
idleTimeoutMs: q.idleTimeoutMs,
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('Failed to save queue to db:', e.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadFromDb() {
|
|
46
|
+
try {
|
|
47
|
+
const rows = db.loadAllQueues();
|
|
48
|
+
for (const saved of rows) {
|
|
49
|
+
// Pause any running queues on restore (PTY may have moved on)
|
|
50
|
+
const queue = {
|
|
51
|
+
sessionId: saved.sessionId,
|
|
52
|
+
mode: saved.mode,
|
|
53
|
+
status: saved.status === 'running' ? 'paused' : saved.status,
|
|
54
|
+
currentIndex: saved.currentIndex,
|
|
55
|
+
items: saved.items.map(item => ({
|
|
56
|
+
id: item.id || crypto.randomUUID(),
|
|
57
|
+
type: item.type || 'inline',
|
|
58
|
+
promptId: item.promptId || null,
|
|
59
|
+
title: item.title || 'Untitled',
|
|
60
|
+
text: item.text || '',
|
|
61
|
+
status: item.status || 'pending',
|
|
62
|
+
})),
|
|
63
|
+
idleTimeoutMs: saved.idleTimeoutMs || 10000,
|
|
64
|
+
promptPatterns: DEFAULT_PROMPT_PATTERNS,
|
|
65
|
+
_idleTimer: null,
|
|
66
|
+
_recentOutput: '',
|
|
67
|
+
_outputSinceLastSend: '',
|
|
68
|
+
_lastOutputTime: 0,
|
|
69
|
+
_onStateChange: null,
|
|
70
|
+
_sendFn: null,
|
|
71
|
+
};
|
|
72
|
+
queues.set(saved.sessionId, queue);
|
|
73
|
+
}
|
|
74
|
+
if (queues.size > 0) {
|
|
75
|
+
console.log(` Restored ${queues.size} queue(s) from database`);
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('Failed to load queues from db:', e.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// NOTE: loadFromDb() is called by init(), which must be called after db.initDb()
|
|
83
|
+
|
|
84
|
+
function init() {
|
|
85
|
+
loadFromDb();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createQueue(sessionId, { mode = 'manual', items = [], idleTimeoutMs = 10000, promptPatterns } = {}) {
|
|
89
|
+
const queue = {
|
|
90
|
+
sessionId,
|
|
91
|
+
mode,
|
|
92
|
+
status: 'idle',
|
|
93
|
+
currentIndex: -1,
|
|
94
|
+
items: items.map(item => ({
|
|
95
|
+
id: crypto.randomUUID(),
|
|
96
|
+
type: item.type || 'inline',
|
|
97
|
+
promptId: item.promptId || null,
|
|
98
|
+
title: item.title || item.text?.slice(0, 60) || 'Untitled',
|
|
99
|
+
text: item.text || '',
|
|
100
|
+
status: 'pending',
|
|
101
|
+
})),
|
|
102
|
+
idleTimeoutMs: idleTimeoutMs || 10000,
|
|
103
|
+
promptPatterns: promptPatterns || DEFAULT_PROMPT_PATTERNS,
|
|
104
|
+
_idleTimer: null,
|
|
105
|
+
_recentOutput: '',
|
|
106
|
+
_outputSinceLastSend: '',
|
|
107
|
+
_lastOutputTime: 0,
|
|
108
|
+
_onStateChange: null,
|
|
109
|
+
_sendFn: null,
|
|
110
|
+
};
|
|
111
|
+
queues.set(sessionId, queue);
|
|
112
|
+
saveToDb(queue);
|
|
113
|
+
if (_onQueueCreated) _onQueueCreated(sessionId);
|
|
114
|
+
return getState(sessionId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getQueue(sessionId) {
|
|
118
|
+
return queues.get(sessionId) || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getState(sessionId) {
|
|
122
|
+
const q = queues.get(sessionId);
|
|
123
|
+
if (!q) return null;
|
|
124
|
+
return {
|
|
125
|
+
sessionId: q.sessionId,
|
|
126
|
+
mode: q.mode,
|
|
127
|
+
status: q.status,
|
|
128
|
+
currentIndex: q.currentIndex,
|
|
129
|
+
items: q.items.map(i => ({
|
|
130
|
+
id: i.id,
|
|
131
|
+
type: i.type,
|
|
132
|
+
promptId: i.promptId,
|
|
133
|
+
title: i.title,
|
|
134
|
+
text: i.text,
|
|
135
|
+
status: i.status,
|
|
136
|
+
})),
|
|
137
|
+
idleTimeoutMs: q.idleTimeoutMs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getAllStates() {
|
|
142
|
+
const result = {};
|
|
143
|
+
for (const [sessionId] of queues) {
|
|
144
|
+
result[sessionId] = getState(sessionId);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function deleteQueue(sessionId) {
|
|
150
|
+
const q = queues.get(sessionId);
|
|
151
|
+
if (q) {
|
|
152
|
+
clearIdleTimer(q);
|
|
153
|
+
queues.delete(sessionId);
|
|
154
|
+
try { db.deleteQueueDb(sessionId); } catch (e) { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function notifyChange(q) {
|
|
159
|
+
saveToDb(q);
|
|
160
|
+
if (q._onStateChange) {
|
|
161
|
+
q._onStateChange(getState(q.sessionId));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Execution Control ---
|
|
166
|
+
|
|
167
|
+
function start(sessionId) {
|
|
168
|
+
const q = queues.get(sessionId);
|
|
169
|
+
if (!q) return null;
|
|
170
|
+
if (q.items.length === 0) return getState(sessionId);
|
|
171
|
+
|
|
172
|
+
q.status = 'running';
|
|
173
|
+
q.currentIndex = -1;
|
|
174
|
+
sendNext(sessionId);
|
|
175
|
+
return getState(sessionId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sendNext(sessionId) {
|
|
179
|
+
const q = queues.get(sessionId);
|
|
180
|
+
if (!q || q.status !== 'running') return null;
|
|
181
|
+
|
|
182
|
+
// Find next pending item
|
|
183
|
+
let nextIdx = -1;
|
|
184
|
+
for (let i = q.currentIndex + 1; i < q.items.length; i++) {
|
|
185
|
+
if (q.items[i].status === 'pending') {
|
|
186
|
+
nextIdx = i;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (nextIdx === -1) {
|
|
192
|
+
// All done
|
|
193
|
+
q.status = 'done';
|
|
194
|
+
clearIdleTimer(q);
|
|
195
|
+
notifyChange(q);
|
|
196
|
+
return getState(sessionId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
q.currentIndex = nextIdx;
|
|
200
|
+
const item = q.items[nextIdx];
|
|
201
|
+
item.status = 'sending';
|
|
202
|
+
q._outputSinceLastSend = '';
|
|
203
|
+
notifyChange(q);
|
|
204
|
+
|
|
205
|
+
// Build text with images embedded as markdown references
|
|
206
|
+
let textToSend = item.text;
|
|
207
|
+
if (item.images && item.images.length > 0) {
|
|
208
|
+
const imgRefs = item.images
|
|
209
|
+
.filter(img => img.path)
|
|
210
|
+
.map(img => ``);
|
|
211
|
+
if (imgRefs.length > 0) {
|
|
212
|
+
textToSend += '\n\n' + imgRefs.join('\n');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Send the prompt text to PTY, then Enter (\r) after a short delay
|
|
217
|
+
// Splitting text and Enter ensures the terminal processes the paste first
|
|
218
|
+
if (q._sendFn) {
|
|
219
|
+
q._sendFn(textToSend);
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
if (q._sendFn) q._sendFn('\r');
|
|
222
|
+
}, 100);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
item.status = 'sent';
|
|
226
|
+
notifyChange(q);
|
|
227
|
+
|
|
228
|
+
// Start idle detection for auto mode
|
|
229
|
+
if (q.mode === 'auto') {
|
|
230
|
+
startIdleTimer(q);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return getState(sessionId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function skip(sessionId) {
|
|
237
|
+
const q = queues.get(sessionId);
|
|
238
|
+
if (!q || q.status !== 'running') return null;
|
|
239
|
+
|
|
240
|
+
// Find next pending item and skip it
|
|
241
|
+
for (let i = q.currentIndex + 1; i < q.items.length; i++) {
|
|
242
|
+
if (q.items[i].status === 'pending') {
|
|
243
|
+
q.items[i].status = 'skipped';
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
notifyChange(q);
|
|
249
|
+
return getState(sessionId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pause(sessionId) {
|
|
253
|
+
const q = queues.get(sessionId);
|
|
254
|
+
if (!q || q.status !== 'running') return null;
|
|
255
|
+
|
|
256
|
+
q.status = 'paused';
|
|
257
|
+
clearIdleTimer(q);
|
|
258
|
+
notifyChange(q);
|
|
259
|
+
return getState(sessionId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resume(sessionId) {
|
|
263
|
+
const q = queues.get(sessionId);
|
|
264
|
+
if (!q || q.status !== 'paused') return null;
|
|
265
|
+
|
|
266
|
+
q.status = 'running';
|
|
267
|
+
|
|
268
|
+
// If current item is already sent, check if we should auto-advance
|
|
269
|
+
const currentItem = q.items[q.currentIndex];
|
|
270
|
+
if (!currentItem || currentItem.status === 'sent') {
|
|
271
|
+
if (q.mode === 'auto') {
|
|
272
|
+
startIdleTimer(q);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
notifyChange(q);
|
|
277
|
+
return getState(sessionId);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stop(sessionId) {
|
|
281
|
+
const q = queues.get(sessionId);
|
|
282
|
+
if (!q) return null;
|
|
283
|
+
|
|
284
|
+
q.status = 'done';
|
|
285
|
+
clearIdleTimer(q);
|
|
286
|
+
notifyChange(q);
|
|
287
|
+
return getState(sessionId);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function setMode(sessionId, mode) {
|
|
291
|
+
const q = queues.get(sessionId);
|
|
292
|
+
if (!q) return null;
|
|
293
|
+
|
|
294
|
+
q.mode = mode;
|
|
295
|
+
if (mode === 'auto' && q.status === 'running') {
|
|
296
|
+
const currentItem = q.items[q.currentIndex];
|
|
297
|
+
if (currentItem && currentItem.status === 'sent') {
|
|
298
|
+
startIdleTimer(q);
|
|
299
|
+
}
|
|
300
|
+
} else if (mode === 'manual') {
|
|
301
|
+
clearIdleTimer(q);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
notifyChange(q);
|
|
305
|
+
return getState(sessionId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- Completion Detection ---
|
|
309
|
+
|
|
310
|
+
function feedOutput(sessionId, data) {
|
|
311
|
+
const q = queues.get(sessionId);
|
|
312
|
+
if (!q || q.status !== 'running') return;
|
|
313
|
+
|
|
314
|
+
q._lastOutputTime = Date.now();
|
|
315
|
+
q._outputSinceLastSend += data;
|
|
316
|
+
if (q._outputSinceLastSend.length > 2048) {
|
|
317
|
+
q._outputSinceLastSend = q._outputSinceLastSend.slice(-2048);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (q.mode === 'auto') {
|
|
321
|
+
const clean = stripAnsi(q._outputSinceLastSend);
|
|
322
|
+
const lastLines = clean.split('\n').slice(-5).join('\n');
|
|
323
|
+
|
|
324
|
+
for (const pattern of q.promptPatterns) {
|
|
325
|
+
if (pattern.test(lastLines)) {
|
|
326
|
+
clearIdleTimer(q);
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
if (q.status === 'running') {
|
|
329
|
+
sendNext(sessionId);
|
|
330
|
+
}
|
|
331
|
+
}, 500);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
resetIdleTimer(q);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function stripAnsi(str) {
|
|
341
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
342
|
+
.replace(/\x1b\][^\x07]*\x07/g, '')
|
|
343
|
+
.replace(/\x1b[()][AB012]/g, '')
|
|
344
|
+
.replace(/\x1b[\[\]()#;?]*[0-9;]*[a-zA-Z@`]/g, '');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function startIdleTimer(q) {
|
|
348
|
+
clearIdleTimer(q);
|
|
349
|
+
q._idleTimer = setTimeout(() => {
|
|
350
|
+
if (q.status === 'running' && q.mode === 'auto') {
|
|
351
|
+
sendNext(q.sessionId);
|
|
352
|
+
}
|
|
353
|
+
}, q.idleTimeoutMs);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function resetIdleTimer(q) {
|
|
357
|
+
if (q._idleTimer) {
|
|
358
|
+
startIdleTimer(q);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function clearIdleTimer(q) {
|
|
363
|
+
if (q._idleTimer) {
|
|
364
|
+
clearTimeout(q._idleTimer);
|
|
365
|
+
q._idleTimer = null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- Hooks ---
|
|
370
|
+
|
|
371
|
+
function setOnStateChange(sessionId, fn) {
|
|
372
|
+
const q = queues.get(sessionId);
|
|
373
|
+
if (q) q._onStateChange = fn;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function setSendFn(sessionId, fn) {
|
|
377
|
+
const q = queues.get(sessionId);
|
|
378
|
+
if (q) q._sendFn = fn;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function onSessionExit(sessionId) {
|
|
382
|
+
deleteQueue(sessionId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
init,
|
|
387
|
+
createQueue,
|
|
388
|
+
getQueue,
|
|
389
|
+
getState,
|
|
390
|
+
getAllStates,
|
|
391
|
+
deleteQueue,
|
|
392
|
+
start,
|
|
393
|
+
sendNext,
|
|
394
|
+
skip,
|
|
395
|
+
pause,
|
|
396
|
+
resume,
|
|
397
|
+
stop,
|
|
398
|
+
setMode,
|
|
399
|
+
feedOutput,
|
|
400
|
+
setOnStateChange,
|
|
401
|
+
setSendFn,
|
|
402
|
+
onSessionExit,
|
|
403
|
+
setOnQueueCreated,
|
|
404
|
+
};
|