pi-orchestrator-config 4.2.0 → 4.3.2

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.
Files changed (100) hide show
  1. package/README.md +76 -67
  2. package/agents/code-reviewer-docs.md +11 -1
  3. package/agents/code-reviewer-guidelines.md +11 -1
  4. package/agents/code-reviewer-quality.md +11 -1
  5. package/agents/code-reviewer-security.md +11 -1
  6. package/agents/code-reviewer-spec.md +11 -1
  7. package/agents/git-expert.md +42 -3
  8. package/agents/github-expert.md +11 -0
  9. package/extensions/acpx-provider/runtime-models.ts +18 -6
  10. package/extensions/cli-provider/agents/claude.ts +3 -2
  11. package/extensions/cli-provider/agents/cursor.ts +3 -5
  12. package/extensions/cli-provider/agents/gemini.ts +3 -2
  13. package/extensions/cli-provider/discover.ts +7 -7
  14. package/extensions/cli-provider/index.ts +5 -0
  15. package/extensions/cli-provider/runtime-models.ts +18 -6
  16. package/extensions/cli-provider/session-reaper.ts +5 -7
  17. package/extensions/cli-provider/sessions.ts +4 -0
  18. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  19. package/extensions/coms/coms-p2p.ts +1719 -423
  20. package/extensions/coms/coms-shared.ts +261 -39
  21. package/extensions/coms/coms-wrapper.ts +100 -62
  22. package/extensions/coms/index.ts +8 -4
  23. package/extensions/coms/mixed-turn.ts +26 -0
  24. package/extensions/coms/probe-socket.ts +42 -0
  25. package/extensions/coms/themeMap.ts +0 -1
  26. package/extensions/image-gen/image-gen.ts +19 -1
  27. package/extensions/orchestrator/async-agents.ts +563 -102
  28. package/extensions/orchestrator/async-capability.ts +8 -8
  29. package/extensions/orchestrator/cron.ts +15 -9
  30. package/extensions/orchestrator/dreaming.ts +32 -25
  31. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  32. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  33. package/extensions/orchestrator/enforcement.ts +80 -27
  34. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  35. package/extensions/orchestrator/git-helpers.ts +2 -2
  36. package/extensions/orchestrator/index.ts +78 -10
  37. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  38. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  39. package/extensions/orchestrator/memory-provenance.ts +29 -25
  40. package/extensions/orchestrator/memory-scoring.ts +23 -18
  41. package/extensions/orchestrator/memory-tools.ts +10 -3
  42. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  43. package/extensions/orchestrator/parse-model-override.ts +29 -0
  44. package/extensions/orchestrator/pi-config-review-state.ts +57 -33
  45. package/extensions/orchestrator/project-settings.ts +333 -127
  46. package/extensions/orchestrator/resolve-agent-model.ts +24 -13
  47. package/extensions/orchestrator/review-porcelain.ts +4 -0
  48. package/extensions/orchestrator/review-ui.ts +63 -15
  49. package/extensions/orchestrator/rule-placeholders.ts +411 -2
  50. package/extensions/orchestrator/rules.ts +80 -132
  51. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  52. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  53. package/extensions/orchestrator/settings-tui.ts +636 -0
  54. package/extensions/orchestrator/state-jsonl.ts +431 -0
  55. package/extensions/orchestrator/subagent-tool.ts +157 -59
  56. package/extensions/orchestrator/sync-limit.ts +23 -0
  57. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  58. package/extensions/orchestrator/utils.ts +8 -5
  59. package/extensions/pidash/pidash.ts +193 -156
  60. package/extensions/pidiff/pidiff.ts +96 -38
  61. package/extensions/pidiff/spawn-lock.ts +62 -0
  62. package/extensions/pitasks/index.ts +430 -0
  63. package/extensions/pitasks/reminders.ts +37 -0
  64. package/extensions/pitasks/task-store.ts +467 -0
  65. package/extensions/pitasks/task-tools.ts +375 -0
  66. package/extensions/pitasks/task-widget.ts +181 -0
  67. package/extensions/pitasks/types.ts +38 -0
  68. package/extensions/providers/acpx-driver.ts +12 -2
  69. package/extensions/providers/built-in-drivers.ts +6 -6
  70. package/extensions/providers/claude-driver.ts +1 -1
  71. package/extensions/providers/cursor-cli-driver.ts +1 -1
  72. package/extensions/providers/gemini-driver.ts +1 -1
  73. package/extensions/providers/index.ts +144 -26
  74. package/extensions/providers/initialized-guard.ts +27 -0
  75. package/extensions/providers/restore-default-model.ts +596 -0
  76. package/extensions/providers/session-shutdown.ts +39 -0
  77. package/extensions/shared/coms-active.ts +23 -0
  78. package/extensions/shared/create-runtime-provider.ts +2 -0
  79. package/extensions/shared/daemon-manager.ts +0 -8
  80. package/extensions/shared/file-logger.ts +132 -25
  81. package/extensions/shared/logger.ts +50 -0
  82. package/extensions/shared/models-dev.ts +421 -0
  83. package/extensions/shared/oneshot.ts +116 -0
  84. package/extensions/shared/resolve-binary.ts +191 -0
  85. package/extensions/shared/types.ts +3 -0
  86. package/package.json +9 -3
  87. package/prompts/coderabbit-rate-limit.md +2 -0
  88. package/rules/00-orchestrator-core.md +3 -0
  89. package/rules/10-agent-routing.md +12 -1
  90. package/rules/20-code-review-loop.md +52 -25
  91. package/rules/30-prompt-templates.md +4 -0
  92. package/rules/35-memory.md +3 -3
  93. package/rules/40-critical-rules.md +3 -0
  94. package/rules/55-coms-protocol.md +84 -18
  95. package/rules/60-task-tracking.md +2 -0
  96. package/settings-keys.json +563 -0
  97. package/templates/coms-feature-manager-prompt.md +4 -6
  98. package/extensions/coms/coms-net-server.ts +0 -1642
  99. package/extensions/coms/coms-net-wrapper.ts +0 -521
  100. package/extensions/coms/coms-net.ts +0 -1589
