teapot-coding-agent 0.37.59 → 0.37.60

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 (42) hide show
  1. package/dist/acp/adapter.js +4 -72
  2. package/dist/agent/agent.js +52 -2246
  3. package/dist/agent/compact-rules.js +0 -53
  4. package/dist/agent/editorconfig.js +4 -56
  5. package/dist/agent/goal-audit.js +0 -34
  6. package/dist/agent/key-pool.js +0 -52
  7. package/dist/agent/kill.js +1 -13
  8. package/dist/agent/llm.js +8 -309
  9. package/dist/agent/mcp-oauth-manager.js +2 -255
  10. package/dist/agent/mcp-registry.js +1 -188
  11. package/dist/agent/mcp.js +1 -85
  12. package/dist/agent/meta-tools.js +0 -26
  13. package/dist/agent/plugins.js +1 -81
  14. package/dist/agent/provider-headers.js +1 -28
  15. package/dist/agent/skills.js +8 -94
  16. package/dist/agent/tools.js +16 -809
  17. package/dist/bus.js +1 -2
  18. package/dist/config-schema.js +0 -40
  19. package/dist/home.js +1 -19
  20. package/dist/index.js +0 -136
  21. package/dist/log/events.js +3 -195
  22. package/dist/master.js +31 -1176
  23. package/dist/migrations/index.js +0 -55
  24. package/dist/migrations/providers.js +0 -13
  25. package/dist/migrations/registry.js +0 -9
  26. package/dist/migrations/session-titles.js +3 -43
  27. package/dist/migrations/sessions.js +2 -20
  28. package/dist/model-meta.js +2 -74
  29. package/dist/scheduler/cron.js +2 -17
  30. package/dist/server/api.js +12 -778
  31. package/dist/server/image-resize.js +1 -64
  32. package/dist/server/term-size.js +1 -120
  33. package/dist/server/terminals.js +0 -159
  34. package/dist/session-title.js +2 -35
  35. package/package.json +1 -1
  36. package/public/assets/{index-KzHw4Sqo.js → index-DhJx7VDq.js} +4 -4
  37. package/public/assets/index-DhJx7VDq.js.map +1 -0
  38. package/public/assets/{shiki-BwDjoZdu.js → shiki-LBMkaWYb.js} +2 -2
  39. package/public/assets/shiki-LBMkaWYb.js.map +1 -0
  40. package/public/index.html +1 -1
  41. package/public/assets/index-KzHw4Sqo.js.map +0 -1
  42. package/public/assets/shiki-BwDjoZdu.js.map +0 -1
package/dist/bus.js CHANGED
@@ -1,4 +1,3 @@
1
1
  import { EventEmitter } from "node:events";
2
- /** Tiny process-wide pub/sub used to push updates to SSE clients (no polling). */
3
2
  export const bus = new EventEmitter();
4
- bus.setMaxListeners(1000); // one per connected client (WS + SSE) — headroom for busy LAN setups
3
+ bus.setMaxListeners(1000);
@@ -1,21 +1,7 @@
1
- /**
2
- * Zod schema for teapot's config — the single source of truth for what a
3
- * valid config looks like, used to gate web-UI edits (PUT /api/config) and any
4
- * programmatic writes with human-readable error messages.
5
- *
6
- * Validation gates; it is not the storage format — the master persists the raw
7
- * user JSON so unknown fields survive round-trips.
8
- */
9
1
  import { z } from "zod";
