phewsh 0.15.1 → 0.15.3
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/commands/ai.js +2 -2
- package/commands/browse.js +2 -2
- package/commands/clarify.js +2 -2
- package/commands/gate.js +2 -2
- package/commands/intent.js +2 -2
- package/commands/login.js +3 -4
- package/commands/mcp.js +4 -3
- package/commands/serve.js +33 -22
- package/commands/session.js +46 -18
- package/commands/setup.js +14 -5
- package/commands/style.js +2 -1
- package/commands/sync.js +3 -5
- package/commands/watch.js +3 -5
- package/lib/config-file.js +36 -0
- package/lib/cors.js +48 -0
- package/lib/harnesses.js +1 -1
- package/lib/session-input.js +67 -0
- package/mcp/http-server.js +42 -42
- package/package.json +1 -1
package/commands/ai.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
buildHeaders, buildBody, getUrl, streamParser,
|
|
8
8
|
} = require('../lib/providers');
|
|
9
9
|
const { HARNESSES, detectInstalled, listHarnesses, runViaHarness } = require('../lib/harnesses');
|
|
10
|
+
const configFile = require('../lib/config-file');
|
|
10
11
|
|
|
11
12
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
12
13
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
@@ -15,8 +16,7 @@ const args = process.argv.slice(3);
|
|
|
15
16
|
const subcommand = args[0];
|
|
16
17
|
|
|
17
18
|
function loadConfig() {
|
|
18
|
-
|
|
19
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
19
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function loadIntentContext() {
|
package/commands/browse.js
CHANGED
|
@@ -4,6 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const {
|
|
5
5
|
getProvider, buildHeaders, buildBody, getUrl, streamParser, detectProvider,
|
|
6
6
|
} = require('../lib/providers');
|
|
7
|
+
const configFile = require('../lib/config-file');
|
|
7
8
|
|
|
8
9
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
9
10
|
|
|
@@ -17,8 +18,7 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
|
17
18
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
18
19
|
|
|
19
20
|
function loadConfig() {
|
|
20
|
-
|
|
21
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
21
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function resolveApiKey(config, provider) {
|
package/commands/clarify.js
CHANGED
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const readline = require('readline');
|
|
10
10
|
const { readPPS, writePPS, createPPS, generateViews } = require('../lib/pps');
|
|
11
|
+
const configFile = require('../lib/config-file');
|
|
11
12
|
|
|
12
13
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
13
14
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
@@ -18,8 +19,7 @@ const rawFromFlag = textFlag !== -1 ? args.slice(textFlag + 1).join(' ') : null;
|
|
|
18
19
|
const isUpdate = args.includes('--update') || args.includes('-u');
|
|
19
20
|
|
|
20
21
|
function loadConfig() {
|
|
21
|
-
|
|
22
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
22
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function getProjectName() {
|
package/commands/gate.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const readline = require('readline');
|
|
9
|
+
const configFile = require('../lib/config-file');
|
|
9
10
|
|
|
10
11
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
11
12
|
const PROJECT_PATH = path.join(INTENT_DIR, 'project.json');
|
|
@@ -91,8 +92,7 @@ function saveGate(gate) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function loadConfig() {
|
|
94
|
-
|
|
95
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
95
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function createPrompter() {
|
package/commands/intent.js
CHANGED
|
@@ -5,6 +5,7 @@ const { execSync } = require('child_process');
|
|
|
5
5
|
const { createPPS, writePPS, generateViews } = require('../lib/pps');
|
|
6
6
|
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const configFile = require('../lib/config-file');
|
|
8
9
|
const args = process.argv.slice(3);
|
|
9
10
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
10
11
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
@@ -156,8 +157,7 @@ async function initIntent() {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
function loadConfig() {
|
|
159
|
-
|
|
160
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
160
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function loadGate() {
|
package/commands/login.js
CHANGED
|
@@ -4,18 +4,17 @@ const readline = require('readline');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { sendOtp, verifyOtp, refreshSession } = require('../lib/supabase');
|
|
7
|
+
const configFile = require('../lib/config-file');
|
|
7
8
|
|
|
8
9
|
const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
|
|
9
10
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
10
11
|
|
|
11
12
|
function loadConfig() {
|
|
12
|
-
|
|
13
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
13
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function saveConfig(config) {
|
|
17
|
-
|
|
18
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
17
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function createPrompter() {
|
package/commands/mcp.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
|
+
const configFile = require('../lib/config-file');
|
|
16
17
|
const { spawn } = require('child_process');
|
|
17
18
|
|
|
18
19
|
const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
|
|
@@ -79,8 +80,8 @@ async function loadCloudProjects() {
|
|
|
79
80
|
const configPath = path.join(PHEWSH_DIR, 'config.json');
|
|
80
81
|
if (!fs.existsSync(configPath)) return [];
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const config = configFile.loadConfig(configPath);
|
|
84
|
+
if (!config) return [];
|
|
84
85
|
if (!config?.supabaseAccessToken || !config?.supabaseUserId) return [];
|
|
85
86
|
|
|
86
87
|
try {
|
|
@@ -92,7 +93,7 @@ async function loadCloudProjects() {
|
|
|
92
93
|
if (session?.access_token) {
|
|
93
94
|
config.supabaseAccessToken = session.access_token;
|
|
94
95
|
config.supabaseRefreshToken = session.refresh_token;
|
|
95
|
-
|
|
96
|
+
configFile.saveConfig(configPath, config);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
package/commands/serve.js
CHANGED
|
@@ -17,6 +17,8 @@ const crypto = require('crypto');
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const fs = require('fs');
|
|
20
|
+
const { corsHeaders, isAllowedRequest } = require('../lib/cors');
|
|
21
|
+
const configFile = require('../lib/config-file');
|
|
20
22
|
|
|
21
23
|
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
22
24
|
const g = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
@@ -204,14 +206,14 @@ function parseBody(req) {
|
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
207
|
-
function cors(res) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
function cors(req, res) {
|
|
210
|
+
for (const [name, value] of Object.entries(corsHeaders(req))) {
|
|
211
|
+
res.setHeader(name, value);
|
|
212
|
+
}
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
function json(res, data, status = 200) {
|
|
214
|
-
cors(res);
|
|
215
|
+
function json(req, res, data, status = 200) {
|
|
216
|
+
cors(req, res);
|
|
215
217
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
216
218
|
res.end(JSON.stringify(data));
|
|
217
219
|
}
|
|
@@ -221,12 +223,16 @@ function main() {
|
|
|
221
223
|
const runtimes = detectRuntimes();
|
|
222
224
|
const hasClaudeCode = runtimes.find(r => r.id === 'claude-code')?.connected;
|
|
223
225
|
|
|
224
|
-
const
|
|
226
|
+
const handleRequest = async (req, res) => {
|
|
225
227
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
226
228
|
|
|
229
|
+
if (!isAllowedRequest(req)) {
|
|
230
|
+
return json(req, res, { error: 'Origin not allowed' }, 403);
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
// CORS preflight
|
|
228
234
|
if (req.method === 'OPTIONS') {
|
|
229
|
-
cors(res);
|
|
235
|
+
cors(req, res);
|
|
230
236
|
res.writeHead(204);
|
|
231
237
|
res.end();
|
|
232
238
|
return;
|
|
@@ -234,7 +240,7 @@ function main() {
|
|
|
234
240
|
|
|
235
241
|
// Health check
|
|
236
242
|
if (url.pathname === '/health' && req.method === 'GET') {
|
|
237
|
-
return json(res, {
|
|
243
|
+
return json(req, res, {
|
|
238
244
|
status: 'ok',
|
|
239
245
|
runtimes: detectRuntimes(),
|
|
240
246
|
version: require('../package.json').version,
|
|
@@ -249,7 +255,7 @@ function main() {
|
|
|
249
255
|
const { actionId, runtimeId, packet } = body;
|
|
250
256
|
|
|
251
257
|
if (!actionId || !runtimeId || !packet) {
|
|
252
|
-
return json(res, { error: 'Missing actionId, runtimeId, or packet' }, 400);
|
|
258
|
+
return json(req, res, { error: 'Missing actionId, runtimeId, or packet' }, 400);
|
|
253
259
|
}
|
|
254
260
|
|
|
255
261
|
const jobId = createJob(actionId, runtimeId, packet);
|
|
@@ -258,9 +264,9 @@ function main() {
|
|
|
258
264
|
// Start execution in background
|
|
259
265
|
executeJob(jobId);
|
|
260
266
|
|
|
261
|
-
return json(res, { jobId, status: 'queued' });
|
|
267
|
+
return json(req, res, { jobId, status: 'queued' });
|
|
262
268
|
} catch (err) {
|
|
263
|
-
return json(res, { error: err.message }, 400);
|
|
269
|
+
return json(req, res, { error: err.message }, 400);
|
|
264
270
|
}
|
|
265
271
|
}
|
|
266
272
|
|
|
@@ -272,8 +278,7 @@ function main() {
|
|
|
272
278
|
const { outcomeStats, pendingDecisions, bypassStats } = require('../lib/outcomes');
|
|
273
279
|
const { listProjects } = require('../lib/projects-index');
|
|
274
280
|
|
|
275
|
-
|
|
276
|
-
try { config = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.phewsh', 'config.json'), 'utf-8')) || {}; } catch { /* none */ }
|
|
281
|
+
const config = configFile.loadConfig(path.join(os.homedir(), '.phewsh', 'config.json'), {});
|
|
277
282
|
|
|
278
283
|
const harnessList = listHarnesses().map(h => ({
|
|
279
284
|
id: h.id, label: h.label, role: h.role, installed: h.installed, headless: h.headless,
|
|
@@ -290,7 +295,7 @@ function main() {
|
|
|
290
295
|
const intentFiles = ['vision.md', 'plan.md', 'next.md']
|
|
291
296
|
.filter(f => fs.existsSync(path.join(process.cwd(), '.intent', f)));
|
|
292
297
|
|
|
293
|
-
return json(res, {
|
|
298
|
+
return json(req, res, {
|
|
294
299
|
project: { name: path.basename(process.cwd()), cwd: process.cwd(), intentFiles },
|
|
295
300
|
route: routeId === 'api'
|
|
296
301
|
? { id: 'api', label: `API (${config.provider || 'anthropic'} key)` }
|
|
@@ -305,7 +310,7 @@ function main() {
|
|
|
305
310
|
version: require('../package.json').version,
|
|
306
311
|
});
|
|
307
312
|
} catch (err) {
|
|
308
|
-
return json(res, { error: err.message }, 500);
|
|
313
|
+
return json(req, res, { error: err.message }, 500);
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
316
|
|
|
@@ -315,7 +320,7 @@ function main() {
|
|
|
315
320
|
if (url.pathname === '/receipts' && req.method === 'GET') {
|
|
316
321
|
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50', 10), 200);
|
|
317
322
|
const project = url.searchParams.get('project') || null;
|
|
318
|
-
return json(res, gatherReceipts({ project, limit }));
|
|
323
|
+
return json(req, res, gatherReceipts({ project, limit }));
|
|
319
324
|
}
|
|
320
325
|
|
|
321
326
|
// Check job status
|
|
@@ -323,8 +328,8 @@ function main() {
|
|
|
323
328
|
if (statusMatch && req.method === 'GET') {
|
|
324
329
|
const jobId = statusMatch[1];
|
|
325
330
|
const job = jobs.get(jobId);
|
|
326
|
-
if (!job) return json(res, { error: 'Job not found' }, 404);
|
|
327
|
-
return json(res, {
|
|
331
|
+
if (!job) return json(req, res, { error: 'Job not found' }, 404);
|
|
332
|
+
return json(req, res, {
|
|
328
333
|
jobId: job.jobId,
|
|
329
334
|
status: job.status,
|
|
330
335
|
statusText: job.statusText,
|
|
@@ -334,10 +339,16 @@ function main() {
|
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
// 404
|
|
337
|
-
json(res, { error: 'Not found' }, 404);
|
|
338
|
-
}
|
|
342
|
+
json(req, res, { error: 'Not found' }, 404);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const server = http.createServer(handleRequest);
|
|
346
|
+
|
|
347
|
+
server.listen(port, '127.0.0.1', () => {
|
|
348
|
+
const mirror = http.createServer(handleRequest);
|
|
349
|
+
mirror.on('error', () => { /* IPv6 unavailable or already bound */ });
|
|
350
|
+
mirror.listen(port, '::1');
|
|
339
351
|
|
|
340
|
-
server.listen(port, () => {
|
|
341
352
|
console.log('');
|
|
342
353
|
console.log(` ${b(w('PHEWSH Serve'))} ${g('v' + require('../package.json').version)}`);
|
|
343
354
|
console.log(` ${g('Live execution bridge for phewsh.com/intent')}`);
|
package/commands/session.js
CHANGED
|
@@ -20,6 +20,8 @@ const { push, pull, ensureValidToken } = require('./sync');
|
|
|
20
20
|
const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
|
|
21
21
|
const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
22
22
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
23
|
+
const configFile = require('../lib/config-file');
|
|
24
|
+
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
23
25
|
const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib/projects-index');
|
|
24
26
|
|
|
25
27
|
// Brand palette shortcuts
|
|
@@ -152,13 +154,11 @@ const INTENT_MODES = {
|
|
|
152
154
|
};
|
|
153
155
|
|
|
154
156
|
function loadConfig() {
|
|
155
|
-
|
|
156
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
157
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
function saveConfig(config) {
|
|
160
|
-
|
|
161
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
161
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function loadIntentContext() {
|
|
@@ -491,6 +491,8 @@ async function main() {
|
|
|
491
491
|
|
|
492
492
|
// ── Turn runners — every route records a decision, leaves a receipt ────
|
|
493
493
|
// Both return true on success so the fallback flow can chain them.
|
|
494
|
+
const failureTracker = createFailureTracker();
|
|
495
|
+
let lastTurnFailure = null;
|
|
494
496
|
|
|
495
497
|
async function runHarnessTurn(input, harnessId, fullSystem) {
|
|
496
498
|
const decisionId = recordDecision({
|
|
@@ -506,6 +508,7 @@ async function main() {
|
|
|
506
508
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
507
509
|
taskId: decisionId, success: true, summary: input.slice(0, 140),
|
|
508
510
|
});
|
|
511
|
+
lastTurnFailure = null;
|
|
509
512
|
awaitingOutcome = decisionId;
|
|
510
513
|
console.log(slate(` via ${HARNESSES[harnessId].label} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
|
|
511
514
|
return true;
|
|
@@ -520,8 +523,11 @@ async function main() {
|
|
|
520
523
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
521
524
|
taskId: decisionId, success: false, summary: input.slice(0, 140),
|
|
522
525
|
});
|
|
523
|
-
const
|
|
524
|
-
|
|
526
|
+
const failure = failureTracker.classify(harnessId, err.message);
|
|
527
|
+
lastTurnFailure = { ...failure, harnessId };
|
|
528
|
+
if (!failure.duplicate) {
|
|
529
|
+
console.error(`\n ${ember('!')} ${cream(HARNESSES[harnessId].label)} ${sage(failure.kind === 'usage-limit' ? 'hit a usage wall' : 'failed')}${slate(' — ' + err.message.split('\n')[0])}`);
|
|
530
|
+
}
|
|
525
531
|
return false;
|
|
526
532
|
}
|
|
527
533
|
}
|
|
@@ -574,10 +580,17 @@ async function main() {
|
|
|
574
580
|
// Fallbacks are a first-class flow: the route changes, the context and
|
|
575
581
|
// record do not. Ask by default; auto-switch only if setup said so.
|
|
576
582
|
async function offerFallbacks(input, fullSystem, failedId) {
|
|
583
|
+
if (lastTurnFailure?.duplicate && lastTurnFailure.harnessId === failedId) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
577
587
|
const options = harnesses
|
|
578
588
|
.filter(h => h.installed && h.headless && h.id !== failedId)
|
|
579
589
|
.map(h => h.id);
|
|
580
590
|
if (config?.apiKey && failedId !== 'api') options.push('api');
|
|
591
|
+
if (lastTurnFailure?.kind === 'usage-limit' && lastTurnFailure.harnessId === failedId) {
|
|
592
|
+
options.push(failedId);
|
|
593
|
+
}
|
|
581
594
|
|
|
582
595
|
if (options.length === 0) {
|
|
583
596
|
console.log(` ${sage('No fallback ready.')} ${slate('Install Codex or Gemini, or add an API key with /key — context would travel automatically.')}`);
|
|
@@ -598,8 +611,14 @@ async function main() {
|
|
|
598
611
|
}
|
|
599
612
|
|
|
600
613
|
const list = options.map((id, i) =>
|
|
601
|
-
`${teal(String(i + 1))} ${sage(id === 'api'
|
|
614
|
+
`${teal(String(i + 1))} ${sage(id === 'api'
|
|
615
|
+
? 'direct API (your key)'
|
|
616
|
+
: id === failedId ? HARNESSES[id].label + ' (retry once)' : HARNESSES[id].label)}`
|
|
602
617
|
).join(slate(' · '));
|
|
618
|
+
if (lastTurnFailure?.kind === 'usage-limit' && lastTurnFailure.harnessId === failedId) {
|
|
619
|
+
const codexReady = options.includes('codex');
|
|
620
|
+
console.log(` ${sage(codexReady ? '/use codex switches the session now, or retry Claude once below.' : 'You can retry this route once below, or /use another installed route.')}`);
|
|
621
|
+
}
|
|
603
622
|
console.log(` ${sage('Retry with your context intact:')} ${list} ${slate('· enter = skip')}`);
|
|
604
623
|
console.log(` ${slate('prefer auto-switching? phewsh setup sets it once')}`);
|
|
605
624
|
awaitingFallback = { input, fullSystem, options };
|
|
@@ -680,13 +699,7 @@ async function main() {
|
|
|
680
699
|
|
|
681
700
|
rl.prompt();
|
|
682
701
|
|
|
683
|
-
|
|
684
|
-
const input = line.trim();
|
|
685
|
-
|
|
686
|
-
if (!input) {
|
|
687
|
-
rl.prompt();
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
702
|
+
async function handleInput(input) {
|
|
690
703
|
|
|
691
704
|
// A bare number right after a route failure picks the fallback
|
|
692
705
|
if (awaitingFallback) {
|
|
@@ -1547,8 +1560,11 @@ async function main() {
|
|
|
1547
1560
|
if (!config?.apiKey) {
|
|
1548
1561
|
console.log(` ${ember('!')} ${sage('No API key set — run /key first.')}`);
|
|
1549
1562
|
} else {
|
|
1563
|
+
config = loadConfig() || {};
|
|
1564
|
+
config.defaultRoute = 'api';
|
|
1565
|
+
saveConfig(config);
|
|
1550
1566
|
route = { type: 'api' };
|
|
1551
|
-
console.log(` ${teal('●')} ${sage('
|
|
1567
|
+
console.log(` ${teal('●')} ${sage('Default route:')} ${cream(routeLabel(route, config))}`);
|
|
1552
1568
|
}
|
|
1553
1569
|
} else if (HARNESSES[target]) {
|
|
1554
1570
|
if (!harnesses.find(h => h.id === target)?.installed) {
|
|
@@ -1556,13 +1572,15 @@ async function main() {
|
|
|
1556
1572
|
} else if (!HARNESSES[target].args) {
|
|
1557
1573
|
console.log(` ${sage(HARNESSES[target].label + ' is interactive-only — drop into it with')} ${cream('/work ' + target)} ${sage('(phewsh records the outcome when you return)')}`);
|
|
1558
1574
|
} else {
|
|
1575
|
+
config = loadConfig() || {};
|
|
1576
|
+
config.defaultRoute = target;
|
|
1577
|
+
saveConfig(config);
|
|
1559
1578
|
route = { type: 'harness', id: target };
|
|
1560
|
-
console.log(` ${teal('●')} ${sage('
|
|
1579
|
+
console.log(` ${teal('●')} ${sage('Default route:')} ${cream(routeLabel(route, config))} ${slate('— saved across sessions')}`);
|
|
1561
1580
|
}
|
|
1562
1581
|
} else {
|
|
1563
1582
|
console.log(` ${sage('Unknown route. Options:')} ${cream(Object.keys(HARNESSES).join(', ') + ', api')}`);
|
|
1564
1583
|
}
|
|
1565
|
-
console.log(` ${slate('make it stick across sessions: phewsh setup')}`);
|
|
1566
1584
|
rl.prompt();
|
|
1567
1585
|
return;
|
|
1568
1586
|
}
|
|
@@ -1806,9 +1824,19 @@ async function main() {
|
|
|
1806
1824
|
|
|
1807
1825
|
console.log('');
|
|
1808
1826
|
rl.prompt();
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
1830
|
+
onNoop: () => rl.prompt(),
|
|
1831
|
+
onError: (err) => {
|
|
1832
|
+
console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
|
|
1833
|
+
rl.prompt();
|
|
1834
|
+
},
|
|
1809
1835
|
});
|
|
1836
|
+
rl.on('line', lineDispatcher.push);
|
|
1810
1837
|
|
|
1811
|
-
rl.on('close', () => {
|
|
1838
|
+
rl.on('close', async () => {
|
|
1839
|
+
await lineDispatcher.drain();
|
|
1812
1840
|
console.log(`\n ${sage('session ended')}\n`);
|
|
1813
1841
|
process.exit(0);
|
|
1814
1842
|
});
|
package/commands/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ const os = require('os');
|
|
|
10
10
|
const readline = require('readline');
|
|
11
11
|
const ui = require('../lib/ui');
|
|
12
12
|
const { HARNESSES, listHarnesses } = require('../lib/harnesses');
|
|
13
|
+
const configFile = require('../lib/config-file');
|
|
13
14
|
|
|
14
15
|
const { b, teal, sage, slate, cream, ember, green } = ui;
|
|
15
16
|
|
|
@@ -17,12 +18,11 @@ const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
|
|
|
17
18
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
18
19
|
|
|
19
20
|
function loadConfig() {
|
|
20
|
-
|
|
21
|
+
return configFile.loadConfig(CONFIG_PATH, {});
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function saveConfig(config) {
|
|
24
|
-
|
|
25
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
25
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function ask(rl, prompt) {
|
|
@@ -58,10 +58,19 @@ module.exports = async function setup() {
|
|
|
58
58
|
console.log('');
|
|
59
59
|
|
|
60
60
|
// Agent-run (no TTY): auto-configure instead of asking questions nobody
|
|
61
|
-
// can answer.
|
|
61
|
+
// can answer. Keep an existing valid route; otherwise pick the first
|
|
62
|
+
// installed harness.
|
|
62
63
|
const chatCapable = installed.filter(h => h.headless);
|
|
63
64
|
if (!process.stdin.isTTY) {
|
|
64
|
-
|
|
65
|
+
const configuredHarness = chatCapable.find(h => h.id === config.defaultRoute);
|
|
66
|
+
if (configuredHarness) {
|
|
67
|
+
if (!config.fallback) config.fallback = 'ask';
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
console.log(` ${teal('●')} ${sage('Kept configured default route:')} ${cream(configuredHarness.label)} ${slate('— no API key needed')}`);
|
|
70
|
+
} else if (config.defaultRoute === 'api' && config.apiKey) {
|
|
71
|
+
saveConfig(config);
|
|
72
|
+
console.log(` ${teal('●')} ${sage('Kept configured default route: API (existing key found)')}`);
|
|
73
|
+
} else if (chatCapable.length > 0) {
|
|
65
74
|
config.defaultRoute = chatCapable[0].id;
|
|
66
75
|
if (!config.fallback) config.fallback = 'ask';
|
|
67
76
|
saveConfig(config);
|
package/commands/style.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
const { select, upsert, SUPABASE_URL, SUPABASE_ANON_KEY } = require('../lib/supabase');
|
|
9
|
+
const configFile = require('../lib/config-file');
|
|
9
10
|
|
|
10
11
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
11
12
|
const STYLE_CACHE_DIR = path.join(os.homedir(), '.phewsh', 'styletree');
|
|
@@ -18,7 +19,7 @@ const c = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
|
18
19
|
const y = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
19
20
|
|
|
20
21
|
function loadConfig() {
|
|
21
|
-
|
|
22
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function ask(rl, q) {
|
package/commands/sync.js
CHANGED
|
@@ -7,6 +7,7 @@ const os = require('os');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { select, upsert, refreshSession } = require('../lib/supabase');
|
|
9
9
|
const { readPPS, writePPS } = require('../lib/pps');
|
|
10
|
+
const configFile = require('../lib/config-file');
|
|
10
11
|
|
|
11
12
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
12
13
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
@@ -15,14 +16,11 @@ const FILE_TO_KIND = { 'vision.md': 'vision', 'plan.md': 'plan', 'next.md': 'nex
|
|
|
15
16
|
const KIND_TO_FILE = { vision: 'vision.md', plan: 'plan.md', next: 'next.md' };
|
|
16
17
|
|
|
17
18
|
function loadConfig() {
|
|
18
|
-
|
|
19
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
19
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function saveConfig(config) {
|
|
23
|
-
|
|
24
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
23
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
function genProjectId() {
|
package/commands/watch.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
|
+
const configFile = require('../lib/config-file');
|
|
16
17
|
|
|
17
18
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
18
19
|
const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
@@ -38,14 +39,11 @@ const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
|
38
39
|
// ── Config + Auth ──────────────────────────────────────────────────────────
|
|
39
40
|
|
|
40
41
|
function loadConfig() {
|
|
41
|
-
|
|
42
|
-
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
42
|
+
return configFile.loadConfig(CONFIG_PATH);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function saveConfig(config) {
|
|
46
|
-
|
|
47
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
46
|
+
configFile.saveConfig(CONFIG_PATH, config);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// ── Context Generation (mirrors context.js) ────────────────────────────────
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function hardenConfigPath(configPath) {
|
|
5
|
+
const dir = path.dirname(configPath);
|
|
6
|
+
if (fs.existsSync(dir)) {
|
|
7
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* best effort on non-POSIX filesystems */ }
|
|
8
|
+
}
|
|
9
|
+
if (fs.existsSync(configPath)) {
|
|
10
|
+
try { fs.chmodSync(configPath, 0o600); } catch { /* best effort on non-POSIX filesystems */ }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadConfig(configPath, fallback = null) {
|
|
15
|
+
if (!fs.existsSync(configPath)) return fallback;
|
|
16
|
+
hardenConfigPath(configPath);
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function saveConfig(configPath, config) {
|
|
25
|
+
const dir = path.dirname(configPath);
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
27
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* best effort on non-POSIX filesystems */ }
|
|
28
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
29
|
+
try { fs.chmodSync(configPath, 0o600); } catch { /* existing files keep their prior mode */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
hardenConfigPath,
|
|
34
|
+
loadConfig,
|
|
35
|
+
saveConfig,
|
|
36
|
+
};
|
package/lib/cors.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const DEFAULT_ALLOWED_ORIGINS = new Set([
|
|
2
|
+
'https://phewsh.com',
|
|
3
|
+
'https://www.phewsh.com',
|
|
4
|
+
'http://localhost:3000',
|
|
5
|
+
'http://127.0.0.1:3000',
|
|
6
|
+
'http://[::1]:3000',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function allowedOrigins() {
|
|
10
|
+
const origins = new Set(DEFAULT_ALLOWED_ORIGINS);
|
|
11
|
+
for (const origin of (process.env.PHEWSH_ALLOWED_ORIGINS || '').split(',')) {
|
|
12
|
+
const trimmed = origin.trim();
|
|
13
|
+
if (trimmed) origins.add(trimmed);
|
|
14
|
+
}
|
|
15
|
+
return origins;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requestOrigin(req) {
|
|
19
|
+
const origin = req.headers.origin;
|
|
20
|
+
return Array.isArray(origin) ? origin[0] : origin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAllowedRequest(req) {
|
|
24
|
+
const origin = requestOrigin(req);
|
|
25
|
+
if (origin) return allowedOrigins().has(origin);
|
|
26
|
+
|
|
27
|
+
// CLI clients do not send browser fetch metadata. A browser can omit Origin
|
|
28
|
+
// on some cross-site requests, so reject those before they reach any route.
|
|
29
|
+
return req.headers['sec-fetch-site'] !== 'cross-site';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function corsHeaders(req) {
|
|
33
|
+
const origin = requestOrigin(req);
|
|
34
|
+
if (!origin || !allowedOrigins().has(origin)) return {};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
'Access-Control-Allow-Origin': origin,
|
|
38
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
39
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Phewsh-Runtime',
|
|
40
|
+
'Access-Control-Max-Age': '600',
|
|
41
|
+
Vary: 'Origin',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
corsHeaders,
|
|
47
|
+
isAllowedRequest,
|
|
48
|
+
};
|
package/lib/harnesses.js
CHANGED
|
@@ -21,7 +21,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
21
21
|
// model flag ignore the preference and use their own config.
|
|
22
22
|
const HARNESSES = {
|
|
23
23
|
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku', 'fable'], args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
24
|
-
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', ...(m ? ['-m', m] : []), p] },
|
|
24
|
+
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p] },
|
|
25
25
|
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])] },
|
|
26
26
|
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
27
27
|
'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function createLineDispatcher(handleInput, {
|
|
2
|
+
onError = (err) => { throw err; },
|
|
3
|
+
onNoop = () => {},
|
|
4
|
+
schedule = setImmediate,
|
|
5
|
+
} = {}) {
|
|
6
|
+
let pendingLines = [];
|
|
7
|
+
let scheduled = false;
|
|
8
|
+
let chain = Promise.resolve();
|
|
9
|
+
|
|
10
|
+
function flush() {
|
|
11
|
+
scheduled = false;
|
|
12
|
+
const lines = pendingLines;
|
|
13
|
+
pendingLines = [];
|
|
14
|
+
const input = lines.join('\n').trim();
|
|
15
|
+
if (!input) {
|
|
16
|
+
onNoop();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
chain = chain.then(() => handleInput(input)).catch(onError);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function push(line) {
|
|
23
|
+
pendingLines.push(String(line));
|
|
24
|
+
if (scheduled) return;
|
|
25
|
+
scheduled = true;
|
|
26
|
+
schedule(flush);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function drain() {
|
|
30
|
+
if (scheduled) flush();
|
|
31
|
+
await chain;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { push, drain };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createFailureTracker() {
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
|
|
40
|
+
function classify(harnessId, message) {
|
|
41
|
+
const text = String(message || '').trim();
|
|
42
|
+
const firstLine = text.split('\n')[0].trim();
|
|
43
|
+
const isClaudeUsageLimit = harnessId === 'claude-code'
|
|
44
|
+
&& /(usage|rate|session|weekly|monthly)?\s*(limit|quota)|exhaust|resets?\s/i.test(text);
|
|
45
|
+
|
|
46
|
+
if (!isClaudeUsageLimit) {
|
|
47
|
+
return { kind: 'failure', duplicate: false, key: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalized = text.toLowerCase().replace(/\s+/g, ' ').slice(0, 300);
|
|
51
|
+
const key = `${harnessId}:${normalized}`;
|
|
52
|
+
const count = (seen.get(key) || 0) + 1;
|
|
53
|
+
seen.set(key, count);
|
|
54
|
+
return {
|
|
55
|
+
kind: 'usage-limit',
|
|
56
|
+
duplicate: count > 1,
|
|
57
|
+
key,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { classify };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
createFailureTracker,
|
|
66
|
+
createLineDispatcher,
|
|
67
|
+
};
|
package/mcp/http-server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { URL } from "url";
|
|
|
27
27
|
|
|
28
28
|
import { readFileSync, readdirSync } from "fs";
|
|
29
29
|
import { join } from "path";
|
|
30
|
+
import corsPolicy from "../lib/cors.js";
|
|
30
31
|
|
|
31
32
|
import {
|
|
32
33
|
loadProjects, recordResult, recordSession, updateLocalStatusMd,
|
|
@@ -35,6 +36,8 @@ import {
|
|
|
35
36
|
import * as runtimes from "./lib/runtime-registry.js";
|
|
36
37
|
import * as queue from "./lib/dispatch-queue.js";
|
|
37
38
|
|
|
39
|
+
const { corsHeaders, isAllowedRequest } = corsPolicy;
|
|
40
|
+
|
|
38
41
|
// Version comes from the phewsh package this server ships inside — never hardcode it.
|
|
39
42
|
const VERSION = (() => {
|
|
40
43
|
try {
|
|
@@ -49,23 +52,16 @@ const DEFAULT_HOST = "127.0.0.1";
|
|
|
49
52
|
|
|
50
53
|
// ─── HTTP helpers ───────────────────────────────────────────────────────────
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
"Access-Control-Allow-Origin": "*",
|
|
54
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
55
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Phewsh-Runtime",
|
|
56
|
-
"Access-Control-Max-Age": "600",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function json(res, status, body) {
|
|
55
|
+
function json(req, res, status, body) {
|
|
60
56
|
res.writeHead(status, {
|
|
61
57
|
"Content-Type": "application/json",
|
|
62
|
-
...
|
|
58
|
+
...corsHeaders(req),
|
|
63
59
|
});
|
|
64
60
|
res.end(JSON.stringify(body));
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
function text(res, status, body) {
|
|
68
|
-
res.writeHead(status, { "Content-Type": "text/plain", ...
|
|
63
|
+
function text(req, res, status, body) {
|
|
64
|
+
res.writeHead(status, { "Content-Type": "text/plain", ...corsHeaders(req) });
|
|
69
65
|
res.end(body);
|
|
70
66
|
}
|
|
71
67
|
|
|
@@ -85,7 +81,7 @@ async function readJsonBody(req) {
|
|
|
85
81
|
|
|
86
82
|
function handleHealth(req, res) {
|
|
87
83
|
const connected = runtimes.list();
|
|
88
|
-
json(res, 200, {
|
|
84
|
+
json(req, res, 200, {
|
|
89
85
|
status: "ok",
|
|
90
86
|
runtimes: connected,
|
|
91
87
|
version: VERSION,
|
|
@@ -97,7 +93,7 @@ async function handleDispatch(req, res) {
|
|
|
97
93
|
try {
|
|
98
94
|
const body = await readJsonBody(req);
|
|
99
95
|
if (!body.packet || !body.packet.objective) {
|
|
100
|
-
return json(res, 400, { error: "Missing packet.objective" });
|
|
96
|
+
return json(req, res, 400, { error: "Missing packet.objective" });
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
// Auto-register the target runtime as "expected" so /health reflects
|
|
@@ -122,26 +118,26 @@ async function handleDispatch(req, res) {
|
|
|
122
118
|
taskSummary: body.packet?.objective?.task?.slice(0, 120),
|
|
123
119
|
});
|
|
124
120
|
|
|
125
|
-
json(res, 200, { jobId: job.jobId, status: job.status });
|
|
121
|
+
json(req, res, 200, { jobId: job.jobId, status: job.status });
|
|
126
122
|
} catch (err) {
|
|
127
|
-
json(res, 400, { error: err.message });
|
|
123
|
+
json(req, res, 400, { error: err.message });
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
function handleStatus(res, jobId) {
|
|
127
|
+
function handleStatus(req, res, jobId) {
|
|
132
128
|
const job = queue.getStatus(jobId);
|
|
133
|
-
if (!job) return json(res, 404, { error: "Job not found" });
|
|
129
|
+
if (!job) return json(req, res, 404, { error: "Job not found" });
|
|
134
130
|
const { packet, ...rest } = job;
|
|
135
|
-
json(res, 200, rest);
|
|
131
|
+
json(req, res, 200, rest);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
|
-
function handleResult(res, jobId) {
|
|
134
|
+
function handleResult(req, res, jobId) {
|
|
139
135
|
const job = queue.getStatus(jobId);
|
|
140
|
-
if (!job) return json(res, 404, { error: "Job not found" });
|
|
136
|
+
if (!job) return json(req, res, 404, { error: "Job not found" });
|
|
141
137
|
if (job.status !== "done" && job.status !== "error") {
|
|
142
|
-
return json(res, 202, { status: job.status, message: "Job not yet complete" });
|
|
138
|
+
return json(req, res, 202, { status: job.status, message: "Job not yet complete" });
|
|
143
139
|
}
|
|
144
|
-
json(res, 200, {
|
|
140
|
+
json(req, res, 200, {
|
|
145
141
|
jobId: job.jobId,
|
|
146
142
|
status: job.status,
|
|
147
143
|
result: job.result,
|
|
@@ -150,7 +146,7 @@ function handleResult(res, jobId) {
|
|
|
150
146
|
});
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
function handleReceipts(res, url) {
|
|
149
|
+
function handleReceipts(req, res, url) {
|
|
154
150
|
// The merged proof trail: every claim an agent made, with the evidence file
|
|
155
151
|
// behind it. Same data `phewsh receipts` shows in the terminal — exposed
|
|
156
152
|
// here so the web app can render it.
|
|
@@ -213,13 +209,13 @@ function handleReceipts(res, url) {
|
|
|
213
209
|
if (e.kind === "dispatch_enqueued") counts.dispatched++;
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
json(res, 200, {
|
|
212
|
+
json(req, res, 200, {
|
|
217
213
|
summary: { ...counts, totalEvents: filtered.length },
|
|
218
214
|
events: filtered.slice(0, limit),
|
|
219
215
|
});
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
function handleJobsList(res, url) {
|
|
218
|
+
function handleJobsList(req, res, url) {
|
|
223
219
|
const limit = Math.min(parseInt(url.searchParams.get("limit") || "50", 10), 200);
|
|
224
220
|
const statusFilter = url.searchParams.get("status") || undefined;
|
|
225
221
|
const jobs = queue.list({ limit, status: statusFilter }).map(j => {
|
|
@@ -229,20 +225,20 @@ function handleJobsList(res, url) {
|
|
|
229
225
|
summary: packet?.objective?.task?.slice(0, 140) || null,
|
|
230
226
|
};
|
|
231
227
|
});
|
|
232
|
-
json(res, 200, { jobs });
|
|
228
|
+
json(req, res, 200, { jobs });
|
|
233
229
|
}
|
|
234
230
|
|
|
235
|
-
function handleNextForRuntime(res, url
|
|
231
|
+
function handleNextForRuntime(req, res, url) {
|
|
236
232
|
const runtimeId = url.searchParams.get("runtime") || req.headers["x-phewsh-runtime"];
|
|
237
|
-
if (!runtimeId) return json(res, 400, { error: "Missing ?runtime= or X-Phewsh-Runtime header" });
|
|
233
|
+
if (!runtimeId) return json(req, res, 400, { error: "Missing ?runtime= or X-Phewsh-Runtime header" });
|
|
238
234
|
|
|
239
235
|
runtimes.register({ id: runtimeId, label: runtimeId, transport: "http" });
|
|
240
236
|
|
|
241
237
|
const job = queue.nextForRuntime(runtimeId);
|
|
242
|
-
if (!job) return json(res, 204, null);
|
|
238
|
+
if (!job) return json(req, res, 204, null);
|
|
243
239
|
|
|
244
240
|
queue.markExecuting(job.jobId, runtimeId, "Picked up by HTTP harness");
|
|
245
|
-
json(res, 200, { jobId: job.jobId, actionId: job.actionId, packet: job.packet });
|
|
241
|
+
json(req, res, 200, { jobId: job.jobId, actionId: job.actionId, packet: job.packet });
|
|
246
242
|
}
|
|
247
243
|
|
|
248
244
|
async function handleJobComplete(req, res, jobId) {
|
|
@@ -251,7 +247,7 @@ async function handleJobComplete(req, res, jobId) {
|
|
|
251
247
|
const { success = true, result = "", issues, agentId, projectId } = body;
|
|
252
248
|
|
|
253
249
|
const job = queue.getStatus(jobId);
|
|
254
|
-
if (!job) return json(res, 404, { error: "Job not found" });
|
|
250
|
+
if (!job) return json(req, res, 404, { error: "Job not found" });
|
|
255
251
|
|
|
256
252
|
const updated = success
|
|
257
253
|
? queue.complete(jobId, result)
|
|
@@ -279,9 +275,9 @@ async function handleJobComplete(req, res, jobId) {
|
|
|
279
275
|
|
|
280
276
|
if (agentId) runtimes.touch(agentId);
|
|
281
277
|
|
|
282
|
-
json(res, 200, { jobId, status: updated.status });
|
|
278
|
+
json(req, res, 200, { jobId, status: updated.status });
|
|
283
279
|
} catch (err) {
|
|
284
|
-
json(res, 400, { error: err.message });
|
|
280
|
+
json(req, res, 400, { error: err.message });
|
|
285
281
|
}
|
|
286
282
|
}
|
|
287
283
|
|
|
@@ -289,8 +285,12 @@ async function handleJobComplete(req, res, jobId) {
|
|
|
289
285
|
|
|
290
286
|
export function startHttpServer({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {}) {
|
|
291
287
|
const server = createServer(async (req, res) => {
|
|
288
|
+
if (!isAllowedRequest(req)) {
|
|
289
|
+
return json(req, res, 403, { error: "Origin not allowed" });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
292
|
if (req.method === "OPTIONS") {
|
|
293
|
-
res.writeHead(204,
|
|
293
|
+
res.writeHead(204, corsHeaders(req));
|
|
294
294
|
return res.end();
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -302,26 +302,26 @@ export function startHttpServer({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {
|
|
|
302
302
|
if (req.method === "POST" && pathname === "/dispatch") return await handleDispatch(req, res);
|
|
303
303
|
if (req.method === "GET" && pathname.startsWith("/status/")) {
|
|
304
304
|
const jobId = pathname.slice("/status/".length);
|
|
305
|
-
return handleStatus(res, jobId);
|
|
305
|
+
return handleStatus(req, res, jobId);
|
|
306
306
|
}
|
|
307
307
|
if (req.method === "GET" && pathname.startsWith("/result/")) {
|
|
308
308
|
const jobId = pathname.slice("/result/".length);
|
|
309
|
-
return handleResult(res, jobId);
|
|
309
|
+
return handleResult(req, res, jobId);
|
|
310
310
|
}
|
|
311
|
-
if (req.method === "GET" && pathname === "/jobs") return handleJobsList(res, url);
|
|
312
|
-
if (req.method === "GET" && pathname === "/receipts") return handleReceipts(res, url);
|
|
313
|
-
if (req.method === "GET" && pathname === "/next") return handleNextForRuntime(res, url
|
|
311
|
+
if (req.method === "GET" && pathname === "/jobs") return handleJobsList(req, res, url);
|
|
312
|
+
if (req.method === "GET" && pathname === "/receipts") return handleReceipts(req, res, url);
|
|
313
|
+
if (req.method === "GET" && pathname === "/next") return handleNextForRuntime(req, res, url);
|
|
314
314
|
if (req.method === "POST" && pathname.match(/^\/jobs\/[^/]+\/complete$/)) {
|
|
315
315
|
const jobId = pathname.split("/")[2];
|
|
316
316
|
return await handleJobComplete(req, res, jobId);
|
|
317
317
|
}
|
|
318
318
|
if (req.method === "GET" && pathname === "/") {
|
|
319
|
-
return text(res, 200, `PHEWSH MCP HTTP transport v${VERSION}\nEndpoints: /health /dispatch /status/:id /result/:id /jobs /receipts /next /jobs/:id/complete\n`);
|
|
319
|
+
return text(req, res, 200, `PHEWSH MCP HTTP transport v${VERSION}\nEndpoints: /health /dispatch /status/:id /result/:id /jobs /receipts /next /jobs/:id/complete\n`);
|
|
320
320
|
}
|
|
321
|
-
return json(res, 404, { error: `No route for ${req.method} ${pathname}` });
|
|
321
|
+
return json(req, res, 404, { error: `No route for ${req.method} ${pathname}` });
|
|
322
322
|
} catch (err) {
|
|
323
323
|
console.error("[http]", err);
|
|
324
|
-
return json(res, 500, { error: err.message });
|
|
324
|
+
return json(req, res, 500, { error: err.message });
|
|
325
325
|
}
|
|
326
326
|
});
|
|
327
327
|
|