@@ -1,521 +0,0 @@
1
- /**
2
- * coms-net-wrapper.ts — On-demand networked agent communication wrapper
3
- *
4
- * Wraps the networked coms extension (coms-net.ts) to support
5
- * activation via /coms-net command instead of auto-start on session_start.
6
- *
7
- * Auto-manages the coms-net hub server lifecycle:
8
- * - Checks if a server is already running (via server.json health check)
9
- * - Spawns one with Bun if not (port 0 = OS picks free port)
10
- * - Server writes server.json + server.secret.json for client auto-discovery
11
- * - Server is cleaned up on session shutdown
12
- */
13
-
14
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
- import type { AutocompleteItem } from "@earendil-works/pi-tui";
16
- import { fuzzyFilter } from "@earendil-works/pi-tui";
17
-
18
- function fuzzy(items: AutocompleteItem[], query: string): AutocompleteItem[] | null {
19
- if (!query.trim()) return items.length > 0 ? items : null;
20
- const result = fuzzyFilter(items, query, i => `${i.label} ${i.description || ""}`);
21
- return result.length > 0 ? result : null;
22
- }
23
- import * as fs from "node:fs";
24
- import * as path from "node:path";
25
- import * as os from "node:os";
26
- import { execSync, spawn } from "node:child_process";
27
- import { fileURLToPath } from "node:url";
28
- import { parseFlags, tokenizeArgs, createDeferredProxy, persistState, pruneStaleRegistry, type DeferredUpstream } from "./coms-shared.js";
29
- import upstreamComsNetInit from "./coms-net.js";
30
-
31
- const COMS_NET_DIR = path.join(os.homedir(), ".pi", "coms-net");
32
- const SERVER_STARTUP_TIMEOUT_MS = 10_000;
33
- const SERVER_POLL_INTERVAL_MS = 300;
34
-
35
- function serverJsonPath(project: string): string {
36
- return path.join(COMS_NET_DIR, "projects", project, "server.json");
37
- }
38
-
39
- function readServerJson(project: string): { local_url?: string; public_url?: string; host?: string; port?: number; pid?: number } | null {
40
- const p = serverJsonPath(project);
41
- try {
42
- if (!fs.existsSync(p)) return null;
43
- return JSON.parse(fs.readFileSync(p, "utf-8"));
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- async function isServerHealthy(project: string): Promise<boolean> {
50
- const sj = readServerJson(project);
51
- if (!sj?.local_url) return false;
52
- try {
53
- const url = new URL(sj.local_url);
54
- if (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") return false;
55
- } catch { return false; }
56
- try {
57
- const resp = await fetch(`${sj.local_url}/health`, { signal: AbortSignal.timeout(2000) });
58
- if (!resp.ok) return false;
59
- const body = await resp.json();
60
- return body?.ok === true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- function findBun(): string | null {
67
- try {
68
- const p = execSync("which bun", { encoding: "utf-8" }).trim();
69
- return p || null;
70
- } catch {
71
- return null;
72
- }
73
- }
74
-
75
- function getServerScriptPath(): string {
76
- return path.resolve(
77
- path.dirname(fileURLToPath(import.meta.url)),
78
- "coms-net-server.ts",
79
- );
80
- }
81
-
82
- async function ensureServerRunning(
83
- project: string,
84
- log: (msg: string) => void,
85
- options?: { port?: string; host?: string },
86
- ): Promise<boolean> {
87
- // If port/host explicitly requested, check if running server matches
88
- if (await isServerHealthy(project)) {
89
- const sj = readServerJson(project);
90
- const wantPort = options?.port || process.env.PI_COMS_NET_PORT;
91
- const wantHost = options?.host || process.env.PI_COMS_NET_HOST;
92
- const needRestart = (wantPort && sj?.port !== Number(wantPort)) ||
93
- (wantHost && sj?.host !== wantHost);
94
- if (needRestart) {
95
- log(`server running but port/host mismatch — restarting`);
96
- await killServer(project, log);
97
- await new Promise(r => setTimeout(r, 1000));
98
- } else {
99
- log("server already running");
100
- return true;
101
- }
102
- }
103
-
104
- const bunPath = findBun();
105
- if (!bunPath) {
106
- log("bun not found — cannot start coms-net server");
107
- return false;
108
- }
109
-
110
- const scriptPath = getServerScriptPath();
111
- if (!fs.existsSync(scriptPath)) {
112
- log(`server script not found: ${scriptPath}`);
113
- return false;
114
- }
115
-
116
- const projDir = path.join(COMS_NET_DIR, "projects", project);
117
- fs.mkdirSync(projDir, { recursive: true });
118
-
119
- // Binds to 127.0.0.1 by default (safe). For LAN access, user sets
120
- // PI_COMS_NET_AUTH_TOKEN and PI_COMS_NET_HOST=0.0.0.0 in their env.
121
- const logFile = path.join(projDir, "server.log");
122
- log(`spawning coms-net server: ${bunPath} ${scriptPath}`);
123
-
124
- const outFd = fs.openSync(logFile, "a");
125
- const errFd = fs.openSync(logFile, "a");
126
- const child = spawn(bunPath, [scriptPath], {
127
- detached: true,
128
- stdio: ["ignore", outFd, errFd],
129
- env: {
130
- ...process.env,
131
- PI_COMS_NET_PROJECT: project,
132
- PI_COMS_NET_PORT: options?.port || process.env.PI_COMS_NET_PORT || "0",
133
- ...(options?.host ? { PI_COMS_NET_HOST: options.host } : {}),
134
- },
135
- });
136
- child.unref();
137
- try { fs.closeSync(outFd); } catch {}
138
- try { fs.closeSync(errFd); } catch {}
139
-
140
- const deadline = Date.now() + SERVER_STARTUP_TIMEOUT_MS;
141
- while (Date.now() < deadline) {
142
- await new Promise(r => setTimeout(r, SERVER_POLL_INTERVAL_MS));
143
- if (await isServerHealthy(project)) {
144
- log("server started successfully");
145
- return true;
146
- }
147
- }
148
-
149
- log("server failed to start within timeout");
150
- return false;
151
- }
152
-
153
- async function killServer(project: string, log: (msg: string) => void): Promise<void> {
154
- const sj = readServerJson(project);
155
- if (!sj?.pid || !Number.isInteger(sj.pid) || sj.pid <= 0) return;
156
- const pid = sj.pid;
157
- // Verify PID is our server by checking health endpoint before killing
158
- if (sj.local_url) {
159
- try {
160
- const resp = await fetch(`${sj.local_url}/health`, { signal: AbortSignal.timeout(2000) });
161
- if (!resp.ok) {
162
- log(`health check failed for pid ${pid} — may not be our server, skipping kill`);
163
- return;
164
- }
165
- } catch {
166
- // Health endpoint unreachable — server likely dead, clean up server.json
167
- log(`pid ${pid} unreachable, cleaning up server.json`);
168
- try { fs.unlinkSync(serverJsonPath(project)); } catch { /* ignore */ }
169
- return;
170
- }
171
- }
172
- try {
173
- process.kill(pid, "SIGTERM");
174
- log(`sent SIGTERM to server pid ${pid}`);
175
- } catch {
176
- log(`server pid ${pid} already dead`);
177
- return;
178
- }
179
- // Wait up to 3s for clean exit, then SIGKILL
180
- let waited = 0;
181
- const check = () => {
182
- try { process.kill(pid, 0); } catch { return; } // dead
183
- waited += 100;
184
- if (waited >= 3000) {
185
- try { process.kill(pid, "SIGKILL"); log(`sent SIGKILL to server pid ${pid}`); } catch (e: any) { log(`SIGKILL to pid ${pid} failed: ${e?.message}`); }
186
- return;
187
- }
188
- setTimeout(check, 100).unref();
189
- };
190
- setTimeout(check, 100).unref();
191
- }
192
-
193
- function isValidProject(project: string): boolean {
194
- return !/[\/\\]|\.\./.test(project);
195
- }
196
-
197
- export function registerComsNet(pi: ExtensionAPI) {
198
- const state: DeferredUpstream = {
199
- capturedSessionStart: null,
200
- capturedSessionShutdown: null,
201
- flagValues: new Map(),
202
- active: false,
203
- };
204
- let activeProject = "";
205
-
206
- function getProject(ctx?: any): string {
207
- // When ctx provides cwd, derive from it to avoid stale cache after resume/dir-change
208
- if (ctx?.cwd) {
209
- const proj = ctx.cwd.replace(/^[\\/]/, "").replace(/[\\/]/g, "__");
210
- if (proj) return proj;
211
- }
212
- if (activeProject) return activeProject;
213
- const fromFlags = state.flagValues.get("project") as string;
214
- if (fromFlags) return fromFlags;
215
- const cwd = process.cwd() || "";
216
- const proj = cwd.replace(/^[\\/]/, "").replace(/[\\/]/g, "__");
217
- if (!proj) throw new Error("coms-net: cannot determine project — no cwd available");
218
- return proj;
219
- }
220
- let serverStartedByUs = false;
221
-
222
- function persist() {
223
- state.extra = { serverStartedByUs, activeProject: getProject() };
224
- // Don't persist auth token to disk — security sensitive
225
- const authToken = state.flagValues.get('auth-token');
226
- if (authToken !== undefined) state.flagValues.delete('auth-token');
227
- try {
228
- persistState(pi, PERSIST_KEY, state);
229
- } finally {
230
- if (authToken !== undefined) state.flagValues.set('auth-token', authToken);
231
- }
232
- }
233
-
234
- const log = (msg: string) => {
235
- try {
236
- pi.appendEntry("coms-net-log", { event: "wrapper", ts: new Date().toISOString(), msg });
237
- } catch (e: any) { console.debug("[coms-net] log append failed:", e?.message || e); }
238
- };
239
-
240
- const PERSIST_KEY = "coms-net-state";
241
-
242
- const proxyPi = createDeferredProxy(
243
- pi, state, "⚠️ coms-net not active. Run `/coms-net start` first.", PERSIST_KEY,
244
- );
245
-
246
- upstreamComsNetInit(proxyPi as any);
247
-
248
- // Prune stale registry entries on session start (cleans up after crashes)
249
- // Also reset coms-net state on fresh starts (non-reload) to clear phantom peers
250
- pi.on("session_start", (evt: any) => {
251
- try { pruneStaleRegistry(); } catch (e: any) { console.debug("[coms-net] stale cleanup:", e?.message?.slice(0, 100)); }
252
- if (evt?.reason !== "reload") {
253
- state.active = false;
254
- state.flagValues.delete("auth-token"); // Don't persist auth tokens
255
- state.extra = {}; // Clear server flags (serverStartedByUs, activeProject, etc.)
256
- persistState(pi, PERSIST_KEY, state);
257
- }
258
- });
259
-
260
- // Restore serverStartedByUs after reload — MUST be registered after
261
- // createDeferredProxy/upstreamComsNetInit so the proxy hydrates state.extra first
262
- pi.on("session_start", (event: any) => {
263
- if (event?.reason !== "reload") return;
264
- if (state.extra?.serverStartedByUs) {
265
- serverStartedByUs = true;
266
- }
267
- if (state.extra?.activeProject) {
268
- activeProject = state.extra.activeProject;
269
- }
270
- });
271
-
272
- // Don't auto-kill the server on session shutdown — other sessions may
273
- // be connected. The server has its own stale detection and cleanup.
274
-
275
-
276
- pi.registerCommand("coms-net", {
277
- description: "Networked agent communication: /coms-net start | connect | disconnect | stop | status",
278
- getArgumentCompletions: (prefix: string) => {
279
- const tokens = prefix.trim().split(/\s+/).filter(Boolean);
280
- const atNextToken = prefix.endsWith(" ") || tokens.length === 0;
281
- const lastPart = atNextToken ? "" : tokens[tokens.length - 1];
282
- const completed = atNextToken ? tokens : tokens.slice(0, -1);
283
- const base = atNextToken ? prefix : prefix.slice(0, prefix.length - lastPart.length);
284
- const mk = (items: {v: string; l: string; d: string}[]) =>
285
- fuzzy(items.map(i => ({ value: base + i.v, label: i.l, description: i.d })), lastPart);
286
-
287
- if (completed.length === 0) {
288
- return mk([
289
- { v: "start", l: "start", d: "Start local server and connect" },
290
- { v: "connect", l: "connect", d: "Connect to a running server" },
291
- { v: "disconnect", l: "disconnect", d: "Disconnect from server (keep server running)" },
292
- { v: "stop", l: "stop", d: "Stop coms-net + kill server" },
293
- { v: "status", l: "status", d: "Show coms-net + server status" },
294
-
295
- ]);
296
- }
297
- if (completed[0] === "start" && (lastPart.startsWith("-") || lastPart === "")) {
298
- const used = new Set(completed.filter(p => p.startsWith("--")));
299
- return mk([
300
- { v: "--cname ", l: "--cname", d: "Agent name" },
301
- { v: "--purpose ", l: "--purpose", d: "Agent purpose" },
302
- { v: "--project ", l: "--project", d: "Project namespace" },
303
- { v: "--color ", l: "--color", d: "Hex color #RRGGBB" },
304
- { v: "--explicit", l: "--explicit", d: "Hide from auto-discovery" },
305
- { v: "--port ", l: "--port", d: "Server port" },
306
- { v: "--host ", l: "--host", d: "Server bind address (e.g. 0.0.0.0)" },
307
- ].filter(f => !used.has(f.v.trim())));
308
- }
309
- if (completed[0] === "connect" && (lastPart.startsWith("-") || lastPart === "")) {
310
- const used = new Set(completed.filter(p => p.startsWith("--")));
311
- return mk([
312
- { v: "--cname ", l: "--cname", d: "Agent name" },
313
- { v: "--purpose ", l: "--purpose", d: "Agent purpose" },
314
- { v: "--project ", l: "--project", d: "Project namespace" },
315
- { v: "--color ", l: "--color", d: "Hex color #RRGGBB" },
316
- { v: "--explicit", l: "--explicit", d: "Hide from auto-discovery" },
317
- { v: "--url ", l: "--url", d: "Hub server URL" },
318
- { v: "--auth-token ", l: "--auth-token", d: "Bearer token for the hub" },
319
- ].filter(f => !used.has(f.v.trim())));
320
- }
321
- return null;
322
- },
323
- handler: async (args: string, ctx: any) => {
324
- const trimmed = (args || "").trim();
325
- const parts = tokenizeArgs(trimmed);
326
- const subcommand = parts[0] || "status";
327
-
328
- if (subcommand === "start") {
329
- if (state.active) {
330
- // Check if server is actually still running
331
- if (await isServerHealthy(getProject(ctx))) {
332
- try { ctx.ui.notify("📡 coms-net already active", "warning"); } catch {}
333
- return;
334
- }
335
- // Server died — reset state and allow restart
336
- state.active = false;
337
- serverStartedByUs = false;
338
- }
339
- state.flagValues = new Map();
340
- parseFlags(parts.slice(1), state.flagValues);
341
-
342
- if (state.flagValues.has("name")) {
343
- try { ctx.ui.notify("📡 coms-net: use --cname instead of --name", "error"); } catch {}
344
- return;
345
- }
346
- // Default project to cwd so sessions in different dirs are isolated
347
- if (!state.flagValues.has("project")) {
348
- const cwd = ctx.cwd || "";
349
- const proj = cwd.replace(/^[\\/]/,"").replace(/[\\/]/g, "__");
350
- if (!proj) {
351
- try { ctx.ui.notify("📡 coms-net: cannot start from /. Run from a project directory.", "error"); } catch {}
352
- return;
353
- }
354
- state.flagValues.set("project", proj);
355
- }
356
- const project = state.flagValues.get("project") as string;
357
- if (!isValidProject(project)) {
358
- try { ctx.ui.notify("📡 coms-net: invalid project name", "error"); } catch {}
359
- return;
360
- }
361
- activeProject = project;
362
-
363
- if (!state.capturedSessionStart) {
364
- try { ctx.ui.notify("📡 coms-net: internal error — no session handler captured", "error"); } catch {}
365
- return;
366
- }
367
-
368
- const port = state.flagValues.get("port") as string | undefined;
369
- const host = state.flagValues.get("host") as string | undefined;
370
-
371
- // Auto-start server, or restart if port/host mismatch
372
- const alreadyRunning = await isServerHealthy(project);
373
- if (alreadyRunning) {
374
- // Check if running server matches requested port/host
375
- const sj = readServerJson(project);
376
- const wantPort = port || process.env.PI_COMS_NET_PORT;
377
- const wantHost = host || process.env.PI_COMS_NET_HOST;
378
- const mismatch = (wantPort && sj?.port !== Number(wantPort)) ||
379
- (wantHost && sj?.host !== wantHost);
380
- if (mismatch) {
381
- log(`server port/host mismatch — restarting (want ${wantHost || "*"}:${wantPort || "*"}, have ${sj?.host}:${sj?.port})`);
382
- await killServer(project, log);
383
- await new Promise(r => setTimeout(r, 1000));
384
- try { ctx.ui.notify("📡 Restarting coms-net server (port/host changed)...", "info"); } catch {}
385
- const started = await ensureServerRunning(project, log, { port, host });
386
- if (!started) {
387
- try { ctx.ui.notify("📡 coms-net: failed to restart server.", "error"); } catch {}
388
- return;
389
- }
390
- serverStartedByUs = true;
391
- }
392
- } else {
393
- try { ctx.ui.notify("📡 Starting coms-net server...", "info"); } catch {}
394
- const started = await ensureServerRunning(project, log, { port, host });
395
- if (!started) {
396
- try { ctx.ui.notify("📡 coms-net: failed to start server. Is Bun installed?", "error"); } catch {}
397
- return;
398
- }
399
- serverStartedByUs = true;
400
- }
401
-
402
- try {
403
- await state.capturedSessionStart({}, ctx);
404
- state.active = true;
405
- serverStartedByUs = true;
406
- persist();
407
- const sj = readServerJson(project);
408
- const serverAddr = (sj?.host && sj?.port ? `http://${sj.host}:${sj.port}` : sj?.public_url || sj?.local_url) || "unknown";
409
- try { ctx.ui.notify(`📡 coms-net active — server at ${serverAddr}`, "info"); } catch {}
410
- } catch (err: any) {
411
- try { ctx.ui.notify(`📡 coms-net start failed: ${err?.message ?? String(err)}`, "error"); } catch {}
412
- }
413
- } else if (subcommand === "connect") {
414
- if (state.active) {
415
- try { ctx.ui.notify("📡 coms-net already active", "warning"); } catch {}
416
- return;
417
- }
418
- state.flagValues = new Map();
419
- parseFlags(parts.slice(1), state.flagValues);
420
-
421
- if (state.flagValues.has("name")) {
422
- try { ctx.ui.notify("📡 coms-net: use --cname instead of --name", "error"); } catch {}
423
- return;
424
- }
425
- // Default project to cwd so sessions in different dirs are isolated
426
- if (!state.flagValues.has("project")) {
427
- const cwd = ctx.cwd || "";
428
- const proj = cwd.replace(/^[\\/]/,"").replace(/[\\/]/g, "__");
429
- if (!proj) {
430
- try { ctx.ui.notify("📡 coms-net: cannot connect from /. Run from a project directory.", "error"); } catch {}
431
- return;
432
- }
433
- state.flagValues.set("project", proj);
434
- }
435
- const project = state.flagValues.get("project") as string;
436
- if (!isValidProject(project)) {
437
- try { ctx.ui.notify("📡 coms-net: invalid project name", "error"); } catch {}
438
- return;
439
- }
440
- activeProject = project;
441
-
442
- if (!state.capturedSessionStart) {
443
- try { ctx.ui.notify("📡 coms-net: internal error — no session handler captured", "error"); } catch {}
444
- return;
445
- }
446
-
447
- // Map --url to server-url for upstream compatibility
448
- if (state.flagValues.has("url")) {
449
- state.flagValues.set("server-url", state.flagValues.get("url"));
450
- }
451
- const serverUrl = state.flagValues.get("server-url") as string | undefined;
452
- try {
453
- await state.capturedSessionStart({}, ctx);
454
- state.active = true;
455
- persist();
456
- serverStartedByUs = false;
457
- const displayUrl = serverUrl || "local server";
458
- try { ctx.ui.notify(`📡 coms-net active — connected to ${displayUrl}`, "info"); } catch {}
459
- } catch (err: any) {
460
- try { ctx.ui.notify(`📡 coms-net connect failed: ${err?.message ?? String(err)}`, "error"); } catch {}
461
- }
462
- } else if (subcommand === "disconnect") {
463
- if (!state.active) {
464
- try { ctx.ui.notify("📡 coms-net not active", "info"); } catch {}
465
- return;
466
- }
467
- if (serverStartedByUs) {
468
- try { ctx.ui.notify("📡 coms-net: you started the server — use /coms-net stop instead", "warning"); } catch {}
469
- return;
470
- }
471
- if (state.capturedSessionShutdown) {
472
- try { await state.capturedSessionShutdown(); } catch {}
473
- }
474
- state.active = false;
475
- persist();
476
- try { ctx.ui.notify("📡 coms-net disconnected", "info"); } catch {}
477
- } else if (subcommand === "stop") {
478
- if (!state.active) {
479
- try { ctx.ui.notify("📡 coms-net not active", "info"); } catch {}
480
- return;
481
- }
482
- if (!serverStartedByUs) {
483
- try { ctx.ui.notify("📡 coms-net: you didn't start the server — use /coms-net disconnect instead", "warning"); } catch {}
484
- return;
485
- }
486
- if (state.capturedSessionShutdown) {
487
- try { await state.capturedSessionShutdown(); } catch {}
488
- }
489
- state.active = false;
490
- persist();
491
- // User explicitly stopped — kill server unconditionally
492
- await killServer(getProject(), log);
493
- serverStartedByUs = false;
494
- try { ctx.ui.notify("📡 coms-net stopped", "info"); } catch {}
495
- } else if (subcommand === "status") {
496
- const project = getProject(ctx);
497
- if (!isValidProject(project)) {
498
- try { ctx.ui.notify("📡 coms-net: invalid project name", "error"); } catch {}
499
- return;
500
- }
501
- const serverUp = await isServerHealthy(project);
502
- const sj = readServerJson(project);
503
- let msg = `📡 coms-net: ${state.active ? "active" : "inactive"}\n`;
504
- msg += `Server: ${serverUp ? `running at ${sj?.host && sj?.port ? `http://${sj.host}:${sj.port}` : sj?.public_url || sj?.local_url}` : "not running"}\n`;
505
- if (serverStartedByUs) msg += "(server started by this session)";
506
- try { ctx.ui.notify(msg, "info"); } catch {}
507
- } else {
508
- try { ctx.ui.notify(`📡 coms-net: unknown subcommand "${subcommand}". Use: start | connect | disconnect | stop | status`, "warning"); } catch {}
509
- }
510
- },
511
- });
512
-
513
- // Kill server on session shutdown if we started it and no other peers are connected
514
- pi.on("session_shutdown", async (event: any) => {
515
- if (!serverStartedByUs) return;
516
- // Don't kill server on reload — it will be reused after reload
517
- if (event?.reason === "reload") return;
518
- await killServer(getProject(), log);
519
- log("server killed on shutdown (we started it)");
520
- });
521
- }