@faviovazquez/deliberate 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.
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # deliberate visual companion server
5
+ # Watches a directory for HTML files and serves the newest one to a browser.
6
+ # User interactions (clicks, selections) are recorded as JSONL events.
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ PROJECT_DIR=""
10
+ HOST="127.0.0.1"
11
+ URL_HOST=""
12
+ PORT=0
13
+ FOREGROUND=false
14
+
15
+ usage() {
16
+ cat <<EOF
17
+ Usage: start-server.sh [OPTIONS]
18
+
19
+ Options:
20
+ --project-dir DIR Project root for persistent storage (recommended)
21
+ --host HOST Bind address (default: 127.0.0.1)
22
+ --url-host HOST Hostname for printed URL (default: same as --host)
23
+ --port PORT Port to listen on (default: random available)
24
+ --foreground Run in foreground (for Codex, Gemini CLI)
25
+ -h, --help Show this help
26
+ EOF
27
+ exit 0
28
+ }
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --project-dir) PROJECT_DIR="$2"; shift 2 ;;
33
+ --host) HOST="$2"; shift 2 ;;
34
+ --url-host) URL_HOST="$2"; shift 2 ;;
35
+ --port) PORT="$2"; shift 2 ;;
36
+ --foreground) FOREGROUND=true; shift ;;
37
+ -h|--help) usage ;;
38
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
39
+ esac
40
+ done
41
+
42
+ # Find available port if not specified
43
+ if [[ "$PORT" == "0" ]]; then
44
+ PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()" 2>/dev/null || echo "52341")
45
+ fi
46
+
47
+ [[ -z "$URL_HOST" ]] && URL_HOST="$HOST"
48
+ [[ "$URL_HOST" == "0.0.0.0" ]] && URL_HOST="localhost"
49
+
50
+ # Create session directory
51
+ TIMESTAMP=$(date +%s)
52
+ PID_PREFIX="$$"
53
+
54
+ if [[ -n "$PROJECT_DIR" ]]; then
55
+ SESSION_DIR="$PROJECT_DIR/.deliberate/companion/${PID_PREFIX}-${TIMESTAMP}"
56
+ else
57
+ SESSION_DIR="/tmp/deliberate-companion/${PID_PREFIX}-${TIMESTAMP}"
58
+ fi
59
+
60
+ SCREEN_DIR="$SESSION_DIR/content"
61
+ STATE_DIR="$SESSION_DIR/state"
62
+
63
+ mkdir -p "$SCREEN_DIR" "$STATE_DIR"
64
+
65
+ # Write the frame template and helper to the session
66
+ cp "$SCRIPT_DIR/frame-template.html" "$SESSION_DIR/frame-template.html"
67
+ cp "$SCRIPT_DIR/helper.js" "$SESSION_DIR/helper.js"
68
+
69
+ # Create the server script (Node.js)
70
+ cat > "$SESSION_DIR/server.js" << 'SERVEREOF'
71
+ const http = require('http');
72
+ const fs = require('fs');
73
+ const path = require('path');
74
+
75
+ const args = process.argv.slice(2);
76
+ const HOST = args[0] || '127.0.0.1';
77
+ const PORT = parseInt(args[1] || '52341', 10);
78
+ const SCREEN_DIR = args[2];
79
+ const STATE_DIR = args[3];
80
+ const SESSION_DIR = args[4];
81
+
82
+ const FRAME_TEMPLATE = fs.readFileSync(path.join(SESSION_DIR, 'frame-template.html'), 'utf8');
83
+ const HELPER_JS = fs.readFileSync(path.join(SESSION_DIR, 'helper.js'), 'utf8');
84
+
85
+ let inactivityTimer = null;
86
+ const INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutes
87
+
88
+ function resetInactivityTimer() {
89
+ if (inactivityTimer) clearTimeout(inactivityTimer);
90
+ inactivityTimer = setTimeout(() => {
91
+ console.log('Inactivity timeout reached. Shutting down.');
92
+ fs.writeFileSync(path.join(STATE_DIR, 'server-stopped'), Date.now().toString());
93
+ process.exit(0);
94
+ }, INACTIVITY_TIMEOUT);
95
+ }
96
+
97
+ function getNewestFile() {
98
+ try {
99
+ const files = fs.readdirSync(SCREEN_DIR)
100
+ .filter(f => f.endsWith('.html'))
101
+ .map(f => ({
102
+ name: f,
103
+ mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtimeMs
104
+ }))
105
+ .sort((a, b) => b.mtime - a.mtime);
106
+ return files.length > 0 ? files[0].name : null;
107
+ } catch (e) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function wrapInFrame(content, filename) {
113
+ // If content is a full HTML document, just inject the helper script
114
+ if (content.trim().startsWith('<!DOCTYPE') || content.trim().startsWith('<html')) {
115
+ return content.replace('</body>', `<script>${HELPER_JS}</script></body>`);
116
+ }
117
+ // Otherwise, wrap in frame template
118
+ return FRAME_TEMPLATE
119
+ .replace('{{CONTENT}}', content)
120
+ .replace('{{HELPER_JS}}', HELPER_JS)
121
+ .replace('{{FILENAME}}', filename || 'deliberate');
122
+ }
123
+
124
+ function getMimeType(ext) {
125
+ const types = {
126
+ '.html': 'text/html',
127
+ '.css': 'text/css',
128
+ '.js': 'application/javascript',
129
+ '.json': 'application/json',
130
+ '.png': 'image/png',
131
+ '.jpg': 'image/jpeg',
132
+ '.svg': 'image/svg+xml'
133
+ };
134
+ return types[ext] || 'text/plain';
135
+ }
136
+
137
+ const server = http.createServer((req, res) => {
138
+ resetInactivityTimer();
139
+
140
+ // CORS headers
141
+ res.setHeader('Access-Control-Allow-Origin', '*');
142
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
143
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
144
+
145
+ if (req.method === 'OPTIONS') {
146
+ res.writeHead(204);
147
+ res.end();
148
+ return;
149
+ }
150
+
151
+ // POST /events -- record user interaction
152
+ if (req.method === 'POST' && req.url === '/events') {
153
+ let body = '';
154
+ req.on('data', chunk => body += chunk);
155
+ req.on('end', () => {
156
+ const eventsFile = path.join(STATE_DIR, 'events');
157
+ fs.appendFileSync(eventsFile, body + '\n');
158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
159
+ res.end('{"status":"ok"}');
160
+ });
161
+ return;
162
+ }
163
+
164
+ // GET /events -- read events
165
+ if (req.method === 'GET' && req.url === '/events') {
166
+ const eventsFile = path.join(STATE_DIR, 'events');
167
+ if (fs.existsSync(eventsFile)) {
168
+ res.writeHead(200, { 'Content-Type': 'application/json' });
169
+ res.end(fs.readFileSync(eventsFile, 'utf8'));
170
+ } else {
171
+ res.writeHead(200, { 'Content-Type': 'application/json' });
172
+ res.end('');
173
+ }
174
+ return;
175
+ }
176
+
177
+ // GET /status -- health check
178
+ if (req.url === '/status') {
179
+ res.writeHead(200, { 'Content-Type': 'application/json' });
180
+ res.end(JSON.stringify({ status: 'running', newest: getNewestFile() }));
181
+ return;
182
+ }
183
+
184
+ // GET / -- serve newest HTML file wrapped in frame
185
+ if (req.url === '/' || req.url === '/index.html') {
186
+ const newest = getNewestFile();
187
+ if (!newest) {
188
+ const waiting = `
189
+ <div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column">
190
+ <h2 style="color:var(--text-primary,#e0e0e0);font-family:system-ui">deliberate</h2>
191
+ <p style="color:var(--text-secondary,#999);font-family:system-ui">Waiting for content...</p>
192
+ </div>`;
193
+ res.writeHead(200, { 'Content-Type': 'text/html' });
194
+ res.end(wrapInFrame(waiting, 'waiting'));
195
+ return;
196
+ }
197
+ const content = fs.readFileSync(path.join(SCREEN_DIR, newest), 'utf8');
198
+ // Clear events when new screen is served
199
+ const eventsFile = path.join(STATE_DIR, 'events');
200
+ if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
201
+
202
+ res.writeHead(200, { 'Content-Type': 'text/html' });
203
+ res.end(wrapInFrame(content, newest));
204
+ return;
205
+ }
206
+
207
+ // Serve static files from screen_dir
208
+ const filePath = path.join(SCREEN_DIR, req.url.slice(1));
209
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
210
+ const ext = path.extname(filePath);
211
+ res.writeHead(200, { 'Content-Type': getMimeType(ext) });
212
+ res.end(fs.readFileSync(filePath));
213
+ return;
214
+ }
215
+
216
+ res.writeHead(404);
217
+ res.end('Not found');
218
+ });
219
+
220
+ server.listen(PORT, HOST, () => {
221
+ const info = {
222
+ type: 'server-started',
223
+ port: PORT,
224
+ url: `http://${args[5] || HOST}:${PORT}`,
225
+ screen_dir: SCREEN_DIR,
226
+ state_dir: STATE_DIR,
227
+ session_dir: SESSION_DIR
228
+ };
229
+
230
+ // Write server info for retrieval
231
+ fs.writeFileSync(path.join(STATE_DIR, 'server-info'), JSON.stringify(info, null, 2));
232
+
233
+ // Output to stdout for the launching script
234
+ console.log(JSON.stringify(info));
235
+
236
+ resetInactivityTimer();
237
+ });
238
+
239
+ process.on('SIGTERM', () => {
240
+ fs.writeFileSync(path.join(STATE_DIR, 'server-stopped'), Date.now().toString());
241
+ process.exit(0);
242
+ });
243
+ process.on('SIGINT', () => {
244
+ fs.writeFileSync(path.join(STATE_DIR, 'server-stopped'), Date.now().toString());
245
+ process.exit(0);
246
+ });
247
+ SERVEREOF
248
+
249
+ # Auto-detect environment
250
+ IS_CODEX=false
251
+ [[ -n "${CODEX_CI:-}" ]] && IS_CODEX=true
252
+
253
+ # Launch the server
254
+ if [[ "$FOREGROUND" == "true" ]] || [[ "$IS_CODEX" == "true" ]]; then
255
+ # Foreground mode: blocks the shell
256
+ exec node "$SESSION_DIR/server.js" "$HOST" "$PORT" "$SCREEN_DIR" "$STATE_DIR" "$SESSION_DIR" "$URL_HOST"
257
+ else
258
+ # Background mode: detach and return immediately
259
+ nohup node "$SESSION_DIR/server.js" "$HOST" "$PORT" "$SCREEN_DIR" "$STATE_DIR" "$SESSION_DIR" "$URL_HOST" > /dev/null 2>&1 &
260
+ SERVER_PID=$!
261
+
262
+ # Wait briefly for the server to start and write server-info
263
+ for i in 1 2 3 4 5; do
264
+ if [[ -f "$STATE_DIR/server-info" ]]; then
265
+ cat "$STATE_DIR/server-info"
266
+ exit 0
267
+ fi
268
+ sleep 0.5
269
+ done
270
+
271
+ # If server-info wasn't written, something went wrong
272
+ echo '{"type":"error","message":"Server failed to start within 2.5 seconds"}' >&2
273
+ exit 1
274
+ fi
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Stop a deliberate visual companion server session
5
+
6
+ if [[ $# -lt 1 ]]; then
7
+ echo "Usage: stop-server.sh SESSION_DIR" >&2
8
+ exit 1
9
+ fi
10
+
11
+ SESSION_DIR="$1"
12
+ STATE_DIR="$SESSION_DIR/state"
13
+
14
+ if [[ ! -d "$SESSION_DIR" ]]; then
15
+ echo "Session directory not found: $SESSION_DIR" >&2
16
+ exit 1
17
+ fi
18
+
19
+ # Read server info to find the process
20
+ if [[ -f "$STATE_DIR/server-info" ]]; then
21
+ # Find the node process serving this session
22
+ SERVER_JS="$SESSION_DIR/server.js"
23
+ PIDS=$(pgrep -f "$SERVER_JS" 2>/dev/null || true)
24
+
25
+ if [[ -n "$PIDS" ]]; then
26
+ echo "$PIDS" | xargs kill 2>/dev/null || true
27
+ echo "Server stopped."
28
+ else
29
+ echo "No running server found for this session."
30
+ fi
31
+ fi
32
+
33
+ # Mark as stopped
34
+ echo "$(date +%s)" > "$STATE_DIR/server-stopped"
35
+
36
+ # Clean up /tmp sessions only
37
+ if [[ "$SESSION_DIR" == /tmp/* ]]; then
38
+ rm -rf "$SESSION_DIR"
39
+ echo "Temporary session cleaned up."
40
+ else
41
+ echo "Session files preserved at: $SESSION_DIR"
42
+ fi
@@ -0,0 +1,60 @@
1
+ # Brainstorm Record
2
+
3
+ ## Metadata
4
+ - **Date**: {{DATE}}
5
+ - **Agents**: {{AGENTS}}
6
+ - **Topic**: {{TOPIC}}
7
+ - **Visual Companion**: {{VISUAL_COMPANION}}
8
+ - **Platform**: {{PLATFORM}}
9
+
10
+ ---
11
+
12
+ ## Context Summary
13
+
14
+ {{CONTEXT_SUMMARY}}
15
+
16
+ ---
17
+
18
+ ## Clarifying Questions & Answers
19
+
20
+ {{CLARIFYING_QA}}
21
+
22
+ ---
23
+
24
+ ## Divergent Phase: Ideas Generated
25
+
26
+ {{DIVERGENT_IDEAS}}
27
+
28
+ ---
29
+
30
+ ## Cross-Pollination
31
+
32
+ {{CROSS_POLLINATION}}
33
+
34
+ ---
35
+
36
+ ## Convergence: Top Directions
37
+
38
+ {{CONVERGENCE}}
39
+
40
+ ---
41
+
42
+ ## Final Design
43
+
44
+ ### Overview
45
+ {{DESIGN_OVERVIEW}}
46
+
47
+ ### Architecture / Structure
48
+ {{DESIGN_ARCHITECTURE}}
49
+
50
+ ### Key Decisions
51
+ {{DESIGN_DECISIONS}}
52
+
53
+ ### Scope Boundaries (what we're NOT building)
54
+ {{SCOPE_BOUNDARIES}}
55
+
56
+ ### Risks and Mitigations
57
+ {{DESIGN_RISKS}}
58
+
59
+ ### Agent Attribution
60
+ {{AGENT_ATTRIBUTION}}
@@ -0,0 +1,64 @@
1
+ # Deliberation Record
2
+
3
+ ## Metadata
4
+ - **Date**: {{DATE}}
5
+ - **Mode**: {{MODE}}
6
+ - **Agents**: {{AGENTS}}
7
+ - **Question**: {{QUESTION}}
8
+ - **Profile**: {{PROFILE}}
9
+ - **Platform**: {{PLATFORM}}
10
+
11
+ ---
12
+
13
+ ## Problem Restatement
14
+
15
+ {{PROBLEM_RESTATEMENT}}
16
+
17
+ ---
18
+
19
+ ## Round 1: Independent Analysis
20
+
21
+ {{ROUND_1_OUTPUTS}}
22
+
23
+ ---
24
+
25
+ ## Round 2: Cross-Examination
26
+
27
+ {{ROUND_2_OUTPUTS}}
28
+
29
+ ### Enforcement Notes
30
+ {{ENFORCEMENT_NOTES}}
31
+
32
+ ---
33
+
34
+ ## Round 3: Crystallization
35
+
36
+ {{ROUND_3_OUTPUTS}}
37
+
38
+ ---
39
+
40
+ ## Verdict
41
+
42
+ ### Verdict Type
43
+ {{VERDICT_TYPE}}
44
+
45
+ ### Consensus Position
46
+ {{CONSENSUS_POSITION}}
47
+
48
+ ### Key Insights by Agent
49
+ {{KEY_INSIGHTS}}
50
+
51
+ ### Points of Agreement
52
+ {{AGREEMENT_POINTS}}
53
+
54
+ ### Points of Disagreement
55
+ {{DISAGREEMENT_POINTS}}
56
+
57
+ ### Minority Report
58
+ {{MINORITY_REPORT}}
59
+
60
+ ### Recommended Next Steps
61
+ {{NEXT_STEPS}}
62
+
63
+ ### Unresolved Questions
64
+ {{UNRESOLVED_QUESTIONS}}