jeo-code 0.6.9 → 0.6.11
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/CHANGELOG.md +18 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/memory-okf.ts +275 -0
- package/src/agent/tools.ts +114 -63
- package/src/ai/model-manager.ts +5 -5
- package/src/ai/providers/anthropic.ts +2 -2
- package/src/ai/providers/gemini.ts +3 -3
- package/src/commands/launch/input.ts +35 -6
- package/src/commands/launch.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.11] - 2026-06-16
|
|
10
|
+
_Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt._
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Larger thinking-token budgets across every level.** `thinkingMaxTokens` is raised (minimal 1k→4k, low 2k→8k, default 4k→16k, high 8k→24k, xhigh 16k→32k), along with the Anthropic (medium 4k→10k, high 10k→24k) and Gemini (low→4k, medium→10k, high→24k) per-provider budgets — so each reasoning level actually gets room to think.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Terminal capability-response sequences no longer corrupt the prompt.** The mouse-report filter (0.6.7) now also swallows Device-Attributes / mode replies that the outer terminal sends when tmux probes it on attach, on both the idle keypress path and the live-turn raw-stdin drain.
|
|
17
|
+
|
|
18
|
+
## [0.6.10] - 2026-06-16
|
|
19
|
+
_OKF memory-format foundation and a hardened bashTool subprocess drain._
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **OKF (Open Knowledge Format) v0.1 foundation** — a standalone schema/format layer for jeo memory: YAML-frontmatter parse/serialize with extension-key round-trip, concept-ID computation, and a tolerant v0.1 conformance validator (`src/agent/memory-okf.ts`). Sprint 01 only — it does not touch the existing distill/inject pipeline yet, and adds zero native dependencies. Design notes live under `docs/okf_mem/`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **bashTool subprocess draining hardened.** A shared `drainPipe` reader plus reader-cancellation, a SIGTERM→SIGKILL kill timer, and a brief pipe-linger ensure stdout/stderr fully drain before teardown — no file-descriptor or child-process leak across the normal, timed-out, and abandoned lifecycles (the subprocess-leak probe stays at baseline).
|
|
26
|
+
|
|
9
27
|
## [0.6.9] - 2026-06-16
|
|
10
28
|
_Live streaming blocks size to their content and the viewport instead of a fixed rectangle._
|
|
11
29
|
|
package/README.ja.md
CHANGED
|
@@ -158,11 +158,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
158
158
|
## 変更履歴 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
162
|
+
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
161
163
|
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
162
164
|
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
163
165
|
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
164
|
-
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
|
-
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -158,11 +158,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
158
158
|
## 변경 이력 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
162
|
+
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
161
163
|
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
162
164
|
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
163
165
|
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
164
|
-
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
|
-
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -158,11 +158,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
158
158
|
## Changelog
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
162
|
+
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
161
163
|
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
162
164
|
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
163
165
|
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
164
|
-
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
|
-
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -158,11 +158,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
158
158
|
## 更新日志 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.11]** (2026-06-16) — Larger reasoning budgets, and terminal capability-response sequences kept out of the prompt.
|
|
162
|
+
- **[0.6.10]** (2026-06-16) — OKF memory-format foundation and a hardened bashTool subprocess drain.
|
|
161
163
|
- **[0.6.9]** (2026-06-16) — Live streaming blocks size to their content and the viewport instead of a fixed rectangle.
|
|
162
164
|
- **[0.6.8]** (2026-06-16) — OAuth loopback callback host pinned to `localhost` to match provider-registered redirect URIs.
|
|
163
165
|
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
164
|
-
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
|
-
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKF (Open Knowledge Format) v0.1 foundation for jeo memory — Sprint 01.
|
|
3
|
+
*
|
|
4
|
+
* Pure schema/format layer: YAML-frontmatter parse/serialize (extension keys
|
|
5
|
+
* round-trip preserved), concept-ID computation, and a tolerant v0.1
|
|
6
|
+
* conformance validator. This module deliberately does NOT touch the existing
|
|
7
|
+
* `memory.ts` distill/inject pipeline (Sprints 02–05 build on this contract).
|
|
8
|
+
*
|
|
9
|
+
* Design contract: docs/okf_mem/sprint-01-format-foundation/index.md
|
|
10
|
+
* Spec digest: docs/okf_mem/concepts/okf-spec-digest.md
|
|
11
|
+
*
|
|
12
|
+
* Why a bespoke YAML subset (not a dep): OKF frontmatter is intentionally tiny
|
|
13
|
+
* — flat `key: value` pairs with scalars and inline `[a, b]` lists. A focused
|
|
14
|
+
* parser keeps jeo's zero-native-dependency promise and gives us exact
|
|
15
|
+
* round-trip control over extension keys (e.g. `confidence`, `last_verified`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** jeo's agreed `type` vocabulary. OKF requires no central registry, but our
|
|
19
|
+
* producers/consumers share these values so injection can filter/prioritize.
|
|
20
|
+
* Unknown types are TOLERATED by the validator (lenient consumption). */
|
|
21
|
+
export const JEO_TYPES = [
|
|
22
|
+
"RepoFact",
|
|
23
|
+
"Command",
|
|
24
|
+
"Gotcha",
|
|
25
|
+
"UserPreference",
|
|
26
|
+
"Reference",
|
|
27
|
+
] as const;
|
|
28
|
+
export type JeoType = (typeof JEO_TYPES)[number];
|
|
29
|
+
|
|
30
|
+
/** Reserved OKF filenames — never concept documents. */
|
|
31
|
+
export const RESERVED_FILES = ["index.md", "log.md"] as const;
|
|
32
|
+
|
|
33
|
+
/** A parsed frontmatter value: scalar or inline list of strings. */
|
|
34
|
+
export type FrontmatterValue = string | number | boolean | string[];
|
|
35
|
+
/** Ordered frontmatter map (JS string-key insertion order is preserved). */
|
|
36
|
+
export type Frontmatter = Record<string, FrontmatterValue>;
|
|
37
|
+
|
|
38
|
+
export interface ParsedConcept {
|
|
39
|
+
/** Frontmatter map, key order preserved. Empty when no frontmatter block. */
|
|
40
|
+
frontmatter: Frontmatter;
|
|
41
|
+
/** Markdown body after the closing `---` (leading newline trimmed). */
|
|
42
|
+
body: string;
|
|
43
|
+
/** True when a `---`-delimited frontmatter block was found and parsed. */
|
|
44
|
+
hasFrontmatter: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Concept identity ────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Concept ID = bundle-relative path with `.md` stripped and separators
|
|
50
|
+
* normalized to `/` (OKF rule: `commands/bun-test.md` → `commands/bun-test`). */
|
|
51
|
+
export function conceptId(bundleRelativePath: string): string {
|
|
52
|
+
const norm = bundleRelativePath.replace(/\\/g, "/").replace(/^\.?\/+/, "");
|
|
53
|
+
return norm.replace(/\.md$/i, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Basename of a bundle-relative path (handles both separators). */
|
|
57
|
+
function basename(p: string): string {
|
|
58
|
+
const norm = p.replace(/\\/g, "/");
|
|
59
|
+
const i = norm.lastIndexOf("/");
|
|
60
|
+
return i === -1 ? norm : norm.slice(i + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when a path's filename is an OKF reserved file (`index.md`/`log.md`). */
|
|
64
|
+
export function isReservedFile(bundleRelativePath: string): boolean {
|
|
65
|
+
const base = basename(bundleRelativePath).toLowerCase();
|
|
66
|
+
return (RESERVED_FILES as readonly string[]).includes(base);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** kebab-case slug for a concept filename: lowercased, non-alphanumerics → `-`,
|
|
70
|
+
* collapsed and trimmed. Empty input yields `untitled`. */
|
|
71
|
+
export function slugify(title: string): string {
|
|
72
|
+
const slug = title
|
|
73
|
+
.normalize("NFKD")
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
76
|
+
.replace(/^-+|-+$/g, "")
|
|
77
|
+
.replace(/-{2,}/g, "-");
|
|
78
|
+
return slug || "untitled";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Frontmatter parse / serialize (round-trip preserving) ────────────────────
|
|
82
|
+
|
|
83
|
+
function unquote(raw: string): string {
|
|
84
|
+
const s = raw.trim();
|
|
85
|
+
if (s.length >= 2 && ((s[0] === '"' && s[s.length - 1] === '"') || (s[0] === "'" && s[s.length - 1] === "'"))) {
|
|
86
|
+
return s.slice(1, -1);
|
|
87
|
+
}
|
|
88
|
+
return s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Parse a single scalar token into the narrowest faithful JS value. */
|
|
92
|
+
function parseScalar(raw: string): FrontmatterValue {
|
|
93
|
+
const s = raw.trim();
|
|
94
|
+
if (s.length === 0) return "";
|
|
95
|
+
// Quoted strings are ALWAYS strings (preserve e.g. okf_version "0.1").
|
|
96
|
+
if ((s[0] === '"' && s[s.length - 1] === '"') || (s[0] === "'" && s[s.length - 1] === "'")) {
|
|
97
|
+
return unquote(s);
|
|
98
|
+
}
|
|
99
|
+
if (s === "true") return true;
|
|
100
|
+
if (s === "false") return false;
|
|
101
|
+
// Integer or float (no leading zeros like 0.1 issue — that's a valid float).
|
|
102
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) {
|
|
103
|
+
const n = Number(s);
|
|
104
|
+
if (Number.isFinite(n)) return n;
|
|
105
|
+
}
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Parse one frontmatter value, supporting inline `[a, b, c]` lists. */
|
|
110
|
+
function parseValue(raw: string): FrontmatterValue {
|
|
111
|
+
const s = raw.trim();
|
|
112
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
113
|
+
const inner = s.slice(1, -1).trim();
|
|
114
|
+
if (inner === "") return [];
|
|
115
|
+
return inner.split(",").map(item => unquote(item.trim()));
|
|
116
|
+
}
|
|
117
|
+
return parseScalar(s);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse an OKF concept document into `{ frontmatter, body, hasFrontmatter }`.
|
|
122
|
+
* Tolerant: a document without a `---` block yields an empty frontmatter and
|
|
123
|
+
* the whole text as body (conformance is judged separately by the validator).
|
|
124
|
+
*/
|
|
125
|
+
export function parseConcept(text: string): ParsedConcept {
|
|
126
|
+
const lines = text.split("\n");
|
|
127
|
+
// A frontmatter block must START at line 0 with a bare `---`.
|
|
128
|
+
if (lines[0]?.trim() !== "---") {
|
|
129
|
+
return { frontmatter: {}, body: text, hasFrontmatter: false };
|
|
130
|
+
}
|
|
131
|
+
let end = -1;
|
|
132
|
+
for (let i = 1; i < lines.length; i++) {
|
|
133
|
+
if (lines[i]?.trim() === "---") {
|
|
134
|
+
end = i;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (end === -1) {
|
|
139
|
+
// Opening `---` with no close: not a valid block — treat as bodyless content.
|
|
140
|
+
return { frontmatter: {}, body: text, hasFrontmatter: false };
|
|
141
|
+
}
|
|
142
|
+
const frontmatter: Frontmatter = {};
|
|
143
|
+
for (let i = 1; i < end; i++) {
|
|
144
|
+
const line = lines[i] ?? "";
|
|
145
|
+
const trimmed = line.trim();
|
|
146
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
147
|
+
const colon = line.indexOf(":");
|
|
148
|
+
if (colon === -1) continue; // skip malformed lines tolerantly
|
|
149
|
+
const key = line.slice(0, colon).trim();
|
|
150
|
+
if (!key) continue;
|
|
151
|
+
frontmatter[key] = parseValue(line.slice(colon + 1));
|
|
152
|
+
}
|
|
153
|
+
const body = lines.slice(end + 1).join("\n").replace(/^\n+/, "");
|
|
154
|
+
return { frontmatter, body, hasFrontmatter: true };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** True when a string scalar must be quoted to round-trip as a string
|
|
158
|
+
* (i.e. would otherwise parse back as a number/bool, or has fragile edges). */
|
|
159
|
+
function needsQuoting(s: string): boolean {
|
|
160
|
+
if (s === "") return true;
|
|
161
|
+
if (s === "true" || s === "false") return true;
|
|
162
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return true;
|
|
163
|
+
if (s !== s.trim()) return true; // leading/trailing whitespace
|
|
164
|
+
if (/[:#\[\]{}",']/.test(s)) return true;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function serializeScalar(v: string | number | boolean): string {
|
|
169
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
170
|
+
return needsQuoting(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function serializeValue(v: FrontmatterValue): string {
|
|
174
|
+
if (Array.isArray(v)) {
|
|
175
|
+
return `[${v.map(item => (needsQuoting(item) ? `"${item.replace(/"/g, '\\"')}"` : item)).join(", ")}]`;
|
|
176
|
+
}
|
|
177
|
+
return serializeScalar(v);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Serialize frontmatter + body back into an OKF concept document.
|
|
182
|
+
* Key order is preserved; `serialize(parse(x))` is idempotent for documents
|
|
183
|
+
* this module produced. Body gets exactly one blank line after the block.
|
|
184
|
+
*/
|
|
185
|
+
export function serializeConcept(frontmatter: Frontmatter, body: string): string {
|
|
186
|
+
const fmLines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${serializeValue(v)}`);
|
|
187
|
+
const block = ["---", ...fmLines, "---"].join("\n");
|
|
188
|
+
const trimmedBody = body.replace(/^\n+/, "");
|
|
189
|
+
return trimmedBody ? `${block}\n\n${trimmedBody}` : `${block}\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── OKF v0.1 conformance ─────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export interface BundleFile {
|
|
195
|
+
/** Bundle-relative path, e.g. `commands/bun-test.md`. */
|
|
196
|
+
path: string;
|
|
197
|
+
/** Raw file contents. */
|
|
198
|
+
content: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface ConformanceIssue {
|
|
202
|
+
path: string;
|
|
203
|
+
/** `error` fails conformance; `warning` is a lenient-guide hint only. */
|
|
204
|
+
level: "error" | "warning";
|
|
205
|
+
message: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ConformanceReport {
|
|
209
|
+
conformant: boolean;
|
|
210
|
+
issues: ConformanceIssue[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ISO_DATE_HEADING = /^#{1,6}\s+(\d{4}-\d{2}-\d{2})/;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate one file against OKF v0.1 + jeo conventions.
|
|
217
|
+
*
|
|
218
|
+
* Errors (reject conformance):
|
|
219
|
+
* - non-reserved `.md` missing a parseable frontmatter block.
|
|
220
|
+
* - frontmatter with missing or empty `type`.
|
|
221
|
+
* - `log.md` date heading that is not ISO 8601 `YYYY-MM-DD`.
|
|
222
|
+
*
|
|
223
|
+
* Warnings (lenient guides — never reject):
|
|
224
|
+
* - unknown `type` value.
|
|
225
|
+
* - missing recommended fields (`title`, `description`).
|
|
226
|
+
*/
|
|
227
|
+
export function validateFile(file: BundleFile): ConformanceIssue[] {
|
|
228
|
+
const issues: ConformanceIssue[] = [];
|
|
229
|
+
const base = basename(file.path).toLowerCase();
|
|
230
|
+
|
|
231
|
+
if (base === "log.md") {
|
|
232
|
+
// Reserved: if present, date headings must be ISO 8601.
|
|
233
|
+
for (const line of file.content.split("\n")) {
|
|
234
|
+
const m = line.match(/^#{1,6}\s+\S+/);
|
|
235
|
+
if (!m) continue;
|
|
236
|
+
// Only enforce on headings that look like dates; tolerate prose headings.
|
|
237
|
+
if (/^#{1,6}\s+\d/.test(line) && !ISO_DATE_HEADING.test(line)) {
|
|
238
|
+
issues.push({ path: file.path, level: "error", message: `log.md date heading not ISO 8601 (YYYY-MM-DD): "${line.trim()}"` });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return issues;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (base === "index.md") {
|
|
245
|
+
// Reserved: index.md needs no frontmatter; nothing to reject.
|
|
246
|
+
return issues;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Concept document.
|
|
250
|
+
const parsed = parseConcept(file.content);
|
|
251
|
+
if (!parsed.hasFrontmatter) {
|
|
252
|
+
issues.push({ path: file.path, level: "error", message: "concept document missing YAML frontmatter block" });
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
255
|
+
const type = parsed.frontmatter.type;
|
|
256
|
+
if (typeof type !== "string" || type.trim() === "") {
|
|
257
|
+
issues.push({ path: file.path, level: "error", message: "frontmatter `type` is required and must be non-empty" });
|
|
258
|
+
} else if (!(JEO_TYPES as readonly string[]).includes(type)) {
|
|
259
|
+
issues.push({ path: file.path, level: "warning", message: `unknown type "${type}" (tolerated; not in jeo vocabulary)` });
|
|
260
|
+
}
|
|
261
|
+
if (!parsed.frontmatter.title) {
|
|
262
|
+
issues.push({ path: file.path, level: "warning", message: "missing recommended field `title`" });
|
|
263
|
+
}
|
|
264
|
+
if (!parsed.frontmatter.description) {
|
|
265
|
+
issues.push({ path: file.path, level: "warning", message: "missing recommended field `description`" });
|
|
266
|
+
}
|
|
267
|
+
return issues;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Validate a whole bundle. Conformant iff there are zero `error`-level issues
|
|
271
|
+
* (warnings never reject — OKF's tolerant consumption model). */
|
|
272
|
+
export function validateBundle(files: BundleFile[]): ConformanceReport {
|
|
273
|
+
const issues = files.flatMap(validateFile);
|
|
274
|
+
return { conformant: !issues.some(i => i.level === "error"), issues };
|
|
275
|
+
}
|
package/src/agent/tools.ts
CHANGED
|
@@ -587,6 +587,31 @@ export async function editTool(
|
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/** Drain a readable byte stream to a string, cancel-safe. We read via an explicit
|
|
591
|
+
* reader (NOT `new Response(stream).text()`, whose read is uncancellable) so a caller
|
|
592
|
+
* can cancel() to force the in-flight read to settle — essential when a SIGKILLed
|
|
593
|
+
* process leaves a backgrounded grandchild holding the pipe's write end open, so it
|
|
594
|
+
* never hits EOF (proven by scripts/subproc-probe.ts ABANDON mode). An optional
|
|
595
|
+
* (caller-throttled) onChunk drives the live output view. */
|
|
596
|
+
async function drainPipe(
|
|
597
|
+
r: ReadableStreamDefaultReader<Uint8Array>,
|
|
598
|
+
onChunk?: (partial: string) => void,
|
|
599
|
+
): Promise<string> {
|
|
600
|
+
const dec = new TextDecoder();
|
|
601
|
+
let out = "";
|
|
602
|
+
try {
|
|
603
|
+
for (;;) {
|
|
604
|
+
const { done, value } = await r.read();
|
|
605
|
+
if (done) break;
|
|
606
|
+
out += dec.decode(value, { stream: true });
|
|
607
|
+
onChunk?.(out);
|
|
608
|
+
}
|
|
609
|
+
out += dec.decode();
|
|
610
|
+
onChunk?.(out);
|
|
611
|
+
} catch { /* cancelled reader surfaces here; return what we have */ }
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
|
|
590
615
|
export async function bashTool(
|
|
591
616
|
command: string,
|
|
592
617
|
cwd: string = process.cwd(),
|
|
@@ -612,6 +637,10 @@ export async function bashTool(
|
|
|
612
637
|
// Run the command using Bun's native spawn
|
|
613
638
|
const proc = Bun.spawn(["bash", "-c", command], {
|
|
614
639
|
cwd: runCwd,
|
|
640
|
+
// Detach stdin (no inherited TTY): a command that reads stdin (`cat`, `read`,
|
|
641
|
+
// a REPL, an interactive prompt) then gets immediate EOF and exits instead of
|
|
642
|
+
// blocking the whole turn forever waiting for input that never comes.
|
|
643
|
+
stdin: "ignore",
|
|
615
644
|
stdout: "pipe",
|
|
616
645
|
stderr: "pipe",
|
|
617
646
|
// Inherit the parent env; merge caller-supplied (sanitized) vars on top.
|
|
@@ -621,90 +650,93 @@ export async function bashTool(
|
|
|
621
650
|
let timedOut = false;
|
|
622
651
|
let aborted = false;
|
|
623
652
|
const TIMEOUT_MS = timeoutMs;
|
|
653
|
+
// Grace after the SHELL exits before we force the pipe readers closed: a finished
|
|
654
|
+
// command's buffered tail flushes far inside this window; only a backgrounded
|
|
655
|
+
// grandchild that inherited the pipe (`cmd &`, daemons, `nohup`) keeps it open
|
|
656
|
+
// past it — and that one must not stall the turn.
|
|
657
|
+
const PIPE_LINGER_MS = 500;
|
|
624
658
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
659
|
+
let stdoutReader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
660
|
+
let stderrReader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
661
|
+
// Force EOF on both pipe readers. The drain loops `await r.read()`, which NEVER
|
|
662
|
+
// returns `done` while ANY process holds the write end open — a SIGKILLed shell
|
|
663
|
+
// can leave a backgrounded grandchild clutching the FD, so killing the shell is
|
|
664
|
+
// not enough (proven by scripts/subproc-probe.ts ABANDON mode). cancel() resolves
|
|
665
|
+
// the in-flight read immediately, unwinding the drains so the call returns instead
|
|
666
|
+
// of hanging forever. Cheap to call repeatedly; harmless before the readers exist.
|
|
667
|
+
const cancelReaders = () => {
|
|
668
|
+
try { stdoutReader?.cancel(); } catch {}
|
|
669
|
+
try { stderrReader?.cancel(); } catch {}
|
|
670
|
+
};
|
|
625
671
|
const timer = setTimeout(() => {
|
|
626
672
|
timedOut = true;
|
|
627
|
-
// Graceful first (SIGTERM), then force-kill (SIGKILL) if it ignores it.
|
|
673
|
+
// Graceful first (SIGTERM), then force-kill (SIGKILL) if it ignores it. At the
|
|
674
|
+
// SIGKILL step ALSO cut the readers: the old code killed the shell but never
|
|
675
|
+
// cancelled the readers, so an orphan-held pipe outlived the deadline and the
|
|
676
|
+
// turn hung forever (the reported freeze).
|
|
628
677
|
try { proc.kill(); } catch {}
|
|
629
|
-
killTimer = setTimeout(() => { try { proc.kill(9); } catch {} }, 3_000);
|
|
678
|
+
killTimer = setTimeout(() => { try { proc.kill(9); } catch {} cancelReaders(); }, 3_000);
|
|
630
679
|
}, TIMEOUT_MS);
|
|
631
680
|
// Abort wiring: if the turn is cancelled, SIGKILL the child immediately AND cancel
|
|
632
|
-
// both pipe readers so the drain loops
|
|
633
|
-
// explicitly (rather than `for await` / `new Response`, whose hidden iterator locks
|
|
634
|
-
// we cannot cancel): cancel() resolves the in-flight read({ done:true }) immediately,
|
|
635
|
-
// unwinding each loop even when the killed child's pipe is slow to hit EOF. Cancelling
|
|
636
|
-
// stderr also prevents a hang — after kill(9) its pipe never sees EOF, so awaiting an
|
|
637
|
-
// uncancellable Response would block forever. Without all this the child is orphaned,
|
|
638
|
-
// holding two pipe FDs (proven by scripts/subproc-probe.ts ABANDON mode: +1 fd & +1
|
|
639
|
-
// child per call).
|
|
640
|
-
let stdoutReader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
641
|
-
let stderrReader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
681
|
+
// both pipe readers so the drain loops unwind at once.
|
|
642
682
|
const onAbort = () => {
|
|
643
683
|
aborted = true;
|
|
644
684
|
try { proc.kill(9); } catch {}
|
|
645
|
-
|
|
646
|
-
try { stderrReader?.cancel(); } catch {}
|
|
685
|
+
cancelReaders();
|
|
647
686
|
};
|
|
648
687
|
if (signal) {
|
|
649
688
|
if (signal.aborted) onAbort();
|
|
650
689
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
651
690
|
}
|
|
652
691
|
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
): Promise<string> => {
|
|
659
|
-
const dec = new TextDecoder();
|
|
660
|
-
let out = "";
|
|
661
|
-
try {
|
|
662
|
-
for (;;) {
|
|
663
|
-
if (aborted) break;
|
|
664
|
-
const { done, value } = await r.read();
|
|
665
|
-
if (done) break;
|
|
666
|
-
out += dec.decode(value, { stream: true });
|
|
667
|
-
onChunk?.(out);
|
|
668
|
-
}
|
|
669
|
-
out += dec.decode();
|
|
670
|
-
onChunk?.(out);
|
|
671
|
-
} catch { /* cancelled reader surfaces here; return what we have */ }
|
|
672
|
-
return out;
|
|
673
|
-
};
|
|
692
|
+
// Start BOTH drains concurrently so neither pipe can fill its 64KB buffer and
|
|
693
|
+
// deadlock the child (a child blocks on write() once an unread pipe fills up).
|
|
694
|
+
// cancelReaders() (abort/timeout) unwinds an in-flight read, so no aborted-flag
|
|
695
|
+
// poll is needed inside the shared drainPipe.
|
|
696
|
+
stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
674
697
|
stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
675
|
-
|
|
676
|
-
|
|
698
|
+
let lastEmit = 0;
|
|
699
|
+
const stdoutPromise = drainPipe(
|
|
700
|
+
stdoutReader,
|
|
701
|
+
onProgress
|
|
702
|
+
? (partial) => {
|
|
703
|
+
// Throttle the live sink to ~80ms.
|
|
704
|
+
const now = Date.now();
|
|
705
|
+
if (now - lastEmit >= 80) { lastEmit = now; onProgress(partial); }
|
|
706
|
+
}
|
|
707
|
+
: undefined,
|
|
708
|
+
).catch(() => "");
|
|
709
|
+
const stderrPromise = drainPipe(stderrReader).catch(() => "");
|
|
710
|
+
|
|
677
711
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
682
|
-
stdout = await drainAll(stdoutReader, (partial) => {
|
|
683
|
-
const now = Date.now();
|
|
684
|
-
if (now - lastEmit >= 80) { lastEmit = now; onProgress(partial); }
|
|
685
|
-
});
|
|
686
|
-
onProgress(stdout);
|
|
687
|
-
} else if (!aborted) {
|
|
688
|
-
stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
689
|
-
stdout = await drainAll(stdoutReader);
|
|
690
|
-
}
|
|
712
|
+
// Wait for the SHELL to exit. Bounded: the timeout path SIGTERMs then SIGKILLs,
|
|
713
|
+
// so even a foreground command that blocks (e.g. on /dev/tty) cannot exceed
|
|
714
|
+
// TIMEOUT_MS (+3s grace).
|
|
691
715
|
if (!aborted) await proc.exited;
|
|
692
|
-
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
|
|
716
|
+
// The shell exited, but a backgrounded grandchild can keep the pipes open
|
|
717
|
+
// indefinitely; without forcing them closed the drains hang the whole turn.
|
|
718
|
+
// Give a finished command's tail a brief grace to flush, then cut the readers
|
|
719
|
+
// in the finally below — so a daemon-spawning command returns ~PIPE_LINGER_MS
|
|
720
|
+
// after the shell exits instead of stalling until the timeout.
|
|
721
|
+
if (!aborted && !timedOut) {
|
|
722
|
+
await Promise.race([
|
|
723
|
+
Promise.all([stdoutPromise, stderrPromise]),
|
|
724
|
+
new Promise<void>(resolve => setTimeout(resolve, PIPE_LINGER_MS)),
|
|
725
|
+
]);
|
|
726
|
+
}
|
|
696
727
|
} finally {
|
|
697
728
|
clearTimeout(timer);
|
|
698
729
|
if (killTimer) clearTimeout(killTimer);
|
|
699
730
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
731
|
+
// Reap a still-alive child (normal exit, abort, or timeout) so no orphaned
|
|
732
|
+
// process survives, then force any pipe still held open (backgrounded
|
|
733
|
+
// grandchild) closed so the awaits below cannot hang.
|
|
703
734
|
if (proc.exitCode === null && proc.signalCode === null) { try { proc.kill(9); } catch {} }
|
|
704
|
-
|
|
705
|
-
await stderrPromise;
|
|
735
|
+
cancelReaders();
|
|
706
736
|
}
|
|
737
|
+
const stdout = await stdoutPromise;
|
|
707
738
|
const stderr = await stderrPromise;
|
|
739
|
+
if (onProgress) onProgress(stdout);
|
|
708
740
|
|
|
709
741
|
let output = [stdout, stderr].filter(Boolean).join("\n");
|
|
710
742
|
const MAX_OUTPUT = 100_000;
|
|
@@ -740,22 +772,41 @@ async function spawnTextWithTimeout(
|
|
|
740
772
|
cwd: string,
|
|
741
773
|
timeoutMs = 60_000,
|
|
742
774
|
): Promise<{ stdout: string; stderr: string; exitCode: number | null; timedOut: boolean }> {
|
|
743
|
-
|
|
775
|
+
// stdin detached so a command that reads stdin gets EOF instead of blocking.
|
|
776
|
+
const proc = Bun.spawn(cmd, { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
744
777
|
let timedOut = false;
|
|
745
778
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
779
|
+
const stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
780
|
+
const stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
781
|
+
const cancelReaders = () => {
|
|
782
|
+
try { stdoutReader.cancel(); } catch {}
|
|
783
|
+
try { stderrReader.cancel(); } catch {}
|
|
784
|
+
};
|
|
785
|
+
const stdoutPromise = drainPipe(stdoutReader);
|
|
786
|
+
const stderrPromise = drainPipe(stderrReader);
|
|
746
787
|
const timer = setTimeout(() => {
|
|
747
788
|
timedOut = true;
|
|
748
789
|
try { proc.kill(); } catch {}
|
|
749
|
-
killTimer = setTimeout(() => { try { proc.kill(9); } catch {} }, 3_000);
|
|
790
|
+
killTimer = setTimeout(() => { try { proc.kill(9); } catch {} cancelReaders(); }, 3_000);
|
|
750
791
|
}, timeoutMs);
|
|
751
792
|
try {
|
|
752
793
|
await proc.exited;
|
|
794
|
+
// Brief grace for a finished command's tail to flush, then cut any pipe still held
|
|
795
|
+
// open by a backgrounded grandchild so the awaits below cannot hang the turn.
|
|
796
|
+
if (!timedOut) {
|
|
797
|
+
await Promise.race([
|
|
798
|
+
Promise.all([stdoutPromise, stderrPromise]),
|
|
799
|
+
new Promise<void>(resolve => setTimeout(resolve, 500)),
|
|
800
|
+
]);
|
|
801
|
+
}
|
|
753
802
|
} finally {
|
|
754
803
|
clearTimeout(timer);
|
|
755
804
|
if (killTimer) clearTimeout(killTimer);
|
|
805
|
+
if (proc.exitCode === null && proc.signalCode === null) { try { proc.kill(9); } catch {} }
|
|
806
|
+
cancelReaders();
|
|
756
807
|
}
|
|
757
|
-
const stdout = await
|
|
758
|
-
const stderr = await
|
|
808
|
+
const stdout = await stdoutPromise;
|
|
809
|
+
const stderr = await stderrPromise;
|
|
759
810
|
return { stdout, stderr, exitCode: proc.exitCode, timedOut };
|
|
760
811
|
}
|
|
761
812
|
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -68,11 +68,11 @@ export function providerModelFor(model: string): string {
|
|
|
68
68
|
|
|
69
69
|
/** Map the configured thinking level to a default max-token budget. */
|
|
70
70
|
export function thinkingMaxTokens(level?: "minimal" | "low" | "medium" | "high" | "xhigh"): number {
|
|
71
|
-
if (level === "minimal") return
|
|
72
|
-
if (level === "low") return
|
|
73
|
-
if (level === "high") return
|
|
74
|
-
if (level === "xhigh") return
|
|
75
|
-
return
|
|
71
|
+
if (level === "minimal") return 4000;
|
|
72
|
+
if (level === "low") return 8000;
|
|
73
|
+
if (level === "high") return 24000;
|
|
74
|
+
if (level === "xhigh") return 31999;
|
|
75
|
+
return 16000;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Map the thinking level to an OpenAI reasoning-effort tier. `minimal` maps to `low`
|
|
@@ -77,8 +77,8 @@ function anthropicSystemBlocks(
|
|
|
77
77
|
function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"], maxTokens: number): number | undefined {
|
|
78
78
|
let budget: number;
|
|
79
79
|
switch (effort) {
|
|
80
|
-
case "medium": budget =
|
|
81
|
-
case "high": budget =
|
|
80
|
+
case "medium": budget = 10000; break;
|
|
81
|
+
case "high": budget = 24000; break;
|
|
82
82
|
default: return undefined;
|
|
83
83
|
}
|
|
84
84
|
return Math.min(budget, Math.max(1024, maxTokens - 1024));
|
|
@@ -17,9 +17,9 @@ export function geminiThinkingBudget(model: string, effort?: CallOptions["reason
|
|
|
17
17
|
const floor = m.includes("pro") ? 128 : 0; // pro-class cannot fully disable thinking
|
|
18
18
|
let budget: number;
|
|
19
19
|
switch (effort) {
|
|
20
|
-
case "low": budget =
|
|
21
|
-
case "medium": budget =
|
|
22
|
-
case "high": budget =
|
|
20
|
+
case "low": budget = 4000; break;
|
|
21
|
+
case "medium": budget = 10000; break;
|
|
22
|
+
case "high": budget = 24000; break;
|
|
23
23
|
case "minimal":
|
|
24
24
|
default: budget = floor;
|
|
25
25
|
}
|
|
@@ -104,16 +104,45 @@ export function matchMouseReport(data: string, i: number): number {
|
|
|
104
104
|
return 0;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* on the
|
|
107
|
+
/** Byte length of a terminal CAPABILITY-RESPONSE sequence beginning at `data[i]`, else 0.
|
|
108
|
+
* These are REPLIES the terminal sends to capability queries — Primary/Secondary Device
|
|
109
|
+
* Attributes (`ESC[?…c` / `ESC[>…c` / `ESC[=…c`), XTVERSION and other DCS replies
|
|
110
|
+
* (`ESC P…ST`), and OSC replies like a color query (`ESC]11;rgb:…ST`). jeo never sends
|
|
111
|
+
* these queries, but tmux probes the OUTER terminal on attach, and the outer terminal's
|
|
112
|
+
* answers can land on stdin (the leaked `62;4;9;22c…>|xterm.js(…)` garbage in the prompt).
|
|
113
|
+
* They are never typed input, so the whole sequence is swallowed. A reply split across
|
|
114
|
+
* chunks (no terminator yet) consumes the tail rather than leaking it. */
|
|
115
|
+
export function matchTerminalReport(data: string, i: number): number {
|
|
116
|
+
// CSI device-attribute / mode replies: ESC [ (? | > | =) … <final letter>.
|
|
117
|
+
if (data.startsWith("\u001b[?", i) || data.startsWith("\u001b[>", i) || data.startsWith("\u001b[=", i)) {
|
|
118
|
+
let j = i + 3;
|
|
119
|
+
while (j < data.length && !/[A-Za-z]/.test(data[j]!)) j++; // params: digits ; : $ → final letter
|
|
120
|
+
return (j < data.length ? j + 1 : data.length) - i;
|
|
121
|
+
}
|
|
122
|
+
// DCS (ESC P … ) or OSC (ESC ] … ) reply — terminated by ST (ESC \) or BEL.
|
|
123
|
+
if (data.startsWith("\u001bP", i) || data.startsWith("\u001b]", i)) {
|
|
124
|
+
let j = i + 2;
|
|
125
|
+
while (j < data.length) {
|
|
126
|
+
if (data[j] === "\u0007") return j + 1 - i; // BEL
|
|
127
|
+
if (data[j] === "\u001b" && data[j + 1] === "\\") return j + 2 - i; // ST
|
|
128
|
+
j++;
|
|
129
|
+
}
|
|
130
|
+
return data.length - i; // unterminated tail (split chunk) — consume the rest
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Remove every terminal MOUSE-REPORT and CAPABILITY-RESPONSE sequence from a plain
|
|
136
|
+
* (non-paste) input segment. The live-turn drain (`queuePromptInputChunk`) reads RAW
|
|
137
|
+
* stdin, so a wheel/click report or a tmux-probed device-attribute reply buffered during
|
|
138
|
+
* a running turn would otherwise have its printable remnant (`[M`, SGR digits, or
|
|
139
|
+
* `62;4;9;22c…`) fed into the next prompt — the same "값 입력" corruption the keyFilter
|
|
140
|
+
* blocks on the idle path. */
|
|
112
141
|
export function stripMouseReports(s: string): string {
|
|
113
142
|
let out = "";
|
|
114
143
|
let i = 0;
|
|
115
144
|
while (i < s.length) {
|
|
116
|
-
const m = matchMouseReport(s, i);
|
|
145
|
+
const m = matchMouseReport(s, i) || matchTerminalReport(s, i);
|
|
117
146
|
if (m > 0) { i += m; continue; }
|
|
118
147
|
out += s[i];
|
|
119
148
|
i += 1;
|
package/src/commands/launch.ts
CHANGED
|
@@ -127,6 +127,7 @@ import {
|
|
|
127
127
|
CURSOR_COMBO_REWRITES,
|
|
128
128
|
matchCursorCombo,
|
|
129
129
|
matchMouseReport,
|
|
130
|
+
matchTerminalReport,
|
|
130
131
|
stripMouseReports,
|
|
131
132
|
rewriteCursorCombos,
|
|
132
133
|
queuePromptInputChunk,
|
|
@@ -175,6 +176,7 @@ export {
|
|
|
175
176
|
CURSOR_COMBO_REWRITES,
|
|
176
177
|
matchCursorCombo,
|
|
177
178
|
matchMouseReport,
|
|
179
|
+
matchTerminalReport,
|
|
178
180
|
stripMouseReports,
|
|
179
181
|
rewriteCursorCombos,
|
|
180
182
|
queuePromptInputChunk,
|
|
@@ -1268,6 +1270,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1268
1270
|
// their payload bytes would otherwise be typed into the prompt. Never in a paste.
|
|
1269
1271
|
const mouse = matchMouseReport(data, i);
|
|
1270
1272
|
if (mouse > 0) { i += mouse; continue; }
|
|
1273
|
+
// Swallow TERMINAL CAPABILITY-RESPONSE sequences (DA1/DA2/XTVERSION/OSC replies):
|
|
1274
|
+
// tmux probes the outer terminal on attach and its answers (`62;4;9;22c…>|xterm.js…`)
|
|
1275
|
+
// can land on stdin; without this they spray into the prompt as typed text.
|
|
1276
|
+
const report = matchTerminalReport(data, i);
|
|
1277
|
+
if (report > 0) { i += report; continue; }
|
|
1271
1278
|
let matched = false;
|
|
1272
1279
|
for (const seq of SHIFT_ENTER_SEQS) {
|
|
1273
1280
|
if (data.startsWith(seq, i)) { out += SENTINEL; i += seq.length; matched = true; break; }
|