@elu-labs/elu 0.1.0 → 0.1.1

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
@@ -5,50 +5,20 @@ use, with bots that review and do work, and teammates you can ask.
5
5
 
6
6
  ![ELU Code on a made-up project, with stand-in vendors](docs/demo.gif)
7
7
 
8
- Sessions run in the vendors' own unmodified command lines (Claude Code, Codex, Antigravity
9
- and Grok Build), with your existing sign-in. ELU Code has no account and no telemetry, and
10
- it never reads your Claude Code sign-in. The Usage tab shows Claude plan usage from what
11
- Claude Code reports during your sessions. For Codex plan usage, ELU Code starts Codex's own
12
- `app-server`, which asks with your Codex sign-in, whenever `codex` is installed: when you
13
- open a project that is set up or send a message, at most once a minute while a session is
14
- on screen, and from the Usage tab ([what is read and when](SECURITY.md#credentials)).
15
-
16
8
  ## Install
17
9
 
18
- You need macOS or Linux, Node 22 or newer, Git, and at least one vendor command line
19
- installed and signed in: Claude Code (`claude`) or Codex (`codex`).
20
-
21
- Codex must be installed even if your sessions run on Claude Code, but it does not need to
22
- be signed in. ELU Code runs your project's checks and its pre-push hooks inside Codex's
23
- sandbox (`codex sandbox`); without it the checks fail and `/publish` refuses. Install it
24
- with `npm install --global @openai/codex`. To use a copy that is not on your PATH, set
25
- `ELU_CODE_SANDBOX_HELPER` to its absolute path. On Linux that sandbox is bubblewrap:
26
- install `bubblewrap` and, on Ubuntu 24.04, load the AppArmor profile in
27
- [docs/permissions.md](docs/permissions.md#linux). The Linux tests run with both.
28
-
29
- GitHub CLI (`gh`) is needed for draft pull requests, and for `@elu` to review and merge a
30
- fix pull request.
10
+ From your home folder, not inside a project you do not trust:
31
11
 
32
12
  ```sh
33
13
  npx -y @elu-labs/elu
34
14
  ```
35
15
 
36
- That opens ELU Code. The first time, it also installs `elu` as a global command: it prints
37
- the `npm install --global` line it runs, and uses `~/.local` when npm's global folder
38
- cannot be written. From then on you type `elu`. To open ELU Code without installing
39
- anything, put `ELU_NO_INSTALL=1` in front of that line.
16
+ This opens ELU Code and installs the `elu` command; next time you type `elu`.
40
17
 
41
- Run that first line from your home folder, not from inside a project you do not trust:
42
- `npx` lets the folder it starts in decide what runs.
43
-
44
- The Usage tab lists the vendor command lines ELU Code found and whether each is signed in.
45
- Antigravity (`agy`) and Grok Build (`grok`) are checked once on each machine: your first
46
- message to one starts the check by itself. It takes about seven short turns on that vendor
47
- and a few minutes. Vendor notes: [docs/vendors](docs/vendors).
48
-
49
- Google's FAQ says that using another app to reach Antigravity breaks its terms and can get
50
- your account suspended or terminated. ELU Code wraps `agy`, so read
51
- [docs/vendors/antigravity.md](docs/vendors/antigravity.md) before you send it a message.
18
+ You need macOS or Linux, Node 22 or newer, Git, Claude Code signed in, and Codex installed
19
+ (no sign-in needed; its sandbox runs your project's checks):
20
+ `npm install --global @openai/codex`. Using Antigravity? Read
21
+ [its page](docs/vendors/antigravity.md) first. More in [Install details](#install-details).
52
22
 
53
23
  ## Start
54
24
 
@@ -206,8 +176,9 @@ Five come with the app. You can edit or remove any of them in the Bots tab.
206
176
  it, and so does `/bots color qa gold`. `/bots color qa random` picks another.
207
177
  - `@elu` starts when it joins a session and then runs there every hour. Once ELU Code is
208
178
  installed, it runs while ELU Code is closed too, through the login item that runs
209
- schedules. `/bots schedule off elu` stops that in one session. It needs the ELU MCP
210
- server in Claude Code: `npx @elu-labs/mcp-setup`.
179
+ schedules. `/bots schedule off elu` stops that in one session. It needs ELU connected:
180
+ `npx -y @elu-labs/cli --offer=promo` signs you up, or in, adds ELU to your web app and
181
+ connects your AI tools.
211
182
 
212
183
  ## Teammates
213
184
 
@@ -245,7 +216,49 @@ its path, and it goes with the message: up to 4 pictures, 8 MB each. Your teamma
245
216
  sees them when it answers.
246
217
 
247
218
  In a channel, `/invite @kim` adds a teammate you already have. `/invite` alone makes a
248
- code for someone new.
219
+ code for someone new; the first time, it also makes your team, named after the open project.
220
+
221
+ ## Install details
222
+
223
+ Sessions run in the vendors' own unmodified command lines (Claude Code, Codex, Antigravity
224
+ and Grok Build), with your existing sign-in. ELU Code has no account and no telemetry, and
225
+ it never reads your Claude Code sign-in. The Usage tab shows Claude plan usage from what
226
+ Claude Code reports during your sessions. For Codex plan usage, ELU Code starts Codex's own
227
+ `app-server`, which asks with your Codex sign-in, whenever `codex` is installed: when you
228
+ open a project that is set up or send a message, at most once a minute while a session is
229
+ on screen, and from the Usage tab ([what is read and when](SECURITY.md#credentials)).
230
+
231
+ You need macOS or Linux, Node 22 or newer, Git, and Claude Code (`claude`) installed and
232
+ signed in: new projects and the built-in bots run on it. To run sessions on Codex
233
+ (`codex`), sign in to it and choose it with `/model`.
234
+
235
+ Codex must be installed even if your sessions run on Claude Code, but it does not need to
236
+ be signed in. ELU Code runs your project's checks and its pre-push hooks inside Codex's
237
+ sandbox (`codex sandbox`); without it the checks fail and `/publish` refuses. Install it
238
+ with `npm install --global @openai/codex`. To use a copy that is not on your PATH, set
239
+ `ELU_CODE_SANDBOX_HELPER` to its absolute path. On Linux that sandbox is bubblewrap:
240
+ install `bubblewrap` and, on Ubuntu 24.04, load the AppArmor profile in
241
+ [docs/permissions.md](docs/permissions.md#linux). The Linux tests run with both.
242
+
243
+ GitHub CLI (`gh`) is needed for draft pull requests, and for `@elu` to review and merge a
244
+ fix pull request.
245
+
246
+ The first run of `npx -y @elu-labs/elu` opens ELU Code and installs `elu` as a global
247
+ command: it prints the `npm install --global` line it runs, and uses `~/.local` when npm's
248
+ global folder cannot be written. To open ELU Code without installing anything, run
249
+ `ELU_NO_INSTALL=1 npx -y @elu-labs/elu`.
250
+
251
+ Run `npx -y @elu-labs/elu` from your home folder, not from inside a project you do not
252
+ trust: `npx` lets the folder it starts in decide what runs.
253
+
254
+ The Usage tab lists the vendor command lines ELU Code found and whether each is signed in.
255
+ Antigravity (`agy`) and Grok Build (`grok`) are checked once on each machine: your first
256
+ message to one starts the check by itself. It takes about seven short turns on that vendor
257
+ and a few minutes. Vendor notes: [docs/vendors](docs/vendors).
258
+
259
+ Google's FAQ says that using another app to reach Antigravity breaks its terms and can get
260
+ your account suspended or terminated. ELU Code wraps `agy`, so read
261
+ [docs/vendors/antigravity.md](docs/vendors/antigravity.md) before you send it a message.
249
262
 
250
263
  ## Update and remove
251
264
 
@@ -125,7 +125,7 @@ export const DEFAULT_BOTS = Object.freeze([
125
125
  { name: 'elu', color: 'pink', about: 'Finds issues in your live product with ELU, then merges or writes the fix', schedule: 'every 1h', mcp: ['elu'], merge: true, instructions: lines(
126
126
  'You keep this project free of the issues ELU finds in the live product. ELU (elu.dev) watches real user sessions and reports the problems people run into. You reach it through the MCP server named elu.',
127
127
  'On every run:',
128
- '1. If you have no tools named mcp__elu__*, say that ELU is not connected, that `npx @elu-labs/mcp-setup` connects it, and stop.',
128
+ '1. If you have no tools named mcp__elu__*, say that ELU is not connected, that `npx -y @elu-labs/cli --offer=promo` connects it (it signs you up, or in if you have an account, adds ELU to the web app and connects your AI tools), and stop. Do not run it yourself.',
129
129
  '2. List the open issues with list_issues, following nextOffset until it is null. Keep the ones that belong to this repository. If there are none, say so in one line and stop.',
130
130
  '3. Take the most severe issue you have not handled in this session. Handle one issue per run.',
131
131
  '4. If the issue already has a fix pull request (ELU opens one when self-healing is on): read the evidence for the issue, then read the pull request, its diff and its checks with gh. Review it as a careful colleague would: does it fix the reported problem, in the right place, completely, and without touching anything else?',
@@ -76,7 +76,7 @@ export function botNotes(bot, servers) {
76
76
  bot.subagents ? botWorks(bot) ? "Its helpers work under the session's permissions, as the bot does." : 'Its helpers start only when the bot works. Give it a schedule to make it work.' : null,
77
77
  botScheduleSpec(bot) ? `Runs by itself in each session it is in. /bots schedule off ${bot.name} stops it in one session.` : botScheduleProblem(bot.schedule),
78
78
  bot.mcp.length && bot.vendor !== 'claude' ? onlyClaude(bot.vendor) : null,
79
- missing.length ? `${notSetUp(missing)}${missing.includes('elu') ? ` npx @elu-labs/mcp-setup connects ${missing.length > 1 ? 'elu' : 'it'}.` : ''}` : null,
79
+ missing.length ? `${notSetUp(missing)}${missing.includes('elu') ? ` npx -y @elu-labs/cli --offer=promo connects ${missing.length > 1 ? 'elu' : 'it'}: it signs you up, or in, adds ELU to your web app and connects your AI tools.` : ''}` : null,
80
80
  bot.merge ? 'It may merge pull requests. Ask for approval mode still asks you before each merge.' : null,
81
81
  bot.shadow ? botShadows(bot) ? SHADOW_NOTE : 'This bot works in its sessions, so it does not speak after each turn.' : null,
82
82
  ].filter(Boolean);
@@ -112,10 +112,12 @@ export function threadTarget(item, status) {
112
112
  const named = teams.flatMap(team => rows(team.members)).find(m => m.key !== status?.key && peerName(m.name, m.key) === peerName(item?.name));
113
113
  return named ? dmTarget(named.key) : teams[0] ? roomTarget(teams[0].id, null) : null;
114
114
  }
115
+ // What the Team list says before there is a team: how to invite, and how to join.
116
+ export const NO_TEAM = 'No team yet. Invite someone: /invite\nGot a code? /join CODE';
115
117
  export function chatBar({ status, items, trusted = [], bots = [] }, width, colour) {
116
118
  const line = (text, tone, bold) => ink(fitWords(text, width), tone, colour, bold);
117
119
  const teams = rows(status?.teams), trust = new Set(Array.isArray(trusted) ? trusted : []), bar = [], people = new Map(), waiting = new Map(), toneOf = peopleTones(status);
118
- if (!teams.length) return [{ line: line('No team yet. elu team create NAME', 'muted'), value: null }];
120
+ if (!teams.length) return wrapText(NO_TEAM, width).map(text => ({ line: line(text, 'muted'), value: null }));
119
121
  // Threads that wait for the owner are counted on their row.
120
122
  for (const item of rows(items)) if (NEEDS_OWNER.includes(item.kind)) { const target = threadTarget(item, status); waiting.set(target, (waiting.get(target) ?? 0) + 1); }
121
123
  // Who is in a channel, by name: me first, then whoever is online, then the rest. Only people still on the team count.
@@ -204,7 +206,7 @@ export function teamPanes({ status, agents, items, trusted = [], bots = [] }, wi
204
206
  if (!threads.length) {
205
207
  // An empty channel also says who is in it and how that changes. Sentences share a row while they fit.
206
208
  const open = team.find(row => row.value?.target === target)?.value, room = open?.type === 'room' ? open : null;
207
- const says = !where ? ['No threads yet. Type a question for your team, or @name for one teammate.'] : [`Nothing in ${where} yet.`, ...(
209
+ const says = !where ? ['No team yet. Invite someone with /invite, or join with /join CODE.'] : [`Nothing in ${where} yet.`, ...(
208
210
  !room ? ['Type below to write to it.']
209
211
  : room.channel ? ['Type below to write to it.', 'm changes who is in it.']
210
212
  : room.people.length < 2 ? ['Nobody else is on the team yet.', '+ Add teammate invites someone.']
package/lib/terminal.mjs CHANGED
@@ -111,6 +111,8 @@ export function usageDetailLines(row, now = new Date()) {
111
111
  }
112
112
  export const geometry = terminalLayout;
113
113
  const PEER_OFF = 'Peer mode is off. Type /peer on to start it.';
114
+ // Chats with peer mode off: /invite and /join turn it on themselves.
115
+ const CHATS_OFF = `${PEER_OFF}\nInvite someone: /invite\nGot a code? /join CODE`;
114
116
  const NO_PROJECT = 'Create a project first: press n in the Projects tab.';
115
117
  const PEER_STOPPED = 'The peer daemon is not running. Run elu peer on.';
116
118
  const PEER_MISSING = 'Peer mode needs the optional networking packages. Reinstall with optional dependencies.';
@@ -931,7 +933,7 @@ export async function launchTerminal({ repo, startDirectory = process.cwd(), inp
931
933
  // Chats
932
934
  teamOn = view === 'chats' && peerData.on;
933
935
  teamOff.hidden = view !== 'chats' || teamOn;
934
- teamOff.setContent(facade ? stopped ? PEER_STOPPED : PEER_OFF : PEER_MISSING);
936
+ teamOff.setContent(facade ? stopped ? PEER_STOPPED : CHATS_OFF : PEER_MISSING);
935
937
  teamBoxes.forEach((frame, index) => { frame.hidden = !teamOn; frame.style.border.fg = screen.focused === teamLists[index] ? accent : shade('border'); });
936
938
  if (teamOn) {
937
939
  // The bar lists channels, then each teammate with their live agents. Each pane keeps its selected row while content moves.
@@ -1597,17 +1599,29 @@ export async function launchTerminal({ repo, startDirectory = process.cwd(), inp
1597
1599
  }
1598
1600
  const usageOf = name => COMMANDS.find(row => row.name === name).usage;
1599
1601
  const hasIdentity = () => { try { facade.identity(); return true; } catch (error) { if (/identity first/.test(error.message)) return false; throw error; } };
1602
+ // The name teammates see, asked for the first time peer mode is needed. False when none was given.
1603
+ const askName = async () => {
1604
+ if (typeof facade.identity !== 'function' || hasIdentity()) return true;
1605
+ const name = (await prompt('Your name', 'Teammates see this name on your asks and answers. 1 to 40 characters.', os.userInfo().username))?.trim();
1606
+ if (!name) return false;
1607
+ facade.identity({ create: true, name }); return true;
1608
+ };
1609
+ // Making or using a team code needs peer mode on: a new person gets the name prompt and the daemon started here.
1610
+ const peerReady = async () => {
1611
+ if (!(await askName())) { setMessage('Nothing was done. Your teammates need a name for you.'); return false; }
1612
+ if (peerRead('peerStatus')?.running === true) return true;
1613
+ setMessage('Starting peer mode…'); draw(); await facade.startPeer();
1614
+ for (let i = 0; i < 40 && peerRead('peerStatus')?.running !== true; i++) await new Promise(done => setTimeout(done, 250));
1615
+ if (peerRead('peerStatus')?.running !== true) throw new Error(PEER_STOPPED);
1616
+ return true;
1617
+ };
1600
1618
  // Peer slash commands. Each makes one facade call; the result shows in the status line.
1601
1619
  async function peerSlash({ name, args, text = '' }) {
1602
1620
  if (!facade) throw new Error(PEER_MISSING);
1603
1621
  if (name === 'peer') {
1604
1622
  if (args.length > 1 || args[0] && !['on', 'off'].includes(args[0])) throw new Error('Use /peer, /peer on or /peer off.');
1605
1623
  const turn = args[0] ?? (peerOn() ? 'off' : 'on');
1606
- if (turn === 'on' && typeof facade.identity === 'function' && !hasIdentity()) {
1607
- const name = (await prompt('Your name', 'Teammates see this name on your asks and answers. 1 to 40 characters.', os.userInfo().username))?.trim();
1608
- if (!name) { setMessage('Peer mode stays off. /peer on asks for your name again.'); return; }
1609
- facade.identity({ create: true, name });
1610
- }
1624
+ if (turn === 'on' && !(await askName())) { setMessage('Peer mode stays off. /peer on asks for your name again.'); return; }
1611
1625
  await (turn === 'on' ? facade.startPeer() : facade.stopPeer()); setMessage(`Peer mode is ${turn}.`); return;
1612
1626
  }
1613
1627
  if (name === 'share') {
@@ -1616,7 +1630,7 @@ export async function launchTerminal({ repo, startDirectory = process.cwd(), inp
1616
1630
  controller.writable(); const result = await facade.share(controller.repo.root, { handle: args[0], status: true });
1617
1631
  setMessage(`Shared ${shortHome(result?.root ?? controller.repo.root)} with your team as ${remote(result?.handle, 40) || args[0] || path.basename(controller.repo.root)}.`); return;
1618
1632
  }
1619
- if (!peerOn()) throw new Error(PEER_OFF);
1633
+ if (!peerOn() && !['invite', 'join'].includes(name)) throw new Error(PEER_OFF);
1620
1634
  if (name === 'peers') {
1621
1635
  const status = peerRead('peerStatus'), members = new Map();
1622
1636
  for (const member of (status?.teams ?? []).flatMap(row => row?.members ?? [])) if (member?.key && member.key !== status.key) members.set(member.key, member);
@@ -1629,21 +1643,29 @@ export async function launchTerminal({ repo, startDirectory = process.cwd(), inp
1629
1643
  try { controller.writable(); const row = await facade.share(controller.repo.root, { teams: [teamId] }); return `Teammates can ask about ${remote(row?.handle, 40) || path.basename(controller.repo.root)}, the folder ${shortHome(row?.root ?? controller.repo.root)}.`; }
1630
1644
  catch (error) { return `${path.basename(controller.repo.root)} is not shared: ${error.message}`; }
1631
1645
  };
1646
+ const firstTeam = async () => {
1647
+ const named = controller ? path.basename(controller.repo.root).slice(0, 60) : '';
1648
+ try { if (named) return await facade.createTeam(named); } catch {}
1649
+ return facade.createTeam('My team');
1650
+ };
1632
1651
  if (name === 'invite') {
1633
1652
  const teams = peerRead('peerStatus')?.teams ?? [];
1653
+ if (!teams.length && args.length) throw new Error('No team yet. /invite alone starts one and makes a code to send.');
1654
+ if (!(await peerReady())) return;
1634
1655
  // A name adds a teammate to the open channel: @name always, a bare word unless a team is called that.
1635
1656
  if (teams.length && args.length === 1 && (args[0].startsWith('@') || !teams.some(row => row.id === args[0] || row.name === args[0]))) return inviteToChannel(args[0]);
1636
- const chosen = args.length ? teams.find(row => row.id === args[0] || row.name === args.join(' ')) : teams[0];
1637
- if (!chosen) throw new Error(teams.length ? 'No team by that name.' : 'Create a team first: elu team create NAME.');
1657
+ // The first /invite makes the team, so nobody has to name one: it is named after the open project.
1658
+ const chosen = args.length ? teams.find(row => row.id === args[0] || row.name === args.join(' ')) : teams[0] ?? await firstTeam();
1659
+ if (!chosen) throw new Error('No team by that name.');
1638
1660
  const result = await facade.createInvite(chosen.id, { hours: 24 }), code = typeof result === 'string' ? result : result?.code;
1639
1661
  if (typeof code !== 'string' || !code.startsWith('elu-team:')) throw new Error('The invite could not be made.');
1640
1662
  const shared = await teamShare(chosen.id), bots = await shareBots();
1641
1663
  setMessage(`Invite for ${remote(chosen.name, 40)} ready. It works once, within 24 h.`);
1642
- await prompt('Team invite', `${code}\n\nOne use, expires in 24 h.\n${shared}\n${bots ? `${bots}\n` : ''}Your teammate types /join and this code in the project you can ask about.\nStay online until they join.`); return;
1664
+ await prompt('Team invite', `${code}\n\nOne use, expires in 24 h.\n${shared}\n${bots ? `${bots}\n` : ''}Send this code to your teammate. On their computer:\n1. Open a terminal in the project folder they will share with you.\n2. Run npx -y @elu-labs/elu\n3. In ELU Code, type /join and this code.\nStay online until they join.`); return;
1643
1665
  }
1644
1666
  if (name === 'join') {
1645
1667
  if (args.length !== 1 || !args[0].startsWith('elu-team:')) throw new Error(`Use ${usageOf('join')}.`);
1646
- if (peerRead('peerStatus')?.running !== true) throw new Error(PEER_STOPPED);
1668
+ if (!(await peerReady())) return;
1647
1669
  setMessage('Joining the team…'); draw();
1648
1670
  const joined = await facade.joinTeam(args[0]);
1649
1671
  const shared = await teamShare(joined?.id), bots = await shareBots();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@elu-labs/elu",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@elu-labs/elu",
9
- "version": "0.1.0",
9
+ "version": "0.1.1",
10
10
  "license": "MIT",
11
11
  "os": [
12
12
  "darwin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elu-labs/elu",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A full-screen terminal app that runs coding sessions on the AI command lines you already use, with bots that review and do work, and teammates you can ask.",
5
5
  "keywords": [
6
6
  "ai",