teapot-coding-agent 0.37.59 → 0.37.61

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 +61 -2247
  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 +9 -310
  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 -1180
  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 +166 -142
  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-C1eqcSbW.js} +4 -4
  37. package/public/assets/index-C1eqcSbW.js.map +1 -0
  38. package/public/assets/{shiki-BwDjoZdu.js → shiki-DaLn_bCe.js} +2 -2
  39. package/public/assets/shiki-DaLn_bCe.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
@@ -1,37 +1,5 @@
1
- /**
2
- * Keep the operator's hard rules alive across a compaction.
3
- *
4
- * ## Why this is not just another summary section
5
- *
6
- * A compaction summary is generated text: everything in it was re-derived by a model from the
7
- * conversation it was shown. That is fine for facts, because facts can be re-checked against the
8
- * code. A RULE cannot — "never install this into Wine", "do not clone repositories to study them"
9
- * were each stated once, obeyed silently for dozens of turns, and are not repeated anywhere the
10
- * next pass could find them. When the summary drops one, nothing downstream notices; the agent
11
- * just starts doing it.
12
- *
13
- * So the rules get a section with a SHAPE the code can check (`MUST` / `MUST NOT` / `SHOULD` at the
14
- * start of each line), extracted on the way out and RE-INSERTED on the way in if a pass lost them.
15
- * The summarizer is still the thing that decides what the rules ARE; this module only makes sure
16
- * that once known they cannot disappear.
17
- *
18
- * ## Why the shape matters more than the wording
19
- *
20
- * A free-form section lets a model merge three rules into one sentence, which is how a `MUST NOT`
21
- * turns into a `SHOULD avoid` and then into nothing. A fixed keyword per line keeps each rule a
22
- * separate, checkable item. `should` is included deliberately — the operator asked for it — but it
23
- * is ranked below the musts so a squeeze drops the weakest first.
24
- */
25
1
  export const RULES_HEADING = "## Hard constraints";
26
- /** the levels, strongest first — the order a squeeze must respect */
27
2
  const LEVELS = ["MUST NOT", "MUST", "SHOULD"];
