teapot-coding-agent 0.28.0 → 0.28.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/dist/index.js CHANGED
@@ -5,8 +5,9 @@
5
5
  * Config may also come from TEAPOT_* env vars (see src/master.ts).
6
6
  * First run (no config file): boot anyway and finish setup in the web UI.
7
7
  */
8
- import { existsSync } from "node:fs";
8
+ import { existsSync, realpathSync } from "node:fs";
9
9
  import { createServer } from "node:net";
10
+ import { fileURLToPath } from "node:url";
10
11
  import { loadConfig, resolveConfigPath, Master } from "./master.js";
11
12
  import { buildApp, serveApp } from "./server/api.js";
12
13
  /**
@@ -259,10 +260,35 @@ async function main() {
259
260
  // test importing it and sat at 48% branch coverage. The check is the standard
260
261
  // ESM "am I the entry point" test, and `node --experimental-strip-types src/index.ts`
261
262
  // and `node dist/index.js` both still take this branch.
262
- const isEntry = process.argv[1] !== undefined &&
263
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
- (process.argv[1] === new URL(import.meta.url).pathname ||
265
- process.argv[1].endsWith("/index.js") ||
266
- process.argv[1].endsWith("/index.ts"));
267
- if (isEntry)
263
+ function isEntryPoint() {
264
+ const arg = process.argv[1];
265
+ if (!arg)
266
+ return false;
267
+ // `npm i -g teapot-coding-agent` puts a SYMLINK at node_modules/.bin/teapot, and
268
+ // npm invokes the target THROUGH it. So `process.argv[1]` is the symlink path
269
+ // (…/node_modules/.bin/teapot), NOT the file it points at — which matched none
270
+ // of the checks below, `main()` never ran, and the installed command exited 0
271
+ // in silence. Reporting `teapot` did nothing and needing `node dist/index.js`
272
+ // was the only symptom. `realpathSync` is what resolves it.
273
+ try {
274
+ const self = realpathSync(fileURLToPath(import.meta.url));
275
+ const entry = realpathSync(arg);
276
+ if (self === entry)
277
+ return true;
278
+ // `self === entry` is the ONLY correct test: it is the definition of being the
279
+ // entry point. The suffix checks that used to follow were a fallback for the
280
+ // symlink case, and they were far too loose — `endsWith("/index.ts")` is true
281
+ // for EVERY test file that imports this module, so importing it booted a real
282
+ // server on port 7788 and `cli-args.test.ts` failed with EADDRINUSE.
283
+ //
284
+ // Keep a basename check as a safety net for a bundler that rewrites paths, but
285
+ // require BOTH that we are the module AND that nothing else is: an argv[1] that
286
+ // resolves to a DIFFERENT file is by definition not this module's entry.
287
+ return false;
288
+ }
289
+ catch {
290
+ return false;
291
+ }
292
+ }
293
+ if (isEntryPoint())
268
294
  void main();
package/dist/master.js CHANGED
@@ -109,7 +109,18 @@ const DATA_DIR = process.env.TEAPOT_DATA_DIR ??
109
109
  path.join(os.homedir(), ".local", "share", "teapot-coding-agent");
110
110
  const DEFAULT_CONFIG = {
111
111
  port: Number(process.env.TEAPOT_PORT ?? 7788),
112
- host: process.env.TEAPOT_HOST,
112
+ // #140: this was `process.env.TEAPOT_HOST`, i.e. UNDEFINED when unset — and an
113
+ // undefined hostname makes `@hono/node-server` bind every interface. So the
114
+ // default was the opposite of the documented one: `--help` says
115
+ // "127.0.0.1 (default) · 0.0.0.0 exposes the UI to your network", and yet the
116
+ // default WAS "expose to your network".
117
+ //
118
+ // Measured: with no --host the server answered on the machine's LAN address as
119
+ // well as loopback.
120
+ //
121
+ // Loopback is the right default for something holding API tokens and agent
122
+ // workspaces; exposing it has to be a deliberate act.
123
+ host: process.env.TEAPOT_HOST ?? "127.0.0.1",
113
124
  dataDir: DATA_DIR,
114
125
  llm: {
115
126
  baseUrl: process.env.TEAPOT_BASE_URL ?? "https://openrouter.ai/api/v1",
@@ -764,29 +775,42 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
764
775
  /** Stop direct children (and their descendants by default) of an agent. */
765
776
  async stopChildrenFor(parentId, ids) {
766
777
  const stopped = [];
767
- const walk = (pid) => {
778
+ // `inScope` is true once we are INSIDE a subtree we were asked to stop, and
779
+ // stays true all the way down.
780
+ //
781
+ // #142: the old walk passed `ids` down unchanged, so after matching an
782
+ // explicit id every DESCENDANT failed `ids.includes(id)` and was skipped —
783
+ // which is why the explicit branch hand-rolled its own one-level loop, and
784
+ // why that loop could not reach a great-grandchild.
785
+ const walk = (pid, inScope) => {
768
786
  for (const { id, agent } of this.childrenOf(pid)) {
769
- if (ids && !ids.includes(id)) {
770
- // still descend: stopping a parent implies stopping its subtree
771
- walk(id);
787
+ const take = inScope || !ids || ids.includes(id);
788
+ if (!take) {
789
+ // out of scope, but its own subtree may hold something in scope
790
+ walk(id, false);
772
791
  continue;
773
792
  }
774
793
  agent.stop("stopped by parent agent");
775
794
  stopped.push(id);
776
- if (ids) {
777
- // explicit id → also take its subtree
778
- const sub = this.childrenOf(id);
779
- for (const { id: gid, agent: g } of sub) {
780
- g.stop("stopped with parent");
781
- stopped.push(gid);
782
- }
783
- }
784
- else {
785
- walk(id); // full-subtree default
786
- }
795
+ // #142: RECURSE for both modes.
796
+ //
797
+ // The explicit-id branch used to iterate `childrenOf(id)` exactly once, so
798
+ // a great-grandchild was never reached — stopping a child left its
799
+ // grandchild's child running, indefinitely, still holding a model
800
+ // connection and billing tokens:
801
+ //
802
+ // root-sub stopped
803
+ // root-sub-sub stopped
804
+ // root-sub-sub-sub running <-- orphaned
805
+ //
806
+ // `walk` already means "this subtree", and it stops every node it visits,
807
+ // so one code path serves both cases. The only difference between the two
808
+ // modes is the `ids && !ids.includes(id)` test at the top, which decides
809
+ // whether a node is in scope — not how deep the walk goes.
810
+ walk(id, true);
787
811
  }
788
812
  };
789
- walk(parentId);
813
+ walk(parentId, false);
790
814
  return { stopped };
791
815
  }
792
816
  /** Deliver a parent's message into a child's mailbox (and wake it). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",