@webinson/peer-core 0.1.977 → 0.1.978

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/README.md CHANGED
@@ -16,4 +16,4 @@ peer core serve --detach # or let the editor start it on demand
16
16
  `peer core status` says who runs Peer here; `peer core stop` stops Peer Core. The Peer desktop app,
17
17
  when it starts, takes coordination over. Any other `peer <command>` goes to the running Peer.
18
18
 
19
- Version 0.1.977 of Peer Local (https://github.com/SemanS/peer-app, branch `peer`), based on T3 Code.
19
+ Version 0.1.978 of Peer Local (https://github.com/SemanS/peer-app, branch `peer`), based on T3 Code.
package/dist/bin.mjs CHANGED
@@ -77,7 +77,7 @@ if (isEntrypoint({
77
77
  } else {
78
78
  const { attachConsoleStdin } = await import("./consoleStdin-DO-vIhyg.mjs");
79
79
  attachConsoleStdin();
80
- const { runCli } = await import("./binCli-BD8xnLcs.mjs");
80
+ const { runCli } = await import("./binCli-B32nSvwp.mjs");
81
81
  runCli(npmPeerBin ? "peer" : "t3");
82
82
  }
83
83
  }
@@ -27680,6 +27680,22 @@ const connectEditor = (id, stateDir, io) => gen$1(function* () {
27680
27680
  stateDir
27681
27681
  }, io);
27682
27682
  });
27683
+ /** The way out of `connectEditor`: Peer's hooks out of Claude Code or Codex, the MCP entry out of the rest. */
27684
+ const disconnectEditor = (id, io) => gen$1(function* () {
27685
+ if (id === "claude" || id === "codex") {
27686
+ const status = yield* (yield* PeerAdapters).setCoordination(id === "claude" ? { claudeHooks: false } : { codexHooks: false });
27687
+ return yield* sayHooks(status, {
27688
+ claude: id === "claude",
27689
+ codex: id === "codex",
27690
+ install: false
27691
+ }, io);
27692
+ }
27693
+ const answer = yield* (yield* McpClients).disconnect({
27694
+ client: id,
27695
+ scope: "user"
27696
+ });
27697
+ yield* io.say(answer.outcome === "manual" ? `${answer.path} is not plain JSON, so Peer left it alone. Remove this entry by hand:\n${answer.entry}` : answer.outcome === "unchanged" ? `${editorTitle(id)} had no Peer entry in ${answer.path}.` : `Disconnected ${editorTitle(id)}: removed Peer's entry from ${answer.path}.`);
27698
+ });
27683
27699
  //#endregion
27684
27700
  //#region src/cli/joinLink.ts
27685
27701
  /**
@@ -27999,11 +28015,13 @@ async function findRepositories(home, limit = 25) {
27999
28015
  const RECOMMENDED = "Recommended";
28000
28016
  /** `~/…` for a path under the home directory: the name matters, the full path is context. */
28001
28017
  const shortPath = (path, home) => path === home || path.startsWith(`${home}${NodePath.sep}`) ? `~${path.slice(home.length)}` : path;
28002
- /** The lines above the menu: who, which workspace, and the project here. */
28003
- const menuContext = (status, here, home) => {
28018
+ /** The lines above the menu: who, which workspace, the project here, and the editors connected. */
28019
+ const menuContext = (status, here, editors) => {
28004
28020
  const names = status.workspaces.map((w) => w.slug);
28005
28021
  const lines = [`Signed in as ${status.email ?? "nobody"}`, names.length === 0 ? "Workspace: none yet" : names.length === 1 ? `Workspace: ${names[0]}` : `Workspaces: ${names.join(", ")}`];
28006
- if (here !== null) lines.push(`Current project: ${NodePath.basename(here.root)} (${shortPath(here.root, home)}) · ${here.state === "connected" ? `connected to ${here.workspace}/${here.project}` : here.state === "clone" ? `a clone of ${here.project}, not connected` : "not connected"}`);
28022
+ if (here !== null) lines.push(`Project: ${NodePath.basename(here.root)} · ${here.state === "connected" ? `Connected${here.project === NodePath.basename(here.root) ? "" : ` as ${here.workspace}/${here.project}`}` : here.state === "clone" ? `Clone of ${here.project}, not connected` : "Not connected"}`);
28023
+ const connected = editors.filter((e) => e.connected);
28024
+ lines.push(connected.length === 0 ? "Editors: none connected yet" : ["Editors:", ...connected.map((e) => ` ✓ ${e.title}`)].join("\n"));
28007
28025
  return lines;
28008
28026
  };
28009
28027
  /**
@@ -28012,7 +28030,7 @@ const menuContext = (status, here, home) => {
28012
28030
  * the project here, else inviting a teammate to it. Inviting is offered where they own or
28013
28031
  * administer a workspace; joining or creating one stays reachable, Exit last.
28014
28032
  */
28015
- const menuItems = (status, here) => {
28033
+ const menuItems = (status, here, editors = []) => {
28016
28034
  const offers = [...(status.foundings ?? []).filter((f) => f.expired !== true).map((f) => ({
28017
28035
  title: `Accept ${f.name}, founded for you`,
28018
28036
  value: {
@@ -28054,9 +28072,19 @@ const menuItems = (status, here) => {
28054
28072
  description: "The app: tasks, agents and who works on what.",
28055
28073
  value: { kind: "open" }
28056
28074
  }];
28075
+ const launch = here?.state === "connected" ? editors.filter((e) => e.connected && e.launchable).map((e) => ({
28076
+ title: `Open ${e.title}`,
28077
+ description: `In ${NodePath.basename(here.root)}, with Peer coordinating its agents.`,
28078
+ value: {
28079
+ kind: "launch",
28080
+ editor: e.id,
28081
+ folder: here.root
28082
+ }
28083
+ })) : [];
28057
28084
  const items = inWorkspace ? [
28058
28085
  ...offers,
28059
28086
  ...here?.state === "connected" ? [
28087
+ ...launch,
28060
28088
  ...invite,
28061
28089
  ...work,
28062
28090
  ...connectHere
@@ -28087,7 +28115,11 @@ const menuItems = (status, here) => {
28087
28115
  value: { kind: "join" }
28088
28116
  }
28089
28117
  ];
28090
- items.push({
28118
+ items.push(editors.some((e) => e.connected) ? {
28119
+ title: "Manage integrations",
28120
+ description: "Connect or disconnect editors and agents.",
28121
+ value: { kind: "editor" }
28122
+ } : {
28091
28123
  title: "Connect an editor",
28092
28124
  description: "So your agents (Claude Code, Codex, Cursor, …) coordinate through Peer.",
28093
28125
  value: { kind: "editor" }
@@ -28122,21 +28154,30 @@ const menu = (ask, steps, io) => gen$1(function* () {
28122
28154
  for (;;) {
28123
28155
  const status = yield* steps.status;
28124
28156
  const here = yield* steps.here;
28125
- const context = menuContext(status, here, steps.home).join("\n");
28157
+ const editors = yield* steps.editors;
28158
+ const editorsConnected = editors.some((e) => e.connected);
28159
+ const context = menuContext(status, here, editors).join("\n");
28126
28160
  if (context !== shown) {
28127
28161
  yield* io.say(context);
28128
28162
  shown = context;
28129
28163
  }
28130
- const item = yield* ask.choose("What would you like to do?", menuItems(status, here));
28131
- if (item === null || item.kind === "exit") return;
28164
+ const item = yield* ask.choose("What would you like to do?", menuItems(status, here, editors));
28165
+ if (item === null || item.kind === "exit") return { editorsConnected };
28166
+ if (item.kind === "launch") return {
28167
+ editorsConnected,
28168
+ launch: {
28169
+ editor: item.editor,
28170
+ folder: item.folder
28171
+ }
28172
+ };
28132
28173
  if (item.kind === "open") {
28133
- if (yield* steps.openPeer.pipe(as(true), catch_((error) => io.say(String(error.message).replace(/^peer core: /, "")).pipe(as(false))))) return;
28174
+ if (yield* steps.openPeer.pipe(as(true), catch_((error) => io.say(String(error.message).replace(/^peer core: /, "")).pipe(as(false))))) return { editorsConnected: false };
28134
28175
  continue;
28135
28176
  }
28136
28177
  const connect = (folder) => gen$1(function* () {
28137
28178
  const joined = yield* steps.share(folder);
28138
28179
  if (joined === null) return;
28139
- const ready = yield* steps.agentsReady;
28180
+ const ready = (yield* steps.editors).some((e) => e.connected);
28140
28181
  yield* io.say([
28141
28182
  "",
28142
28183
  `✓ Signed in as ${status.email ?? ""}`,
@@ -28174,10 +28215,19 @@ const menu = (ask, steps, io) => gen$1(function* () {
28174
28215
  if (folder !== null && folder.trim() !== "") yield* connect(folder.trim());
28175
28216
  return;
28176
28217
  }
28177
- case "editor":
28178
- yield* steps.editor;
28218
+ case "editor": {
28219
+ const id = yield* ask.choose("Which editor or agent?", editors.map((e) => ({
28220
+ title: `${e.connected ? "✓ " : " "}${e.title}`,
28221
+ description: e.connected ? "Connected: choose it to disconnect" : "Connect it",
28222
+ value: e.id
28223
+ })));
28224
+ const chosen = editors.find((e) => e.id === id);
28225
+ if (chosen === void 0) return;
28226
+ if (!chosen.connected) yield* steps.connectEditor(chosen.id);
28227
+ else if (yield* ask.confirm(`Disconnect ${chosen.title} from Peer?`, false)) yield* steps.disconnectEditor(chosen.id);
28179
28228
  shown = "";
28180
28229
  return;
28230
+ }
28181
28231
  case "workspace": {
28182
28232
  const next = yield* ask.choose("Join or create", [{
28183
28233
  title: "Join a workspace",
@@ -28732,7 +28782,7 @@ const runMenu = (stateDir) => gen$1(function* () {
28732
28782
  const workspaces = yield* PeerWorkspaces;
28733
28783
  const state = yield* PeerState;
28734
28784
  const status = workspaces.sync.pipe(hubFailure);
28735
- yield* menu(menuAsk, {
28785
+ return yield* menu(menuAsk, {
28736
28786
  status,
28737
28787
  home: NodeOS.homedir(),
28738
28788
  here: gen$1(function* () {
@@ -28757,11 +28807,17 @@ const runMenu = (stateDir) => gen$1(function* () {
28757
28807
  project: `${match.workspace}/${match.project}`
28758
28808
  };
28759
28809
  }),
28760
- agentsReady: gen$1(function* () {
28810
+ editors: gen$1(function* () {
28761
28811
  const coordination = (yield* status).coordination;
28762
28812
  const mcp = yield* (yield* McpClients).connected;
28763
- return coordination.claudeHooks || (coordination.codexHookHomes ?? []).some((home) => home.installed && home.trusted) || mcp.length > 0;
28764
- }).pipe(orElseSucceed(() => false)),
28813
+ const env = yield* Environment;
28814
+ return EDITORS.map((id) => ({
28815
+ id,
28816
+ title: editorTitle(id),
28817
+ connected: id === "claude" ? coordination.claudeHooks : id === "codex" ? (coordination.codexHookHomes ?? []).some((home) => home.installed) : mcp.includes(id),
28818
+ launchable: (id === "claude" || id === "codex") && onPath(id, env.PATH)
28819
+ }));
28820
+ }).pipe(orElseSucceed(() => [])),
28765
28821
  join: (slug) => join({ target: slug }, terminalIo).pipe(flatMap$1((joined) => sayNextStep(joined, terminalIo)), hubFailure),
28766
28822
  founding: (slug) => workspaces.acceptFounding({ slug }).pipe(andThen(log(`Accepted: ${slug}. You are its owner.`)), hubFailure),
28767
28823
  create: (name) => create({ name }, terminalIo).pipe(asVoid, hubFailure),
@@ -28814,10 +28870,8 @@ const runMenu = (stateDir) => gen$1(function* () {
28814
28870
  if (opener !== void 0) yield* launch(PEER_DOWNLOAD);
28815
28871
  return yield* fail(`peer core: the Peer app is not installed here${opener === void 0 ? "" : "; its download page is open"}: ${PEER_DOWNLOAD}`);
28816
28872
  }),
28817
- editor: gen$1(function* () {
28818
- const editor = yield* shareAsk.choose("Connect which editor?", editorChoices(yield* detectedEditors));
28819
- if (editor !== null) yield* connectEditor(editor, stateDir, terminalIo);
28820
- }).pipe(hubFailure)
28873
+ connectEditor: (id) => connectEditor(id, stateDir, terminalIo).pipe(hubFailure),
28874
+ disconnectEditor: (id) => disconnectEditor(id, terminalIo).pipe(hubFailure)
28821
28875
  }, terminalIo);
28822
28876
  });
28823
28877
  const registerIn = (stateDir) => (path, name) => tryPromise({
@@ -28832,12 +28886,15 @@ const loginCommand = make$62("login", {
28832
28886
  }).pipe(withDescription("Sign this computer's Peer in to its Peer Hub; on a terminal, then a menu: create or join a workspace, share, invite."), withHandler((flags) => gen$1(function* () {
28833
28887
  const config = yield* resolveConfig(flags);
28834
28888
  yield* refuseWhileRunning(config.stateDir);
28835
- yield* gen$1(function* () {
28889
+ const end = yield* gen$1(function* () {
28836
28890
  const signedIn = (yield* (yield* PeerState).read).persisted.email;
28837
28891
  const email = getOrUndefined(flags.email)?.trim();
28838
28892
  if (email === void 0 || signedIn !== null && signedIn === email) {
28839
28893
  if (signedIn === null) return yield* fail("peer core: nobody is signed in; run peer core login <email>.");
28840
- if (!isTerminal()) return yield* log(`Signed in: ${signedIn}`);
28894
+ if (!isTerminal()) {
28895
+ yield* log(`Signed in: ${signedIn}`);
28896
+ return;
28897
+ }
28841
28898
  return yield* runMenu(config.stateDir);
28842
28899
  }
28843
28900
  yield* login({
@@ -28845,9 +28902,41 @@ const loginCommand = make$62("login", {
28845
28902
  ...isSome(flags.code) ? { code: flags.code.value } : {},
28846
28903
  ...isSome(flags.hub) ? { hub: flags.hub.value } : {}
28847
28904
  }, terminalIo);
28848
- if (isTerminal()) yield* runMenu(config.stateDir);
28905
+ return isTerminal() ? yield* runMenu(config.stateDir) : void 0;
28849
28906
  }).pipe(hubFailure, provide(layer$107.pipe(provideMerge(oneShotLayer(config)))));
28907
+ if (end !== void 0) yield* afterMenu(end, config, flags);
28850
28908
  })));
28909
+ /** Whether `command` is an executable in one of `path`'s folders. */
28910
+ const onPath = (command, path) => (path ?? "").split(NodePath.delimiter).some((dir) => {
28911
+ try {
28912
+ NodeFS.accessSync(NodePath.join(dir, command), NodeFS.constants.X_OK);
28913
+ return true;
28914
+ } catch {
28915
+ return false;
28916
+ }
28917
+ });
28918
+ /**
28919
+ * After the menu, with its state written and closed: Peer Core keeps coordinating when an editor
28920
+ * is connected and no Peer runs (the hooks only talk to a running Peer), and the editor the
28921
+ * person chose starts in the project, in this terminal.
28922
+ */
28923
+ const afterMenu = (end, config, flags) => gen$1(function* () {
28924
+ if ((end.editorsConnected || end.launch !== void 0) && (yield* ownerOf(config.stateDir)) === null) {
28925
+ yield* serveDetached(config, flags);
28926
+ yield* log("Peer Core coordinates your agents in the background; peer core stop ends it.");
28927
+ }
28928
+ const launch = end.launch;
28929
+ if (launch === void 0) return;
28930
+ yield* log(`Starting ${editorTitle(launch.editor)} in ${launch.folder}…`);
28931
+ if ((yield* callback((resume) => {
28932
+ const child = NodeChildProcess.spawn(launch.editor, [], {
28933
+ cwd: launch.folder,
28934
+ stdio: "inherit"
28935
+ });
28936
+ child.once("error", () => resume(succeed$1(127)));
28937
+ child.once("exit", (exit) => resume(succeed$1(exit ?? 0)));
28938
+ })) === 127) return yield* fail(`peer core: could not start ${launch.editor}; is it installed?`);
28939
+ });
28851
28940
  const createCommand = make$62("create", {
28852
28941
  ...authLocationFlags,
28853
28942
  name: String$2("name").pipe(withDescription$2("The workspace's name.")),
@@ -55448,4 +55537,4 @@ function runCli(root = "t3") {
55448
55537
  //#endregion
55449
55538
  export { runCli };
55450
55539
 
55451
- //# sourceMappingURL=binCli-BD8xnLcs.mjs.map
55540
+ //# sourceMappingURL=binCli-B32nSvwp.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer-core",
3
- "version": "0.1.977",
3
+ "version": "0.1.978",
4
4
  "description": "Peer's coordination for coding agents without the Peer app: the `peer` command (Peer Core).",
5
5
  "license": "MIT",
6
6
  "repository": {