10
2
  export const ProviderSchema = z.object({
11
3
  baseUrl: z.union([z.string().url("baseUrl must be a valid URL"), z.literal("")]).optional(),
12
4
  apiKey: z.string().optional(),
13
- /**
14
- * Several credentials for ONE provider, rotated round-robin (#170). Blank entries
15
- * are rejected rather than silently dropped: the operator wrote them, and a key
16
- * that vanishes without a word is how "I added 3 keys but it still rate-limits"
17
- * happens.
18
- */
19
5
  apiKeys: z
20
6
  .array(z.string().min(1, "an api key entry must not be empty"))
21
7
  .max(50, "at most 50 keys per provider")
@@ -41,45 +27,20 @@ export const TaskSchema = z.object({
41
27
  prompt: z.string().min(1, "prompt required"),
42
28
  forked: z.boolean().optional(),
43
29
  });
44
- /** the patch shape accepted by PUT /api/config */
45
30
  export const ConfigPatchSchema = z.object({
46
31
  providers: z.record(z.string().min(1, "provider name required"), ProviderSchema).optional(),
47
32
  defaultProvider: z.string().optional(),
48
33
  progressIntervalMs: z.number().int().min(10_000, "progress interval must be ≥ 10s").optional(),
49
34
  progressMinChars: z.number().int().min(100).optional(),
50
- // null clears a previously pinned compact budget → back to per-model derivation
51
35
  contextTokenBudget: z.number().int().min(1_000).nullable().optional(),
52
- // null clears a saved window pin → back to per-model inference (same contract as contextTokenBudget)
53
36
  contextWindowTokens: z.number().int().min(1_000).nullable().optional(),
54
37
  maxSpawnDepth: z.number().int().min(0).max(8).optional(),
55
- /**
56
- * The shared secret protecting `/api/*` — settable from the settings panel.
57
- *
58
- * It used to be reachable only through `POST /api/setup`, which refuses once a config file exists.
59
- * That was fine while the first-run wizard existed; the wizard is gone, so there was NO way to set
60
- * a password at all. Asked as "初回起動ウィザードは前に廃止したはずだからパスワード設定できないの
61
- * では?" — correct.
62
- *
63
- * An EMPTY STRING is meaningful and clears it (back to loopback-only trust), which is why this
64
- * accepts `""` rather than requiring a minimum length. `TEAPOT_API_TOKEN` still wins over it.
65
- */
66
38
  password: z.string().max(200).optional(),
67
- /** #161: prune old tool output early (opt-in, for small-window models) */
68
39
  shortContext: z.boolean().optional(),
69
- // soft cap on LLM turns in one round; reaching it nudges the model to wrap
70
- // up (not an error) — 0 disables the cap
71
40
  maxTurnsPerRound: z.number().int().min(0).optional(),
72
- // round-fatal API errors: "stop" (default) or "retry" (back off + fresh
73
- // round — for unattended agents that should ride out outages)
74
41
  onError: z.enum(["stop", "retry"]).optional(),
75
42
  retryDelayMs: z.number().int().min(1_000).optional(),
76
43
  tasks: z.array(TaskSchema).optional(),
77
- /**
78
- * MCP servers (#67). Validated here so a bad entry fails the SAVE with a readable
79
- * message instead of surfacing later as a tool that never appears: a stdio server
80
- * needs a command, a network one needs a url, and the name is what namespaces every
81
- * tool it exposes.
82
- */
83
44
  mcpServers: z
84
45
  .array(z
85
46
  .object({
@@ -101,7 +62,6 @@ export const ConfigPatchSchema = z.object({
101
62
  }))
102
63
  .optional(),
103
64
  });
104
- /** Format a ZodError into a compact operator-readable message. */
105
65
  export function formatZodError(error) {
106
66
  return error.issues
107
67
  .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
package/dist/home.js CHANGED
@@ -1,24 +1,8 @@
1
- /**
2
- * The user's home directory, on every platform (#110).
3
- *
4
- * `process.env.HOME` is a POSIX convention and is normally UNSET on Windows, so
5
- * expanding `~` with `process.env.HOME ?? "~"` produced the LITERAL "~" there —
6
- * and since the result reached `path.resolve` + `mkdirSync`, teapot created a
7
- * directory named `~`.
8
- *
9
- * The `env` parameters exist so a test can drive the Windows shape
10
- * deterministically; `os.homedir()` on a Linux CI box would report `/root` and
11
- * hide the condition under test.
12
- */
13
- /** @returns "" when no home is known — callers leave `~` alone rather than invent a path */
14
1
  export function homeDir(env = process.env) {
15
2
  if (env.HOME)
16
3
  return env.HOME;
17
4
  if (env.USERPROFILE)
18
5
  return env.USERPROFILE;
19
- // Windows can report HOMEDRIVE + HOMEPATH. Both must be present:
20
- // `undefined + path` yields the STRING "undefined", which is exactly the class
21
- // of bug this helper exists to remove.
22
6
  const drive = env.HOMEDRIVE;
23
7
  const rest = env.HOMEPATH;
24
8
  if (drive && rest)
@@ -27,13 +11,11 @@ export function homeDir(env = process.env) {
27
11
  return drive;
28
12
  return "";
29
13
  }
30
- /** Expand a leading `~`. One place, so the Windows case cannot be got wrong twice (#110). */
31
14
  export function expandTilde(input, env = process.env) {
32
- // a Windows path can be `~\foo`, hence the backslash in the lookahead
33
15
  if (!/^~(?=$|[/\\])/.test(input))
34
16
  return input;
35
17
  const home = homeDir(env);
36
18
  if (!home)
37
- return input; // no home known: leaving `~` is more honest than inventing one
19
+ return input;
38
20
  return input.replace(/^~(?=$|[/\\])/, home);
39
21
  }
package/dist/index.js CHANGED
@@ -1,52 +1,20 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * teapot master entry point.
4
- * Usage: teapot [--port N] [--host addr] [--config file.json] [config.json]
5
- * Config may also come from TEAPOT_* env vars (see src/master.ts).
6
- * First run (no config file): boot anyway and finish setup in the web UI.
7
- */
8
2
  import { existsSync, realpathSync } from "node:fs";
9
3
  import { fileURLToPath } from "node:url";
10
4
  import { loadConfig, resolveConfigPath, Master } from "./master.js";
11
5
  import { buildApp, serveApp } from "./server/api.js";
12
- /**
13
- * Parse `teapot [--port N] [-p N] [--host addr] [--config file] [config.json]`.
14
- *
15
- * Exported so it can be tested: the parser decides which config file is read and
16
- * which port is bound, so a regression here starts the wrong server on the wrong
17
- * port with no error anyone would read.
18
- *
19
- * The bare-word rule keeps the FIRST positional as the config path: a later bare
20
- * word is a stray argument, not an override, and silently replacing the config
21
- * with it would be worse than ignoring it.
22
- *
23
- * BOTH spellings are accepted: `--host 0.0.0.0` and `--host=0.0.0.0`. Only the
24
- * space-separated form used to parse, so `--host=0.0.0.0` — the spelling people
25
- * reach for, and the one every other CLI accepts — was silently DISCARDED and the
26
- * server came up on loopback with nothing readable to say why. Reported as
27
- * "--host が効いてない、0.0.0.0 にしても他デバイスからアクセスできない", and a
28
- * silently-ignored flag is the worst possible shape for this: the operator sees a
29
- * server that started fine and blames their network.
30
- */
31
6
  export function parseArgs(argv) {
32
7
  let cfgArg;
33
8
  let portOverride;
34
9
  let hostOverride;
35
10
  for (let i = 0; i < argv.length; i++) {
36
11
  const a = argv[i];
37
- // `--name=value` and `--name value` both work. Splitting here means every
38
- // branch below sees the plain name and reads its value the same way, so a
39
- // future flag cannot repeat the `--host=` omission.
40
12
  const eq = a.startsWith("--") ? a.indexOf("=") : -1;
41
13
  const name = eq === -1 ? a : a.slice(0, eq);
42
- // for `--name=value` the value is already in `a`; otherwise take the next word
43
14
  const value = () => (eq === -1 ? argv[++i] : a.slice(eq + 1));
44
15
  if (name === "--port" || name === "-p") {
45
16
  const raw = value();
46
17
  const n = Number(raw);
47
- // NaN is dropped rather than applied: `config.port = NaN` would fall through
48
- // to the listen call and produce a far more confusing failure than ignoring
49
- // a mistyped flag
50
18
  if (!Number.isNaN(n))
51
19
  portOverride = n;
52
20
  }
@@ -79,15 +47,6 @@ async function main() {
79
47
  if (hostOverride)
80
48
  config.host = hostOverride;
81
49
  console.log(`[teapot] config: ${configPath}${configExisted ? "" : " (not found — first run)"}`);
82
- /**
83
- * NO PORT HANDOVER DANCE.
84
- *
85
- * This used to read `TEAPOT_RESTART_FROM`, SIGTERM the predecessor and poll until the port was
86
- * free, because the successor was a SEPARATE process racing the old one for one socket. The live
87
- * update now replaces the process in place with `process.execve`, so the pid never changes: the
88
- * exec releases the listener as part of replacing the image (verified) and this process is the
89
- * only one that ever binds. The whole EADDRINUSE window #35 worked around cannot occur.
90
- */
91
50
  const hasProviders = Object.keys(config.providers ?? {}).length > 0;
92
51
  if (!config.llm.apiKey && !hasProviders)
93
52
  console.warn("[teapot] warning: no API key configured — finish setup in the web UI");
@@ -96,64 +55,24 @@ async function main() {
96
55
  const master = new Master(config, configPath);
97
56
  master.configFileExists = configExisted;
98
57
  await master.start();
99
- /**
100
- * LIVE UPDATE: resume what the previous image was running.
101
- *
102
- * The set was written to the config by `restartServer()` immediately before the exec, because
103
- * after a restart the log cannot tell "was working when the update hit" from "finished" or
104
- * "stopped by the operator" — resuming every configured agent would start work nobody asked for,
105
- * and resuming none is the bug being fixed ("サーバーを再起動して動作中だったセッションのエージェント
106
- * ループを再開").
107
- *
108
- * Deliberately AFTER `start()` so the agents exist, and BEFORE the port is served so the operator
109
- * reloading the page sees the resumed sessions already moving.
110
- */
111
58
  if (process.env.TEAPOT_RESUMED) {
112
59
  delete process.env.TEAPOT_RESUMED;
113
60
  const resumed = await master.resumeAfterUpdate();
114
61
  if (resumed.length)
115
62
  console.log(`[teapot] live update: ${resumed.length} session(s) resumed`);
116
63
  }
117
- // ACP mode (#15): JSON-RPC over stdio, no HTTP server. Deliberately placed
118
- // AFTER the master is up (so config is resolved and agents work) but BEFORE
119
- // serveApp, so an editor launching `teapot --acp` never binds a port.
120
64
  if (process.argv.includes("--acp")) {
121
65
  const { AcpAdapter } = await import("./acp/adapter.js");
122
66
  const acp = new AcpAdapter({ master });
123
- // nothing else may write to stdout — it carries the protocol
124
67
  console.error("[teapot] ACP mode: JSON-RPC over stdio");
125
68
  await acp.listen();
126
69
  return;
127
70
  }
128
71
  const app = buildApp(master);
129
- // kept for the process lifetime; nothing closes it explicitly any more (the supervisor
130
- // branch that did is gone — `process.execve` replaces the image and releases the socket)
131
72
  serveApp(app, config.port, config.host);
132
- /**
133
- * Say out loud when the API has no auth, and when it is reachable beyond loopback.
134
- *
135
- * The OpenCode advisory (CVE-2026-22812) was not just "a server was open" — it was open and
136
- * nobody knew. `apiToken()` is empty on a fresh install because the setup wizard sets the
137
- * password THROUGH the API, so a token cannot be demanded before one exists; the honest response
138
- * is to make the state visible on every start rather than to pretend it is protected.
139
- *
140
- * See docs/security-advisories.md.
141
- */
142
73
  const noAuth = !(process.env.TEAPOT_API_TOKEN || master.config.password);
143
74
  const beyondLoopback = !!config.host && config.host !== "127.0.0.1" && config.host !== "localhost";
144
75
  if (noAuth) {
145
- /**
146
- * On LOOPBACK this is the normal, safe default — so say so plainly instead of printing an
147
- * alarming "no API token set" on every start.
148
- *
149
- * Asked as "no API token setはなんだろう": nothing is wrong. On loopback only this machine can
150
- * connect, so an unset password is the safe default rather than a fault — and it is settable in
151
- * settings → live update (it uses to be reachable only through the first-run wizard, which was
152
- * removed). The message now says that, on stdout at info level; `console.error` made a healthy
153
- * start look like a fault.
154
- *
155
- * The WARNING branch is unchanged and still goes to stderr: that one IS a problem.
156
- */
157
76
  if (!beyondLoopback) {
158
77
  console.log("[teapot] auth: none needed — bound to loopback only, so only this machine can reach /api. " +
159
78
  "Set TEAPOT_API_TOKEN or config.password before exposing this port.");
@@ -163,8 +82,6 @@ async function main() {
163
82
  "drive the agents. Set TEAPOT_API_TOKEN or config.password now.");
164
83
  }
165
84
  }
166
- // agent crash isolation: an agent error never escapes its own loop; here we
167
- // also make sure the process survives unexpected rejections.
168
85
  process.on("uncaughtException", (err) => {
169
86
  console.error("[teapot] uncaught exception (master survived):", err);
170
87
  });
@@ -176,56 +93,15 @@ async function main() {
176
93
  if (shuttingDown)
177
94
  return;
178
95
  shuttingDown = true;
179
- /**
180
- * THERE IS NO SUPERVISOR MODE ANY MORE — and removing it IS the memory-leak fix.
181
- *
182
- * The old live update spawned a successor and stayed resident as its supervisor, polling
183
- * `pidAlive(replacement)` every 500ms for the ENTIRE lifetime of the new server. Everything
184
- * this process owned stayed alive with it: every agent object and its log handles, every `bus`
185
- * listener, the 15s master tick, plus three MORE signal handlers that `supervise()` added on
186
- * top of the ones `shutdown()` had already registered. Each update therefore left another full
187
- * copy of the server resident, which is the reported leak.
188
- *
189
- * `restartServer()` now replaces the image in place with `process.execve`, so there is no
190
- * successor to supervise: the exec reuses this pid, this process group and this terminal, so
191
- * nothing is orphaned and nothing lingers. `execve` only resolves if it FAILED, and that path
192
- * throws inside `restartServer`, so nothing is left trusting this branch.
193
- */
194
- /**
195
- * Ctrl-C during a live update must not cancel the handover half-way.
196
- *
197
- * `restartServer()` records what to resume and then execs. A SIGINT in that window ran this
198
- * shutdown path, which DISPOSES every agent and exits — so the operator saw
199
- *
200
- * [teapot] live update: waiting for 1 agent(s)…
201
- * ^C
202
- * [teapot] shutting down (SIGINT)...
203
- * [teapot] live update: 0 session(s) will resume
204
- *
205
- * and the work that was running never came back. The flag is set by `restartServer()` for the
206
- * whole duration of the handover, which is exactly the window where the operator's ^C is
207
- * ambiguous: it can mean "cancel the update" or "stop the server", and treating it as the latter
208
- * silently discards the resume set that was just written.
209
- *
210
- * A second ^C while an update is in flight is the escape hatch — that is why this checks the
211
- * signal rather than ignoring it outright.
212
- */
213
96
  const updating = master.updateInFlight === true;
214
97
  if (updating && signal === "SIGINT" && !process.env.TEAPOT_FORCE_SHUTDOWN) {
215
98
  console.log("\n[teapot] live update is in progress — press ^C again to abort it and stop");
216
99
  process.env.TEAPOT_FORCE_SHUTDOWN = "1";
217
- // do NOT reset `shuttingDown`: the next signal must get through, so clear it
218
100
  shuttingDown = false;
219
101
  return;
220
102
  }
221
103
  console.log(`\n[teapot] shutting down${signal ? ` (${signal})` : ""}...`);
222
104
  await Promise.allSettled([...master.agents.values()].map((a) => a.dispose()));
223
- // During a live update the replacement SIGTERMs us to take the port. Linger
224
- // so our own in-flight HTTP responses flush first — notably
225
- // /api/update/restart, whose answer the browser is still waiting on; the
226
- // socket is released the moment we exit, which is what lets the replacement
227
- // bind. We must NOT wait for the replacement to come up: it is itself
228
- // waiting for this port to free, so that would deadlock the handover (#35).
229
105
  if (process.env.TEAPOT_RESTART_FROM)
230
106
  await new Promise((r) => setTimeout(r, 1_500));
231
107
  process.exit(0);
@@ -233,27 +109,15 @@ async function main() {
233
109
  process.on("SIGINT", () => void shutdown("SIGINT"));
234
110
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
235
111
  }
236
- // Only when RUN, not when imported: `void main()` at module scope meant importing
237
- // this file for `parseArgs` started a whole server. `node
238
- // --experimental-strip-types src/index.ts` and `node dist/index.js` both take
239
- // this branch.
240
112
  function isEntryPoint() {
241
113
  const arg = process.argv[1];
242
114
  if (!arg)
243
115
  return false;
244
- // `npm i -g teapot-coding-agent` puts a SYMLINK at node_modules/.bin/teapot and
245
- // npm invokes the target THROUGH it, so `process.argv[1]` is the symlink path,
246
- // NOT the file it points at: the installed command then exited 0 in silence.
247
- // `realpathSync` is what resolves it.
248
116
  try {
249
117
  const self = realpathSync(fileURLToPath(import.meta.url));
250
118
  const entry = realpathSync(arg);
251
119
  if (self === entry)
252
120
  return true;
253
- // `self === entry` is the ONLY correct test: it is the definition of being the
254
- // entry point. A suffix check such as `endsWith("/index.ts")` is true for
255
- // EVERY test file that imports this module, so importing it booted a real
256
- // server on port 7788 and `cli-args.test.ts` failed with EADDRINUSE.
257
121
  return false;
258
122
  }
259
123
  catch {