phewsh 0.15.78 → 0.15.80

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/phewsh.js CHANGED
@@ -103,6 +103,8 @@ const COMMANDS = {
103
103
  setup: () => require('../commands/setup')(),
104
104
  update: () => require('../commands/update')(),
105
105
  serve: () => require('../commands/serve')(),
106
+ project: () => require('../commands/project')(),
107
+ projects: () => require('../commands/project')(),
106
108
  sequence: () => require('../commands/sequence')(),
107
109
  seq: () => require('../commands/sequence')(),
108
110
  ambient: () => require('../commands/ambient')(),
@@ -161,6 +163,7 @@ function showHelp() {
161
163
  console.log(` ${cyan('watch')} ${g('Auto-sync .intent/ → native harness files + cloud')}`);
162
164
  console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
163
165
  console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/ion or /intent')}`);
166
+ console.log(` ${cyan('project')} ${g('Choose which projects this machine\'s worker shows on /ion')}`);
164
167
  console.log(` ${cyan('ion')} ${g('Shared visual room for humans + local agents')}`);
165
168
  console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
166
169
  console.log(` ${cyan('ambient')} ${g('Continuity without launching phewsh — enhance your other tools')}`);
@@ -0,0 +1,121 @@
1
+ // phewsh project — the serve registry: which projects may this machine's
2
+ // worker expose to phewsh.com/ion and /cockpit?
3
+ //
4
+ // Jul 8 2026 Option C ruling: one worker per machine, explicit registry,
5
+ // identity = normalized git remote (never the folder name). Adding a project
6
+ // here does NOT execute anything and does NOT let anyone run work remotely —
7
+ // it only makes the project visible as "online" when `phewsh serve` runs.
8
+ // Manual claim remains the execution boundary.
9
+ //
10
+ // Usage:
11
+ // phewsh project Show the registry + what to do next
12
+ // phewsh project add [path] Register a project (default: this directory)
13
+ // phewsh project list Same as bare `phewsh project`
14
+ // phewsh project remove <name|path> Stop exposing a project
15
+
16
+ const path = require('path');
17
+ const { addServeProject, removeServeProject, serveProjects } = require('../lib/projects-index');
18
+
19
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
20
+ const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
21
+ const w = (s) => `\x1b[97m${s}\x1b[0m`;
22
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
23
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
24
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
25
+
26
+ function showList() {
27
+ const projects = serveProjects();
28
+ console.log('');
29
+ console.log(` ${b(w('Served projects'))} ${g('— what this machine\'s worker shows on phewsh.com/ion')}`);
30
+ console.log('');
31
+ if (projects.length === 0) {
32
+ console.log(` ${g('None yet. Registering a project is one command, run inside its repo:')}`);
33
+ console.log('');
34
+ console.log(` ${cyan('cd <your-project> && phewsh project add')}`);
35
+ console.log('');
36
+ console.log(` ${g('Then start the worker once for the whole machine:')} ${cyan('phewsh serve')}`);
37
+ } else {
38
+ for (const p of projects) {
39
+ console.log(` ${green('●')} ${w(p.name)}`);
40
+ console.log(` ${g('repo:')} ${g(p.remote || 'no remote')}`);
41
+ console.log(` ${g('path:')} ${g(p.path)}`);
42
+ }
43
+ console.log('');
44
+ console.log(` ${g('These appear as this machine\'s projects when')} ${cyan('phewsh serve')} ${g('is running.')}`);
45
+ console.log(` ${g('Add another:')} ${cyan('phewsh project add [path]')} ${g('Stop exposing one:')} ${cyan('phewsh project remove <name>')}`);
46
+ }
47
+ console.log('');
48
+ console.log(` ${g('Registering never executes anything — work still starts only when a')}`);
49
+ console.log(` ${g('human claims it. See phewsh.com/ion for the team room.')}`);
50
+ console.log('');
51
+ }
52
+
53
+ function main() {
54
+ const args = process.argv.slice(3);
55
+ const sub = args[0];
56
+
57
+ if (sub === 'add') {
58
+ const dir = args[1] ? path.resolve(args[1]) : process.cwd();
59
+ try {
60
+ const p = addServeProject(dir);
61
+ console.log('');
62
+ console.log(` ${green('✓')} ${w(p.name)} ${g('registered for this machine\'s worker.')}`);
63
+ console.log(` ${g('identity:')} ${g(p.remote)}`);
64
+ console.log('');
65
+ console.log(` ${g('Next:')} ${cyan('phewsh serve')} ${g('— one worker serves every registered project;')}`);
66
+ console.log(` ${g('phewsh.com/ion will show it as online on this machine.')}`);
67
+ console.log('');
68
+ } catch (err) {
69
+ console.log('');
70
+ console.log(` ${yellow('●')} ${err.message.split('\n').join(`\n ${g('')}`)}`);
71
+ console.log('');
72
+ process.exit(1);
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (sub === 'remove' || sub === 'rm') {
78
+ if (!args[1]) {
79
+ console.log('');
80
+ console.log(` ${g('Which one? ')} ${cyan('phewsh project remove <name|path>')}`);
81
+ console.log('');
82
+ showList();
83
+ process.exit(1);
84
+ }
85
+ const hit = removeServeProject(args[1]);
86
+ console.log('');
87
+ if (hit) {
88
+ console.log(` ${green('✓')} ${w(hit.name)} ${g('is no longer exposed by this machine\'s worker.')}`);
89
+ console.log(` ${g('(The project itself is untouched — this only changes what the worker shows.)')}`);
90
+ } else {
91
+ console.log(` ${yellow('●')} ${g('No served project matches')} ${w(args[1])}${g('. Here\'s what\'s registered:')}`);
92
+ showList();
93
+ process.exit(1);
94
+ }
95
+ console.log('');
96
+ return;
97
+ }
98
+
99
+ if (sub && sub !== 'list' && sub !== 'ls' && sub !== 'help' && sub !== '--help' && sub !== '-h') {
100
+ console.log('');
101
+ console.log(` ${yellow('●')} ${g('Unknown subcommand:')} ${w(sub)}`);
102
+ }
103
+
104
+ if (sub === 'help' || sub === '--help' || sub === '-h') {
105
+ console.log('');
106
+ console.log(` ${b(w('phewsh project'))} ${g('— choose which projects this machine\'s worker exposes')}`);
107
+ console.log('');
108
+ console.log(` ${cyan('phewsh project')} ${g('show the registry + guidance')}`);
109
+ console.log(` ${cyan('phewsh project add [path]')} ${g('register a project (default: here)')}`);
110
+ console.log(` ${cyan('phewsh project remove <name|path>')} ${g('stop exposing a project')}`);
111
+ console.log('');
112
+ console.log(` ${g('Safety: registering only affects visibility. Execution always requires')}`);
113
+ console.log(` ${g('a human claim — nothing runs remotely because it was registered.')}`);
114
+ console.log('');
115
+ return;
116
+ }
117
+
118
+ showList();
119
+ }
120
+
121
+ module.exports = main;
package/commands/serve.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // phewsh serve --port 8080 Start on custom port
13
13
 
14
14
  const http = require('http');
15
- const { execSync, spawn } = require('child_process');
15
+ const { execFileSync, spawn } = require('child_process');
16
16
  const crypto = require('crypto');
17
17
  const os = require('os');
18
18
  const path = require('path');
@@ -35,6 +35,18 @@ function getPort() {
35
35
  return 7483;
36
36
  }
37
37
 
38
+ // The project this worker serves = the directory it was started in. The
39
+ // normalized origin remote is the identity the claim path already verifies
40
+ // against (task.js repo-match); name alone is display, remote is truth.
41
+ function currentProject() {
42
+ let remote = null;
43
+ try {
44
+ remote = execFileSync('git', ['remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] })
45
+ .toString().trim() || null;
46
+ } catch { /* not a git repo, or no origin — worker still serves the directory */ }
47
+ return { name: path.basename(process.cwd()), remote };
48
+ }
49
+
38
50
  // ─── Runtime Detection ─────────────────────────────────────────────────────
39
51
 
40
52
  // Harness runners — shared table in lib/harnesses.js. PHEWSH is not a
@@ -59,6 +71,7 @@ function detectRuntimes() {
59
71
  // ─── Job Queue ─────────────────────────────────────────────────────────────
60
72
 
61
73
  const { gatherReceipts, recordSessionEvent, recordResultFile } = require('../lib/receipts-data');
74
+ const { serveProjects } = require('../lib/projects-index');
62
75
 
63
76
  const jobs = new Map();
64
77
 
@@ -245,10 +258,15 @@ function main() {
245
258
  return;
246
259
  }
247
260
 
248
- // Health check
261
+ // Health check — includes which project this worker is serving, so the
262
+ // web can say "worker online — <project>" instead of an anonymous dot.
263
+ // `projects` = the explicit serve registry (`phewsh project add`); only
264
+ // deliberately registered projects are exposed, never the session index.
249
265
  if (url.pathname === '/health' && req.method === 'GET') {
250
266
  return json(req, res, {
251
267
  status: 'ok',
268
+ project: currentProject(),
269
+ projects: serveProjects().map(p => ({ name: p.name, remote: p.remote })),
252
270
  runtimes: detectRuntimes(),
253
271
  version: require('../package.json').version,
254
272
  uptime: process.uptime(),
@@ -314,6 +332,7 @@ function main() {
314
332
  pending: pendingDecisions().length,
315
333
  bypasses: bypassStats(),
316
334
  recentProjects: listProjects().slice(0, 5).map(p => ({ name: p.name, path: p.path, lastOpened: p.lastOpened })),
335
+ servedProjects: serveProjects().map(p => ({ name: p.name, remote: p.remote })),
317
336
  version: require('../package.json').version,
318
337
  });
319
338
  } catch (err) {
@@ -351,6 +370,26 @@ function main() {
351
370
 
352
371
  const server = http.createServer(handleRequest);
353
372
 
373
+ // One worker per machine (per port) for now. A second `phewsh serve` used to
374
+ // die with a raw EADDRINUSE stack — say what's true and how to proceed instead.
375
+ server.on('error', (err) => {
376
+ if (err.code === 'EADDRINUSE') {
377
+ console.log('');
378
+ console.log(` ${yellow('●')} A phewsh worker is already running on port ${port}.`);
379
+ console.log('');
380
+ console.log(` ${g('One worker per machine for now — the running worker serves the project')}`);
381
+ console.log(` ${g('directory it was started in. To serve a different project:')}`);
382
+ console.log(` ${g('· stop the other worker (Ctrl+C) and start this one, or')}`);
383
+ console.log(` ${g('· run on another port:')} ${w(`phewsh serve --port ${port + 1}`)}`);
384
+ console.log(` ${g('(note: phewsh.com currently discovers port 7483 only)')}`);
385
+ console.log('');
386
+ console.log(` ${g('A one-worker-many-projects registry is the planned next step.')}`);
387
+ console.log('');
388
+ process.exit(1);
389
+ }
390
+ throw err;
391
+ });
392
+
354
393
  server.listen(port, '127.0.0.1', () => {
355
394
  const mirror = http.createServer(handleRequest);
356
395
  mirror.on('error', () => { /* IPv6 unavailable or already bound */ });
@@ -374,6 +413,20 @@ function main() {
374
413
  console.log(` ${g('https://docs.anthropic.com/en/docs/claude-code')}`);
375
414
  }
376
415
  console.log('');
416
+ const registered = serveProjects();
417
+ const here = currentProject();
418
+ console.log(` ${b('Projects this worker shows on phewsh.com/ion:')}`);
419
+ if (registered.length === 0) {
420
+ console.log(` ${g('only the current directory:')} ${w(here.name)}${here.remote ? g(` (${here.remote})`) : ''}`);
421
+ console.log(` ${g('Register projects by name so the web knows them:')} ${cyan('phewsh project add')} ${g('(run inside each repo)')}`);
422
+ } else {
423
+ for (const p of registered) {
424
+ const isHere = p.path === process.cwd();
425
+ console.log(` ${green('●')} ${w(p.name)}${g(` (${p.remote})`)}${isHere ? g(' ← current directory') : ''}`);
426
+ }
427
+ console.log(` ${g('Manage the list:')} ${cyan('phewsh project')}`);
428
+ }
429
+ console.log('');
377
430
  console.log(` ${g('Open phewsh.com/ion to see the worker online, or phewsh.com/intent → Work')}`);
378
431
  console.log(` ${g('Press Ctrl+C to stop')}`);
379
432
  console.log('');
@@ -13,7 +13,8 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
 
16
- const INDEX_FILE = path.join(os.homedir(), '.phewsh', 'project-index.json');
16
+ // PHEWSH_PROJECT_INDEX override exists for tests only — never point real use away from home.
17
+ const INDEX_FILE = process.env.PHEWSH_PROJECT_INDEX || path.join(os.homedir(), '.phewsh', 'project-index.json');
17
18
 
18
19
  // Shallow-scanned roots when the user asks to find projects. One level deep,
19
20
  // opt-in only — deep-scanning someone's machine uninvited is invasive.
@@ -114,6 +115,75 @@ function scanForCandidates(roots = SCAN_ROOTS) {
114
115
  return found;
115
116
  }
116
117
 
118
+ // ─── Serve registry (Jul 8 2026 Option C ruling) ────────────────────────────
119
+ // Auto-recorded session entries are NOT auto-exposed to the worker. Only
120
+ // projects the human deliberately registers with `phewsh project add` carry
121
+ // serve:true, and only those appear on the bridge/web. Identity is the
122
+ // normalized git remote — the same convention the task-claim path enforces —
123
+ // never the folder name.
124
+
125
+ const { execFileSync } = require('child_process');
126
+ const { normalizeRemote } = require('./team-tasks');
127
+
128
+ function originRemote(dir) {
129
+ try {
130
+ return execFileSync('git', ['remote', 'get-url', 'origin'], {
131
+ cwd: dir, stdio: ['ignore', 'pipe', 'ignore'],
132
+ }).toString().trim() || null;
133
+ } catch { return null; }
134
+ }
135
+
136
+ /**
137
+ * Register a project so the local worker may expose it. Validates before
138
+ * writing; throws with a plain-language message when something is missing.
139
+ */
140
+ function addServeProject(dir) {
141
+ if (!fs.existsSync(path.resolve(dir))) throw new Error(`That folder doesn't exist: ${path.resolve(dir)}`);
142
+ const key = fs.realpathSync(path.resolve(dir)); // one entry per real directory, symlinks collapse
143
+
144
+ if (!fs.existsSync(path.join(key, '.git'))) {
145
+ throw new Error(`Not a git repository: ${key}\nA served project needs a repo so work stays traceable. Run this inside a git repo (or git init first).`);
146
+ }
147
+ const remote = originRemote(key);
148
+ if (!remote) {
149
+ throw new Error(`This repo has no 'origin' remote yet.\nThe remote is the project's identity — it's how phewsh guarantees work lands in the right repo.\nAdd one first: git remote add origin <url>`);
150
+ }
151
+ const index = load();
152
+ index.projects[key] = {
153
+ ...(index.projects[key] || {}),
154
+ name: path.basename(key),
155
+ path: key,
156
+ remote: normalizeRemote(remote),
157
+ serve: true,
158
+ serveAddedAt: new Date().toISOString(),
159
+ lastOpened: index.projects[key]?.lastOpened || new Date().toISOString(),
160
+ };
161
+ save(index);
162
+ return index.projects[key];
163
+ }
164
+
165
+ /** Stop exposing a project (by name or path). The session-index entry stays. */
166
+ function removeServeProject(nameOrPath) {
167
+ const index = load();
168
+ let wantPath = path.resolve(nameOrPath);
169
+ try { wantPath = fs.realpathSync(wantPath); } catch { /* not a live path — name match still works */ }
170
+ const hit = Object.values(index.projects).find(p =>
171
+ p.serve && (p.path === wantPath || p.name === nameOrPath));
172
+ if (!hit) return null;
173
+ delete index.projects[hit.path].serve;
174
+ delete index.projects[hit.path].serveAddedAt;
175
+ save(index);
176
+ return hit;
177
+ }
178
+
179
+ /** Projects the worker is allowed to expose, pruned to paths that still exist. */
180
+ function serveProjects() {
181
+ const index = load();
182
+ return Object.values(index.projects)
183
+ .filter(p => p.serve === true && fs.existsSync(p.path))
184
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
185
+ }
186
+
117
187
  function fmtAgo(ts) {
118
188
  if (!ts) return '';
119
189
  const mins = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
@@ -123,4 +193,4 @@ function fmtAgo(ts) {
123
193
  return `${Math.floor(hrs / 24)}d ago`;
124
194
  }
125
195
 
126
- module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, scanForCandidates, fmtAgo };
196
+ module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, scanForCandidates, fmtAgo, addServeProject, removeServeProject, serveProjects };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.78",
3
+ "version": "0.15.80",
4
4
  "description": "One mission. Many AI tools. No lost context. PHEWSH keeps your project's intent, decisions, and working context aligned across Claude Code, Codex, Cursor, Gemini and your team — so the next AI knows what the last one learned.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"