28
- /**
29
- * Parse the `## Hard constraints` section out of a summary.
30
- *
31
- * Tolerant on purpose: the summarizer is asked for a shape, not guaranteed to produce it. A line
32
- * that does not start with a keyword is ignored rather than guessed at, because inventing a rule
33
- * from prose is worse than losing one — a wrong rule is obeyed.
34
- */
35
3
  export function extractRules(summary) {
36
4
  const lines = summary.split("\n");
37
5
  const start = lines.findIndex((l) => l.trim().toLowerCase() === RULES_HEADING.toLowerCase());
@@ -41,13 +9,11 @@ export function extractRules(summary) {
41
9
  const seen = new Set();
42
10
  for (let i = start + 1; i < lines.length; i++) {
43
11
  const raw = lines[i];
44
- // a new `##` heading ends the section
45
12
  if (/^#{1,6}\s/.test(raw.trim()))
46
13
  break;
47
14
  const line = raw.replace(/^\s*[-*+]\s*/, "").trim();
48
15
  if (!line)
49
16
  continue;
50
- // longest keyword first, or `MUST NOT` parses as `MUST` + "NOT …"
51
17
  const m = /^(MUST NOT|MUST|SHOULD)\b[:\s]*(.*)$/i.exec(line);
52
18
  if (!m)
53
19
  continue;
@@ -55,7 +21,6 @@ export function extractRules(summary) {
55
21
  const text = m[2].trim();
56
22
  if (!text)
57
23
  continue;
58
- // de-duplicate on the normalised text: two paraphrases of one rule is one rule
59
24
  const key = text.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/\s+/g, " ").trim();
60
25
  if (seen.has(key))
61
26
  continue;
@@ -64,7 +29,6 @@ export function extractRules(summary) {
64
29
  }
65
30
  return out;
66
31
  }
67
- /** the section body for a set of rules, in strongest-first order */
68
32
  export function renderRules(rules) {
69
33
  const rank = (r) => LEVELS.indexOf(r.level);
70
34
  return [...rules]
@@ -72,16 +36,6 @@ export function renderRules(rules) {
72
36
  .map((r) => `- ${r.level} ${r.text}`)
73
37
  .join("\n");
74
38
  }
75
- /**
76
- * Re-insert rules a compaction pass dropped.
77
- *
78
- * `before` is what was known going in, `after` is the section the new summary produced. Anything in
79
- * `before` whose text is not present in `after` is appended (strongest first) so a pass can add
80
- * rules and reword them, but can never lose one. Returns the summary with the section rewritten.
81
- *
82
- * Rewriting rather than appending is what makes this idempotent: running it twice on the same
83
- * summary produces the same summary, which matters because the update path compacts repeatedly.
84
- */
85
39
  export function enforceRules(summary, before) {
86
40
  const after = extractRules(summary);
87
41
  const present = new Set(after.map((r) => r.text.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/\s+/g, " ").trim()));
@@ -95,7 +49,6 @@ export function enforceRules(summary, before) {
95
49
  const lines = summary.split("\n");
96
50
  const start = lines.findIndex((l) => l.trim().toLowerCase() === RULES_HEADING.toLowerCase());
97
51
  if (start < 0) {
98
- // no section at all — append one rather than silently keeping nothing
99
52
  return { summary: `${summary.trimEnd()}\n\n${section}`, restored };
100
53
  }
101
54
  let end = lines.length;
@@ -110,12 +63,6 @@ export function enforceRules(summary, before) {
110
63
  restored,
111
64
  };
112
65
  }
113
- /**
114
- * The rules as a line the summarizer is asked to keep, for the UPDATE path.
115
- *
116
- * Passed as part of the prior checkpoint so a pass that is told "preserve what is still true" can
117
- * see the rules explicitly instead of having to find them in prose.
118
- */
119
66
  export function rulesForPrompt(rules) {
120
67
  if (!rules.length)
121
68
  return "";
@@ -1,52 +1,16 @@
1
- /**
2
- * `.editorconfig` support (#109) — "edit_file の改行コードを .editorconfig に合わせて
3
- * 正規化するとか?".
4
- *
5
- * ## What this DOES and deliberately does NOT do
6
- *
7
- * An EXISTING file already keeps its own line ending (#84, `detectEol`): the file's
8
- * own bytes are the strongest possible evidence, and a `.editorconfig` that
9
- * disagrees with the file on disk is nearly always stale relative to it. So this
10
- * only supplies an answer where there was NONE:
11
- *
12
- * - a NEW file (nothing to detect), and
13
- * - a MIXED-EOL file, where `detectEol` returns null on purpose because choosing
14
- * a winner would be a silent whole-file rewrite.
15
- *
16
- * That keeps one rule: **the file's own content outranks configuration, which
17
- * outranks the LF default.** `end_of_line = crlf` in a repo whose files are LF does
18
- * not silently convert every file the agent touches.
19
- *
20
- * ## Why this is not a general editorconfig library
21
- *
22
- * The format has glob sections, brace expansion and a precedence order. Only the
23
- * facts that change how a WRITE behaves are read — today that is `end_of_line`
24
- * (plus `insert_final_newline`, which decides whether a new file ends in a
25
- * newline). Anything else (`indent_style`, `charset`, …) has no in-repo consumer,
26
- * and guessing at it would be untested surface.
27
- */
28
1
  import { readFileSync, existsSync } from "node:fs";
29
2
  import path from "node:path";
30
3
  const NONE = { eol: null };
31
- /**
32
- * Read the `.editorconfig` files that apply to `targetPath`, nearest-wins.
33
- *
34
- * Walks from the file's own directory up to the filesystem root so a nested
35
- * `.editorconfig` overrides the repo root, which is the documented precedence.
36
- * Sections are matched against the path RELATIVE to the file that declared them.
37
- * A `root = true` stops the walk, as the format requires.
38
- */
39
4
  export function editorConfigFor(targetPath) {
40
5
  const dirs = [];
41
6
  let dir = path.dirname(path.resolve(targetPath));
42
7
  for (;;) {
43
- dirs.push(dir); // nearest first, which is also the precedence order
8
+ dirs.push(dir);
44
9
  const parent = path.dirname(dir);
45
10
  if (parent === dir)
46
11
  break;
47
12
  dir = parent;
48
13
  }
49
- // nearest-first, so the FIRST answer found wins; `root = true` truncates the walk
50
14
  const applicable = [];
51
15
  for (const d of dirs) {
52
16
  const file = path.join(d, ".editorconfig");
@@ -61,9 +25,8 @@ export function editorConfigFor(targetPath) {
61
25
  }
62
26
  applicable.push({ dir: d, text });
63
27
  if (/^\s*root\s*=\s*true\s*$/im.test(text))
64
- break; // this file is the root
28
+ break;
65
29
  }
66
- // apply outermost -> innermost so the nearest file's value lands last
67
30
  let out = NONE;
68
31
  for (const { dir: declDir, text } of applicable.reverse()) {
69
32
  const rel = path.relative(declDir, path.resolve(targetPath)).split(path.sep).join("/");
@@ -86,7 +49,6 @@ export function editorConfigFor(targetPath) {
86
49
  }
87
50
  return out;
88
51
  }
89
- /** `[section]` blocks with their `key = value` pairs. Comments and blanks ignored. */
90
52
  function parseSections(text) {
91
53
  const sections = [];
92
54
  let current = null;
@@ -101,26 +63,17 @@ function parseSections(text) {
101
63
  continue;
102
64
  }
103
65
  if (!current)
104
- continue; // a property outside any section applies to nothing
66
+ continue;
105
67
  const eq = line.indexOf("=");
106
68
  if (eq === -1)
107
69
  continue;
108
70
  const key = line.slice(0, eq).trim().toLowerCase();
109
71
  const value = line.slice(eq + 1).trim().toLowerCase();
110
- // the FIRST value wins within a section (the format's rule)
111
72
  if (key && value && !current.props.has(key))
112
73
  current.props.set(key, value);
113
74
  }
114
75
  return sections;
115
76
  }
116
- /**
117
- * Does an editorconfig glob match a POSIX-style relative path?
118
- *
119
- * Supports the parts that appear in real configs: `*` (within a segment), `**`
120
- * (across segments), `?`, `{a,b}` alternation, and a `[abc]` class. A section with
121
- * no `/` matches the BASENAME at any depth, which is the format's documented special
122
- * case (and why `[*.md]` in a root file applies to `docs/x.md`).
123
- */
124
77
  export function sectionMatches(pattern, relPath) {
125
78
  const anchored = pattern.includes("/");
126
79
  const names = anchored ? [relPath] : [relPath.split("/").pop() ?? ""];
@@ -128,14 +81,12 @@ export function sectionMatches(pattern, relPath) {
128
81
  const re = globToRegExp(body, anchored);
129
82
  return names.some((n) => re.test(n));
130
83
  }
131
- /** Translate an editorconfig glob into a RegExp. */
132
84
  function globToRegExp(glob, anchored) {
133
85
  let out = "";
134
86
  for (let i = 0; i < glob.length; i++) {
135
87
  const c = glob[i];
136
88
  if (c === "*") {
137
89
  if (glob[i + 1] === "*") {
138
- // `**/` matches zero or more leading segments; a bare `**` is `.*`
139
90
  if (glob[i + 2] === "/") {
140
91
  out += "(?:.*/)?";
141
92
  i += 2;
@@ -146,7 +97,7 @@ function globToRegExp(glob, anchored) {
146
97
  }
147
98
  }
148
99
  else {
149
- out += "[^/]*"; // a single star never crosses a separator
100
+ out += "[^/]*";
150
101
  }
151
102
  continue;
152
103
  }
@@ -169,7 +120,6 @@ function globToRegExp(glob, anchored) {
169
120
  if (c === "[") {
170
121
  const close = glob.indexOf("]", i);
171
122
  if (close !== -1) {
172
- // a character class is passed through as-is, minus any escaping surprises
173
123
  out += glob.slice(i, close + 1).replace(/[\\^]/g, "\\$&");
174
124
  i = close;
175
125
  continue;
@@ -177,7 +127,5 @@ function globToRegExp(glob, anchored) {
177
127
  }
178
128
  out += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
179
129
  }
180
- // anchored patterns are relative to the config's directory; a basename pattern
181
- // already matched against the basename, so neither needs a prefix
182
130
  return new RegExp(`^${out}$`, anchored ? "" : "");
183
131
  }
@@ -1,26 +1,4 @@
1
- /**
2
- * Completion-audit verdict parsing and the auditor prompt.
3
- *
4
- * The auditor is an INDEPENDENT reviewer: a separate LLM call that decides whether the worker's
5
- * `finish(goalComplete=true)` is honest against the goal's verification contract. Its answer must
6
- * be unambiguous, because the two readings are not symmetric — a missed approval costs one more
7
- * round, while a misread rejection sends the worker to fix gaps that nobody named.
8
- */
9
- /** provider fallbacks for a message with no usable text */
10
1
  const PLACEHOLDER = /^\((tool call|no content|no output)\)$/i;
11
- /**
12
- * Read the auditor's verdict.
13
- *
14
- * The verdict must be the LAST non-empty line, and it must carry a reason. Upstream pi-goal-x made
15
- * this exact correction: matching the marker ANYWHERE lets a prose mention of it be read as the
16
- * verdict — "I would only approve if the tests passed" is a REJECTION that contains the word.
17
- *
18
- * MEASURED against the real failure: an auditor that answered `Independent verification against …`
19
- * (prose, no marker) was scored as a verdict three times in a row, hit the unavailable cap, and the
20
- * goal was abandoned with the audit never actually running. Requiring the marker on the last line
21
- * turns that into an honest "no verdict", which the caller already handles by retrying or failing
22
- * closed with a loud note.
23
- */
24
2
  export function parseAuditVerdict(reply) {
25
3
  const text = String(reply ?? "").trim();
26
4
  if (!text || PLACEHOLDER.test(text))
@@ -29,7 +7,6 @@ export function parseAuditVerdict(reply) {
29
7
  .split("\n")
30
8
  .map((l) => l.trim())
31
9
  .filter(Boolean);
32
- // the verdict line is the LAST one; a reason may wrap onto earlier lines
33
10
  const last = lines[lines.length - 1] ?? "";
34
11
  const approved = /^APPROVED\b(:|\s-|$)(.*)$/i.exec(last);
35
12
  if (approved) {
@@ -39,19 +16,8 @@ export function parseAuditVerdict(reply) {
39
16
  if (rejected) {
40
17
  return { kind: "changes-required", reason: (rejected[2] ?? "").trim() || "(no gaps named)" };
41
18
  }
42
- /**
43
- * No marker on the last line. The reason still matters — it is shown to the worker when this
44
- * happens repeatedly — so keep the tail rather than discarding the reply.
45
- */
46
19
  return { kind: "none", detail: `auditor reply had no verdict on its last line: ${last.slice(0, 200)}` };
47
20
  }
48
- /**
49
- * The auditor's instructions.
50
- *
51
- * Deliberately NOT the worker's conversation: this call is a review, and handing the auditor a
52
- * 200k-token work log makes it continue the work instead of judging it. MEASURED: three audits in
53
- * one session replied with prose that started re-verifying the goal rather than naming a verdict.
54
- */
55
21
  export function auditPrompt(input) {
56
22
  return [
57
23
  "You are an independent completion AUDITOR, not the worker. Decide whether the goal is genuinely complete.",
@@ -1,34 +1,3 @@
1
- /**
2
- * Multiple API keys per provider (#170) — "複数鍵登録しといて、規定のリトライ回数を超えたら
3
- * 次の鍵にローテ / 全キーを回ってもまだレートリミットだったらエラーで停止".
4
- *
5
- * ## The algorithm, deliberately small
6
- *
7
- * A request is made with the current key. The HTTP client already retries a rate limit
8
- * a fixed number of times on that key (its own backoff ladder). When those retries are
9
- * spent and the failure is STILL a rate limit, the pool moves to the next key and the
10
- * whole ladder runs again on it. When the last key is exhausted too, the error is
11
- * raised and the turn stops.
12
- *
13
- * That is the entire policy:
14
- *
15
- * key1 [retry × N] -> key2 [retry × N] -> … -> raise
16
- *
17
- * ## What this intentionally does NOT do
18
- *
19
- * An earlier version of this file tracked per-key "recovered at" timestamps, parsed
20
- * provider reset hints, classified a 429 as transient-vs-exhausted, and persisted spent
21
- * accounts to disk. None of that was asked for, none of it was generic (the hint
22
- * formats are provider-specific), and the operator's actual ask is the loop above. A
23
- * simpler rule is also easier to reason about when it goes wrong: with this policy the
24
- * only state is "which key am I on", and the only outcome is success or a raised error.
25
- *
26
- * ## Provider-agnostic
27
- *
28
- * Nothing here knows about any particular endpoint. "Rate limited" is the HTTP 429 that
29
- * every OpenAI-compatible provider uses; the caller decides which statuses qualify.
30
- */
31
- /** Build a pool from config order, dropping blanks and duplicates. */
32
1
  export function makeKeyPool(keys) {
33
2
  const seen = new Set();
34
3
  const unique = [];
@@ -41,22 +10,11 @@ export function makeKeyPool(keys) {
41
10
  }
42
11
  return { keys: unique, current: 0 };
43
12
  }
44
- /** The key in use, or null for an empty pool (or no pool at all). */
45
13
  export function currentKey(pool) {
46
- // tolerate a missing pool: most callers simply have no `keyPool`, and making every
47
- // one of them guard first is how a `undefined.keys` crash reaches a 400 (measured)
48
14
  if (!pool)
49
15
  return null;
50
16
  return pool.keys[pool.current] ?? null;
51
17
  }
52
- /**
53
- * Move to the next key, or null when every key has already had its turn.
54
- *
55
- * Null is the "全キーを回ってもまだレートリミットだった" signal: the caller stops and raises
56
- * the last error instead of cycling forever. No wraparound on purpose — wrapping would
57
- * re-try keys whose retry ladder was just spent, turning a hard limit into an infinite
58
- * loop.
59
- */
60
18
  export function nextKey(pool) {
61
19
  const next = pool.current + 1;
62
20
  if (next >= pool.keys.length)
@@ -64,22 +22,12 @@ export function nextKey(pool) {
64
22
  pool.current = next;
65
23
  return pool.keys[next];
66
24
  }
67
- /** Start over from the first key (a fresh turn gets the full cycle again). */
68
25
  export function resetPool(pool) {
69
26
  pool.current = 0;
70
27
  }
71
- /** How many keys this pool holds. */
72
28
  export function poolSize(pool) {
73
29
  return pool.keys.length;
74
30
  }
75
- /**
76
- * Is this failure a rate limit — the only kind rotating can fix?
77
- *
78
- * 429 is the universal spelling. 402 ("payment required") and 403 are included because
79
- * an exhausted subscription and a quota-blocked key answer that way, and a second
80
- * account is exactly what resolves them. Everything else (5xx, 400, a network error) is
81
- * NOT rotated: it would fail identically on every key, so rotating just burns the cycle.
82
- */
83
31
  export function isRateLimitStatus(status) {
84
32
  return status === 429 || status === 402 || status === 403;
85
33
  }
@@ -1,19 +1,10 @@
1
- /** can this platform kill a whole process group? */
2
1
  export function canKillProcessGroup(platform = process.platform) {
3
2
  return platform !== "win32";
4
3
  }
5
- /**
6
- * Kill a spawned child, and its whole group where the platform allows it.
7
- *
8
- * Deliberately does NOT throw: callers are timeouts and abort handlers, where a
9
- * throw would replace the real message with a secondary failure.
10
- */
11
4
  export function killTree(child, platform = process.platform) {
12
5
  if (!child?.pid)
13
6
  return { killed: false, wholeTree: false, reason: "no-pid" };
14
7
  if (!canKillProcessGroup(platform)) {
15
- // Windows: no group kill. Try the direct child so at least THAT stops, and
16
- // report that the tree is not covered rather than implying it is.
17
8
  try {
18
9
  process.kill(child.pid, "SIGKILL");
19
10
  return { killed: true, wholeTree: false };
@@ -28,13 +19,11 @@ export function killTree(child, platform = process.platform) {
28
19
  }
29
20
  }
30
21
  try {
31
- process.kill(-child.pid, "SIGKILL"); // whole group
22
+ process.kill(-child.pid, "SIGKILL");
32
23
  return { killed: true, wholeTree: true };
33
24
  }
34
25
  catch (err) {
35
26
  const code = err.code;
36
- // EPERM on the group can still mean the child is killable directly, and
37
- // EPERM is what a partially-privileged tree produces.
38
27
  if (code === "EPERM") {
39
28
  try {
40
29
  process.kill(child.pid, "SIGKILL");
@@ -49,7 +38,6 @@ export function killTree(child, platform = process.platform) {
49
38
  return { killed: false, wholeTree: false, reason: "error" };
50
39
  }
51
40
  }
52
- /** a sentence for the tool result, so a partial kill is never silent */
53
41
  export function describeKill(outcome) {
54
42
  if (outcome.killed && outcome.wholeTree)
55
43
  return "killed (whole process group)";