@amenophis1er/foreman 0.1.3 → 0.1.4

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
@@ -60,6 +60,19 @@ which account pays, the Claude Code install, Ollama and Codex if present, the
60
60
  browser missions will use, the port, Tailscale, the data directory — and
61
61
  exits. Nothing blocks unless it says so.
62
62
 
63
+ **Environment.** Everything the CLI reads; `foreman --help` prints the same list.
64
+
65
+ | Variable | Meaning | Default |
66
+ |---|---|---|
67
+ | `PORT` | listen port | `4177` |
68
+ | `FOREMAN_HOME` | state directory: runs, settings, logs | `~/.foreman` |
69
+ | `FOREMAN_BIND` | `auto` (loopback + Tailscale when present), `local`, or `all` | `auto` |
70
+ | `FOREMAN_BROWSER` | browser for missions: `chrome`, `chromium`, `msedge`, `firefox` | `chrome` |
71
+ | `FOREMAN_CLAUDE_CONFIG_DIR` | the Claude Code install missions run under | inherited |
72
+ | `FOREMAN_CLAUDE_EXECUTABLE` | the Claude Code executable | bundled |
73
+ | `FOREMAN_AUTH_MODE` | assert `api-key` or `subscription`; fail at start on mismatch | unset |
74
+ | `FOREMAN_NO_TELEGRAM` | `1` to start without the Telegram bot: for a second server beside the main one, since Telegram allows one poller per bot | unset |
75
+
63
76
  ## A mission, start to finish
64
77
 
65
78
  1. **Fleet.** The board: what needs you (answerable right there), what is
@@ -213,6 +226,16 @@ npm run dev # API + Vite together
213
226
  scripts/dev-restart.sh # restarts the server only when nothing would be lost
214
227
  ```
215
228
 
229
+ To run a checkout beside an installed Foreman on the same machine, give it its
230
+ own port and leave the bot to the installed one:
231
+
232
+ ```sh
233
+ PORT=4178 FOREMAN_BIND=local FOREMAN_NO_TELEGRAM=1 npm start
234
+ ```
235
+
236
+ Both read `~/.foreman`; look from the second, drive from the first — two
237
+ servers running missions against one store is the one case nothing guards.
238
+
216
239
  Releases: `npm version <patch|minor|major> && git push --follow-tags` — CI
217
240
  tests, publishes to npm with provenance, and creates the GitHub release.
218
241
 
package/bin/foreman.mjs CHANGED
@@ -43,6 +43,7 @@ Environment:
43
43
  FOREMAN_CLAUDE_CONFIG_DIR Claude Code install missions run under
44
44
  FOREMAN_CLAUDE_EXECUTABLE Claude Code executable (default: bundled)
45
45
  FOREMAN_AUTH_MODE Assert 'api-key' or 'subscription'; fail on mismatch
46
+ FOREMAN_NO_TELEGRAM=1 Do not attach the Telegram bot (a second server beside the main one)
46
47
  `;
47
48
 
48
49
  const [command = 'start', ...rest] = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amenophis1er/foreman",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Autonomous mission runner on the Claude Agent SDK: a director plans, delegates to workers, verifies, and reports — from one dashboard, your phone, or the CLI.",
5
5
  "keywords": [
6
6
  "claude",
package/src/cli.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * be up when the laptop lid is closed; this is that.
14
14
  */
15
15
  import { execFile, spawn } from 'node:child_process';
16
- import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
16
+ import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
17
17
  import { openSync } from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
@@ -170,8 +170,12 @@ async function serviceUninstall(): Promise<number> {
170
170
  /** Is the service registered, and is it running? Null pid when registered but idle. */
171
171
  async function serviceState(): Promise<{ installed: boolean; running: boolean; pid?: number }> {
172
172
  if (process.platform === 'darwin') {
173
+ // Installed means the plist is on disk; a stopped service (booted out
174
+ // by `service stop`) is still installed, and `service start` brings it
175
+ // back. launchctl only knows about loaded jobs, so it answers "running".
176
+ const installed = await access(path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`)).then(() => true, () => false);
173
177
  const r = await sh('launchctl', ['print', `gui/${os.userInfo().uid}/${LABEL}`]);
174
- if (r.code !== 0) return { installed: false, running: false };
178
+ if (r.code !== 0) return { installed, running: false };
175
179
  const pid = Number(/pid = (\d+)/.exec(r.out)?.[1]);
176
180
  return { installed: true, running: Number.isFinite(pid) && pid > 0, ...(pid ? { pid } : {}) };
177
181
  }
@@ -230,7 +234,11 @@ async function serviceRestart(): Promise<number> {
230
234
  async function serviceStatus(): Promise<number> {
231
235
  if (process.platform === 'darwin') {
232
236
  const r = await sh('launchctl', ['print', `gui/${os.userInfo().uid}/${LABEL}`]);
233
- if (r.code !== 0) { console.log('Not installed. `foreman service install` keeps Foreman running.'); return 1; }
237
+ if (r.code !== 0) {
238
+ const st = await serviceState();
239
+ console.log(st.installed ? 'Installed · stopped — `foreman service start` starts it (it also comes back at next login).' : 'Not installed. `foreman service install` keeps Foreman running.');
240
+ return 1;
241
+ }
234
242
  const state = /state = (\w+)/.exec(r.out)?.[1] ?? 'unknown';
235
243
  const pid = /pid = (\d+)/.exec(r.out)?.[1];
236
244
  console.log(`Installed · ${state}${pid ? ` · pid ${pid}` : ''} · http://localhost:${PORT}`);
package/src/server.ts CHANGED
@@ -608,6 +608,11 @@ let telegramBot: TelegramBot | null = null;
608
608
  /** (Re)build the Telegram side from the stored token and linked chat. */
609
609
  async function reattachTelegram(): Promise<boolean> {
610
610
  notifyHub.detach('telegram');
611
+ // A second server beside the installed one (a dev checkout on another
612
+ // port) must not start a second poller: Telegram allows one per bot, and
613
+ // two fight over getUpdates. FOREMAN_NO_TELEGRAM=1 leaves the channel to
614
+ // whichever server does not set it.
615
+ if (process.env.FOREMAN_NO_TELEGRAM === '1') return false;
611
616
  const s = await notifySettings();
612
617
  const token = await getSecret(store.root, 'telegram');
613
618
  if (!token) { void telegramBot?.stop(); telegramBot = null; return false; }
@@ -1348,6 +1353,7 @@ const server = http.createServer(async (req, res) => {
1348
1353
  providerHasKey: await providerHasKeyOf(p),
1349
1354
  activeRun: run ? { ...run.meta } : null,
1350
1355
  lastRun: lastRun && {
1356
+ id: lastRun.id,
1351
1357
  mission: lastRun.mission, title: lastRun.title, status: lastRun.status,
1352
1358
  createdAt: lastRun.createdAt, costUsd: lastRun.costUsd,
1353
1359
  // The card may print a dollar only where the dollar was real.