@litfamily/litopencode 1.0.36 โ 1.0.38
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 +16 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/state.d.ts +1 -0
- package/dist/state.js +1 -0
- package/dist/tools.d.ts +15 -5
- package/dist/tui/mission-band.d.ts +7 -0
- package/dist/tui/mission-band.js +4 -0
- package/dist/tui/mission-skills.js +3 -1
- package/dist/tui/mission-source-v2.d.ts +23 -0
- package/dist/tui/mission-source-v2.js +29 -4
- package/dist/tui/mission-source.d.ts +1 -0
- package/dist/tui/mission-source.js +3 -2
- package/dist/tui/mission-text.d.ts +3 -3
- package/dist/tui/mission-text.js +5 -5
- package/dist/v2/events.d.ts +1 -1
- package/dist/v2/events.js +31 -5
- package/dist/v2/notices.d.ts +13 -0
- package/dist/v2/notices.js +143 -0
- package/dist/v2/tui-footer.d.ts +20 -0
- package/dist/v2/tui-footer.js +56 -0
- package/dist/v2/tui-notices.d.ts +7 -0
- package/dist/v2/tui-notices.js +45 -0
- package/dist/v2/tui.d.ts +43 -1
- package/dist/v2/tui.js +345 -32
- package/dist/v2/usage-limits.d.ts +19 -0
- package/dist/v2/usage-limits.js +70 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +24 -5
- package/docs/reference.md +29 -7
- package/package.json +6 -2
- package/skills/lit-typographic-motion/engine/cover/render.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +3 -2
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +1 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +1 -0
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +109 -5
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +39 -1
- package/skills/lit-typographic-motion/references/cover.md +2 -1
- package/skills/lit-typographic-motion/references/runtime.md +1 -1
- package/skills/lit-typographic-motion/references/stage.md +48 -2
- package/skills/lit-typographic-motion/requirements-audio.txt +92 -89
- package/skills/managed-skill-manifest.json +13 -13
- package/skills/readme-studio/templates/hyperframes/package-lock.json +4 -4
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/skills/readme-studio/templates/remotion/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ are tracked separately from this product history.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 1.0.38 - 2026-11-02
|
|
9
|
+
|
|
10
|
+
- Dependency alerts cleared: zod 4.6.6 (the runtime dependency and the Remotion cover template), the audio runtime's msgpack 1.2.3, @hono/node-server 2.1.5 in the HyperFrames cover template, and toml 5.0.1 in the development tree.
|
|
11
|
+
- Releases are faster: the publish workflow no longer repeats the test suite and the QA probes. It checks with GitHub that CI already passed on the exact commit being released and stops if it did not. A publish from your own machine still runs the full suite first.
|
|
12
|
+
- On OpenCode 2 the footer line beside the prompt now steps down with the terminal width instead of being clipped by the host: the ignition mark with the whole stage chip, the mark with a smaller chip, the chip alone, `๐ฅ 1/3`, the bare flame, then nothing. Before, a narrow terminal cut the mark mid-word and mangled the host's own figures next to it.
|
|
13
|
+
- On OpenCode 2 the status card shows the OpenCode Go plan windows (`5h 42% ยท 1w 18%`) for sessions on the `opencode-go` provider, read from the host's usage source and asked at most once a minute. Other providers and signed-out hosts show nothing, because OpenCode 2 gives a plugin no other usage data.
|
|
14
|
+
- The automatic-handoff notices now reach the screen on OpenCode 2. The server plugin cannot raise a toast there, so it leaves each notice in `.litopencode/notices.jsonl` and the TUI plugin shows it.
|
|
15
|
+
- Automatic handoff now fires on OpenCode 2. The plugin waited for a `session.status` or `session.idle` event that OpenCode 2 never publishes, so a turn that crossed the percent was never followed by the handoff request. It now reads the end of a turn from `session.execution.succeeded`, `failed` and `interrupted` (and the start from `session.execution.started`), which also records the session as idle for the status card.
|
|
16
|
+
- A flaky test of the alias goal path now waits for the ledger event instead of only the goal in the fake host, so slow runners no longer fail it.
|
|
17
|
+
|
|
18
|
+
## 1.0.37 - 2026-11-01
|
|
19
|
+
|
|
20
|
+
- `lit-typographic-motion` films can now carry real keyed footage, such as a reaction clip or a spinning-cat loop shot on a green screen. Key it outside the renderer into 8-bit RGBA PNG frames, put them in `stage/clips/<folder>/` and list them in `stage/clips/clips.json` (`id`, `dir`, `count`, `fps`). The page loads `/lit/clips.js`, registers a clip once with `LitStage.clip({ parent, id, box })` and shows it from `render(t)`; the frame is picked from film time, so a cue is plain arithmetic and the determinism replay holds. A declared clip is checked against its bytes (12 clips, 360 frames each and 1500 in all, 40 MB each and 128 MB in all, 720 px on the long side, up to 30 fps, real transparency) and is exempt from the flipbook rule; any frame sequence you did not declare is still refused with exit 17. A cover takes no clips. The flash audit and the text checks run on the final pixels, and the report lists each clip. `LitStage.clip.circle`, `wipe` and `iris` keep working.
|
|
21
|
+
- On OpenCode 2 the lit HUD is as complete as on OpenCode 1. The status card (discipline, workflow map, bar, live step, skill chips, lanes in parallel, context use and branch) now sits under every session, the stage chip appears beside the ignition line under the prompt, and the home screen shows the last goal. Until now only the ignition line and the sidebar panel were drawn there. The card reads OpenCode 2's own tool names, so a `skill` call (`id`) lights its chip and `subagent` calls show as lanes.
|
|
22
|
+
- OpenCode 2 also gets pop-ups for the workflow stage moving, a discipline switching and a lane starting or finishing, next to the plan, goal, blocker and completion pop-ups it already had. The restart notice, the `lit adhd` suggestion and the opt-in finish alert (`notify` in the plugin options) work there too.
|
|
23
|
+
|
|
8
24
|
## 1.0.36 - 2026-10-31
|
|
9
25
|
|
|
10
26
|
- On OpenCode 2.2 and later, `lit loop`, `litwork` and `/start-work` set OpenCode's native `/goal` for you. The goal carries your task plus what done means here (tests pass, a real-surface check ran, temporary files are cleaned up, the ledger is closed), and each change prints one `LitOpenCode goal:` line. LitOpenCode manages only goals it created: it resumes one that a server restart paused, clears one after OpenCode's verdict and the ledger both say done, and reports a goal you made as left alone without touching it. When OpenCode says the goal is met but the ledger is open, you get a conflict line and no done. Planning, research, review and bare `lit` never set a goal. OpenCode 1 and older OpenCode 2 builds have no `/goal`, so the ledger carries the work and `native-goal-unavailable` is recorded once.
|
package/README-Ko-KR.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion.webp" width="100%" alt="LitFamily ๋ชจ์
์ปค๋ฒ: ์ผ๊ณฑ ๋ก๋ด ํจ๋์ด ์ฐจ๋ก๋ก ์ผ์ง๊ณ , LitOpenCode ๋ก๋ด์ ๋๊ณผ ํ
๋๋ฆฌ๊ฐ ๋น๋ ๋ค LITFAMILY์ KEEP THE WORK LIT. ๋ฌธ๊ตฌ๊ฐ ๋ฐ์์ง๋ ์์" /></picture></p>
|
|
2
2
|
|
|
3
3
|
# LitOpenCode
|
|
4
4
|
|
|
5
5
|
**LitOpenCode๋ OpenCode์ฉ ์ํฌํ๋ก ํ๋ฌ๊ทธ์ธ์
๋๋ค. ํ๋กฌํํธ์ `lit`๋ง ๋ถ์ด๋ฉด ๊ณํํ๊ณ , ๋ง๋ค๊ณ , ํ์ธํ๊ณ , ๊ธฐ๋ก๊น์ง ๋จ๊ฒจ์ ๋ค์ ์ธ์
์ด ์ด๋์ ๋ฉ์ท๋์ง ๋ฐ๋ก ์๋๋ค.**
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
8
|
+
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-version.svg" alt="1.0.38" /></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="์๊ฐ npm ๋ค์ด๋ก๋ ์" /></a>
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-license.svg" alt="MIT ๋ผ์ด์ ์ค" /></a>
|
|
11
11
|
<a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub ์คํ" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
|
|
|
57
57
|
## ๋ ๋ณด๊ธฐ
|
|
58
58
|
|
|
59
59
|
- [GitHub์ ์ฌ์ฉ ์๋ด](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide-Ko-KR.md): ์ค์น ์ต์
, ์๋ ์ธ์์ธ๊ณ, ๋ฌธ์ ํด๊ฒฐ, ์ ๊ฑฐ
|
|
60
|
-
- [์ํฌํ๋ก ์์ธ ์๋ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
61
|
-
- [๋ง์ด๊ทธ๋ ์ด์
์๋ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
62
|
-
- [LitFamily ํ๋ธ](https://github.com/wjgoarxiv/litfamily) ยท [MIT ๋ผ์ด์ ์ค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
60
|
+
- [์ํฌํ๋ก ์์ธ ์๋ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference-Ko-KR.md) ยท [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference.md)
|
|
61
|
+
- [๋ง์ด๊ทธ๋ ์ด์
์๋ด](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/CHANGELOG.md) ยท [๊ฐ์ธ์ ๋ณด์ ๋คํธ์ํฌ ๋์](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/privacy.md)
|
|
62
|
+
- [LitFamily ํ๋ธ](https://github.com/wjgoarxiv/litfamily) ยท [MIT ๋ผ์ด์ ์ค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE) ยท [์ํ ์ ์ฑ
](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/TRADEMARK.md) ยท [ASCII ๊ธ๊ผด ๋ผ์ด์ ์ค](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/JetBrainsMono-OFL.txt)
|
|
63
63
|
|
|
64
64
|
<details>
|
|
65
65
|
<summary>ASCII ๋ก๊ณ ๋ณต์ฌ</summary>
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
# LitOpenCode
|
|
4
4
|
|
|
5
5
|
**LitOpenCode is a workflow plugin for OpenCode. Add `lit` to a prompt and the work gets planned, built, checked and written down, so the next session knows where you stopped.**
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
8
|
+
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-version.svg" alt="1.0.38" /></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="npm downloads per month" /></a>
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
11
11
|
<a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub stars" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
|
|
|
57
57
|
## More
|
|
58
58
|
|
|
59
59
|
- [Guide on GitHub](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide.md): install options, automatic handoff, troubleshooting and removal.
|
|
60
|
-
- [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
61
|
-
- [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
62
|
-
- [LitFamily hub](https://github.com/wjgoarxiv/litfamily) ยท [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
60
|
+
- [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/reference.md)
|
|
61
|
+
- [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/migration.md) ยท [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/CHANGELOG.md) ยท [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/privacy.md)
|
|
62
|
+
- [LitFamily hub](https://github.com/wjgoarxiv/litfamily) ยท [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/LICENSE) ยท [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/TRADEMARK.md) ยท [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.38/docs/assets/readme/JetBrainsMono-OFL.txt)
|
|
63
63
|
|
|
64
64
|
<details>
|
|
65
65
|
<summary>Copy ASCII logo</summary>
|
package/dist/state.d.ts
CHANGED
package/dist/state.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createRuntimePaths(projectRoot) {
|
|
|
34
34
|
knowledgeDir: path.join(runtimeDir, "knowledge"),
|
|
35
35
|
knowledgeClaimsFile: path.join(runtimeDir, "knowledge", "claims.jsonl"),
|
|
36
36
|
autoHandoffFile: path.join(runtimeDir, "auto-handoff.json"),
|
|
37
|
+
noticesFile: path.join(runtimeDir, "notices.jsonl"),
|
|
37
38
|
workflowStackDir: path.join(runtimeDir, "workflow-stack"),
|
|
38
39
|
adhdDir: path.join(runtimeDir, "adhd"),
|
|
39
40
|
livenessDir: path.join(runtimeDir, "liveness"),
|
package/dist/tools.d.ts
CHANGED
|
@@ -10,34 +10,44 @@ export declare const litOpenCodeTools: Readonly<{
|
|
|
10
10
|
args: Readonly<{
|
|
11
11
|
[k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
12
12
|
}>;
|
|
13
|
-
execute(args:
|
|
13
|
+
execute(args: {
|
|
14
|
+
[x: string]: unknown;
|
|
15
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
|
|
14
16
|
};
|
|
15
17
|
litwork: {
|
|
16
18
|
description: string;
|
|
17
19
|
args: Readonly<{
|
|
18
20
|
[k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
19
21
|
}>;
|
|
20
|
-
execute(args:
|
|
22
|
+
execute(args: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
|
|
21
25
|
};
|
|
22
26
|
"start-work": {
|
|
23
27
|
description: string;
|
|
24
28
|
args: Readonly<{
|
|
25
29
|
[k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
26
30
|
}>;
|
|
27
|
-
execute(args:
|
|
31
|
+
execute(args: {
|
|
32
|
+
[x: string]: unknown;
|
|
33
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
|
|
28
34
|
};
|
|
29
35
|
"review-work": {
|
|
30
36
|
description: string;
|
|
31
37
|
args: Readonly<{
|
|
32
38
|
[k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
33
39
|
}>;
|
|
34
|
-
execute(args:
|
|
40
|
+
execute(args: {
|
|
41
|
+
[x: string]: unknown;
|
|
42
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
|
|
35
43
|
};
|
|
36
44
|
wikify: {
|
|
37
45
|
description: string;
|
|
38
46
|
args: Readonly<{
|
|
39
47
|
[k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
|
|
40
48
|
}>;
|
|
41
|
-
execute(args:
|
|
49
|
+
execute(args: {
|
|
50
|
+
[x: string]: unknown;
|
|
51
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<ToolResult>;
|
|
42
52
|
};
|
|
43
53
|
}>;
|
|
@@ -36,6 +36,13 @@ export type CardFigures = {
|
|
|
36
36
|
readonly branch?: string;
|
|
37
37
|
readonly inLane: boolean;
|
|
38
38
|
readonly restartTo?: string;
|
|
39
|
+
readonly limits?: readonly CardLimit[];
|
|
40
|
+
};
|
|
41
|
+
/** A plan window the host reports (`5h 42%`), shown after the context figure; absent when the host reports none. */
|
|
42
|
+
export type CardLimit = {
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly percent: number;
|
|
45
|
+
readonly limited: boolean;
|
|
39
46
|
};
|
|
40
47
|
export type CardInputs = {
|
|
41
48
|
readonly view: MissionView;
|
package/dist/tui/mission-band.js
CHANGED
|
@@ -150,6 +150,7 @@ function topLine(view, frame, palette, width, border, rest, mark) {
|
|
|
150
150
|
}
|
|
151
151
|
return fitLine([{ text: "โญ" + "โ".repeat(Math.max(0, width)), fg: border }], width, palette);
|
|
152
152
|
}
|
|
153
|
+
const limitWarnPercent = 90;
|
|
153
154
|
/** The figures in the bottom border, most important first; the last ones go first when room runs out. */
|
|
154
155
|
function figureParts(view, figures, palette) {
|
|
155
156
|
const parts = [];
|
|
@@ -159,6 +160,9 @@ function figureParts(view, figures, palette) {
|
|
|
159
160
|
const percent = Math.round(figures.contextPercent);
|
|
160
161
|
parts.push({ text: `ctx ${percent}%`, fg: percent >= contextWarnPercent ? palette.warning : palette.muted });
|
|
161
162
|
}
|
|
163
|
+
for (const limit of figures.limits ?? []) {
|
|
164
|
+
parts.push({ text: `${limit.label} ${limit.percent}%${limit.limited ? " limited" : ""}`, fg: limit.limited || limit.percent >= limitWarnPercent ? palette.warning : palette.muted });
|
|
165
|
+
}
|
|
162
166
|
if (view.active)
|
|
163
167
|
parts.push({ text: `โณ ${elapsedText(view.elapsedMinutes)}${figures.inLane ? " ยท in lane" : ""}`, fg: palette.muted });
|
|
164
168
|
if (figures.branch !== undefined)
|
|
@@ -25,7 +25,9 @@ export function readSkillUse(part, at) {
|
|
|
25
25
|
const input = isRecord(state.input) ? state.input : {};
|
|
26
26
|
const start = isRecord(state.time) && typeof state.time.start === "number" ? state.time.start : at;
|
|
27
27
|
if (part.tool === "skill") {
|
|
28
|
-
|
|
28
|
+
// OpenCode 1's skill tool takes `name`; OpenCode 2's takes `id`.
|
|
29
|
+
const skill = typeof input.name === "string" ? input.name : typeof input.id === "string" ? input.id : undefined;
|
|
30
|
+
return skill !== undefined && ownSkills.has(skill) ? { name: skill, at: start } : undefined;
|
|
29
31
|
}
|
|
30
32
|
const name = ownTools[part.tool];
|
|
31
33
|
if (name === undefined || input.action === "status")
|
|
@@ -15,16 +15,39 @@ type V2Message = {
|
|
|
15
15
|
readonly content?: ReadonlyArray<{
|
|
16
16
|
readonly type: string;
|
|
17
17
|
readonly name?: string;
|
|
18
|
+
readonly text?: string;
|
|
18
19
|
readonly state?: {
|
|
19
20
|
readonly status?: string;
|
|
21
|
+
readonly input?: unknown;
|
|
22
|
+
readonly metadata?: unknown;
|
|
20
23
|
};
|
|
21
24
|
readonly time?: {
|
|
22
25
|
readonly created?: number;
|
|
23
26
|
readonly ran?: number;
|
|
27
|
+
readonly completed?: number;
|
|
24
28
|
};
|
|
25
29
|
}>;
|
|
26
30
|
};
|
|
31
|
+
type V2Model = {
|
|
32
|
+
readonly providerID: string;
|
|
33
|
+
readonly modelID: string;
|
|
34
|
+
readonly limit?: {
|
|
35
|
+
readonly context?: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
27
38
|
export type MissionDataV2 = {
|
|
39
|
+
readonly location?: {
|
|
40
|
+
readonly vcs?: {
|
|
41
|
+
info(): {
|
|
42
|
+
readonly branch?: {
|
|
43
|
+
readonly current?: string;
|
|
44
|
+
};
|
|
45
|
+
} | undefined;
|
|
46
|
+
};
|
|
47
|
+
readonly model?: {
|
|
48
|
+
list(): ReadonlyArray<V2Model> | undefined;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
28
51
|
readonly session: {
|
|
29
52
|
status(sessionID: string): "idle" | "running";
|
|
30
53
|
readonly message: {
|
|
@@ -5,11 +5,23 @@ function isMessage(value) {
|
|
|
5
5
|
export function sessionMessagesV2(data, sessionID) {
|
|
6
6
|
return (data.session.message.list(sessionID) ?? []).filter(isMessage).filter((message) => message.type === "user" || message.type === "assistant");
|
|
7
7
|
}
|
|
8
|
+
/** The provider table the context figure reads: each model's context window, from the host's model list. */
|
|
9
|
+
function providersV2(data) {
|
|
10
|
+
const byProvider = new Map();
|
|
11
|
+
for (const model of data.location?.model?.list() ?? []) {
|
|
12
|
+
const models = byProvider.get(model.providerID) ?? {};
|
|
13
|
+
models[model.modelID] = { limit: { context: model.limit?.context } };
|
|
14
|
+
byProvider.set(model.providerID, models);
|
|
15
|
+
}
|
|
16
|
+
return [...byProvider].map(([id, models]) => ({ id, models }));
|
|
17
|
+
}
|
|
8
18
|
export function missionStateFromV2(data, directory) {
|
|
9
19
|
const index = new Map();
|
|
20
|
+
const branch = data.location?.vcs?.info()?.branch?.current?.trim();
|
|
10
21
|
return {
|
|
11
22
|
path: directory === undefined ? {} : { directory },
|
|
12
|
-
|
|
23
|
+
...(branch === undefined || branch === "" ? {} : { vcs: { branch } }),
|
|
24
|
+
provider: providersV2(data),
|
|
13
25
|
session: {
|
|
14
26
|
messages(sessionID) {
|
|
15
27
|
return sessionMessagesV2(data, sessionID).map((message) => {
|
|
@@ -35,9 +47,22 @@ export function missionStateFromV2(data, directory) {
|
|
|
35
47
|
return [];
|
|
36
48
|
if (message.type === "user")
|
|
37
49
|
return message.metadata === undefined ? [] : [{ metadata: message.metadata }];
|
|
38
|
-
return (message.content ?? [])
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
return (message.content ?? []).flatMap((item) => {
|
|
51
|
+
if (item.type === "text" && typeof item.text === "string")
|
|
52
|
+
return [{ type: "text", text: item.text }];
|
|
53
|
+
if (item.type !== "tool" || typeof item.name !== "string")
|
|
54
|
+
return [];
|
|
55
|
+
return [{
|
|
56
|
+
type: "tool",
|
|
57
|
+
tool: item.name,
|
|
58
|
+
state: {
|
|
59
|
+
status: item.state?.status,
|
|
60
|
+
input: item.state?.input,
|
|
61
|
+
metadata: item.state?.metadata,
|
|
62
|
+
time: { start: item.time?.ran ?? item.time?.created, ...(item.time?.completed === undefined ? {} : { end: item.time.completed }) }
|
|
63
|
+
}
|
|
64
|
+
}];
|
|
65
|
+
});
|
|
41
66
|
}
|
|
42
67
|
};
|
|
43
68
|
}
|
|
@@ -52,6 +52,7 @@ export type MissionStateSource = {
|
|
|
52
52
|
readonly tool?: unknown;
|
|
53
53
|
readonly state?: unknown;
|
|
54
54
|
readonly metadata?: unknown;
|
|
55
|
+
readonly text?: unknown;
|
|
55
56
|
}>;
|
|
56
57
|
};
|
|
57
58
|
/** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
|
|
@@ -330,7 +330,8 @@ function taskSources(state, sessionID, now) {
|
|
|
330
330
|
const assistants = state.session.messages(sessionID).filter((message) => message.role === "assistant").slice(-recentAssistantMessages);
|
|
331
331
|
for (const message of assistants) {
|
|
332
332
|
for (const part of state.part(message.id)) {
|
|
333
|
-
|
|
333
|
+
// OpenCode 1 delegates through `task`, OpenCode 2 through `subagent`.
|
|
334
|
+
if (part.type !== "tool" || (part.tool !== "task" && part.tool !== "subagent") || !isRecord(part.state))
|
|
334
335
|
continue;
|
|
335
336
|
const toolState = part.state;
|
|
336
337
|
const status = toolState.status;
|
|
@@ -348,7 +349,7 @@ function taskSources(state, sessionID, now) {
|
|
|
348
349
|
children.add(child);
|
|
349
350
|
const todos = child === undefined ? [] : state.session.todo(child).filter((todo) => todo.status !== "cancelled");
|
|
350
351
|
const done = todos.filter((todo) => todo.status === "completed").length;
|
|
351
|
-
const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : "subagent";
|
|
352
|
+
const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : typeof input.agent === "string" ? input.agent : "subagent";
|
|
352
353
|
// A running task last moved when its own session did; its start time alone would read a long, busy run as idle.
|
|
353
354
|
const moved = child === undefined ? undefined : lastActivity(state, child);
|
|
354
355
|
sources.push({
|
|
@@ -100,13 +100,13 @@ export declare function groupsWidth(groups: readonly ChipGroup[]): number;
|
|
|
100
100
|
export declare function fade(segment: Segment, alpha: number, palette: MissionPalette): Segment;
|
|
101
101
|
export declare const chipSparkWidth = 7;
|
|
102
102
|
export declare function chipSparkCells(t: number, palette: MissionPalette): Segment[] | undefined;
|
|
103
|
-
export declare function chipTier(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): ChipTier;
|
|
103
|
+
export declare function chipTier(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number, largest?: ChipTier): ChipTier;
|
|
104
104
|
/**
|
|
105
105
|
* The map chip on the right of the prompt's agent/model row: `๐ฅ LIT plan โ โบ WORK โฐโฐโฐโฑโฑโฑโฑ 3/7 โบ review`,
|
|
106
106
|
* then the live step when the full chip leaves room for it, shedding labels before values as `budget`
|
|
107
|
-
* shrinks
|
|
107
|
+
* shrinks, and never larger than `largest`. Idle sessions render nothing.
|
|
108
108
|
*/
|
|
109
|
-
export declare function chipLine(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): Line;
|
|
109
|
+
export declare function chipLine(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number, largest?: ChipTier): Line;
|
|
110
110
|
/** The header status: a glyph and words, or the COMPLETE badge that pops in and settles to green text. */
|
|
111
111
|
export declare function statusSegments(view: MissionView, frame: MotionFrame, palette: MissionPalette): {
|
|
112
112
|
readonly segments: Segment[];
|
package/dist/tui/mission-text.js
CHANGED
|
@@ -396,8 +396,8 @@ export function chipSparkCells(t, palette) {
|
|
|
396
396
|
}
|
|
397
397
|
return cells;
|
|
398
398
|
}
|
|
399
|
-
export function chipTier(view, frame, palette, budget) {
|
|
400
|
-
for (const tier of ["full", "medium"]) {
|
|
399
|
+
export function chipTier(view, frame, palette, budget, largest = "full") {
|
|
400
|
+
for (const tier of largest === "full" ? ["full", "medium"] : largest === "medium" ? ["medium"] : []) {
|
|
401
401
|
if (groupsWidth(chipGroups(view, frame, palette, tier)) <= budget)
|
|
402
402
|
return tier;
|
|
403
403
|
}
|
|
@@ -406,12 +406,12 @@ export function chipTier(view, frame, palette, budget) {
|
|
|
406
406
|
/**
|
|
407
407
|
* The map chip on the right of the prompt's agent/model row: `๐ฅ LIT plan โ โบ WORK โฐโฐโฐโฑโฑโฑโฑ 3/7 โบ review`,
|
|
408
408
|
* then the live step when the full chip leaves room for it, shedding labels before values as `budget`
|
|
409
|
-
* shrinks
|
|
409
|
+
* shrinks, and never larger than `largest`. Idle sessions render nothing.
|
|
410
410
|
*/
|
|
411
|
-
export function chipLine(view, frame, palette, budget) {
|
|
411
|
+
export function chipLine(view, frame, palette, budget, largest = "full") {
|
|
412
412
|
if (!view.active)
|
|
413
413
|
return [];
|
|
414
|
-
const tier = chipTier(view, frame, palette, budget);
|
|
414
|
+
const tier = chipTier(view, frame, palette, budget, largest);
|
|
415
415
|
const groups = chipGroups(view, frame, palette, tier);
|
|
416
416
|
if (tier === "full") {
|
|
417
417
|
const live = liveSegments(view, frame, palette, 24);
|
package/dist/v2/events.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type EventModelLike = {
|
|
|
36
36
|
};
|
|
37
37
|
export type V2EventOptions = {
|
|
38
38
|
readonly projectRoot: string;
|
|
39
|
-
readonly paths: Pick<RuntimePaths, "autoHandoffFile"
|
|
39
|
+
readonly paths: Pick<RuntimePaths, "autoHandoffFile"> & Partial<Pick<RuntimePaths, "noticesFile">>;
|
|
40
40
|
readonly autoHandoffConfig: AutoHandoffConfig;
|
|
41
41
|
readonly boundedAuthority: BoundedAuthorityOptions;
|
|
42
42
|
readonly getSession: SessionLookup;
|
package/dist/v2/events.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAutoHandoff } from "../auto-handoff-hooks.js";
|
|
2
2
|
import { LifecycleConflictError, LifecycleSafetyError, createBoundedAuthorityLifecycle, parseFencedProgress } from "../bounded-authority.js";
|
|
3
3
|
import { markLiveness } from "../session-liveness.js";
|
|
4
|
+
import { appendNotice } from "./notices.js";
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
7
|
}
|
|
@@ -9,10 +10,20 @@ function str(value) {
|
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* The calls the OpenCode 1 automatic handoff makes, answered by OpenCode 2. There is no server toast on
|
|
12
|
-
* OpenCode 2, so
|
|
13
|
+
* OpenCode 2, so `tui.showToast` leaves the notice in the project's notices file, which the TUI plugin
|
|
14
|
+
* reads and shows (notices.ts). Without a notices file the call is accepted and dropped.
|
|
13
15
|
*/
|
|
14
|
-
function handoffClient(session, model, statuses) {
|
|
16
|
+
function handoffClient(session, model, statuses, noticesFile) {
|
|
15
17
|
const client = {
|
|
18
|
+
tui: {
|
|
19
|
+
async showToast(input) {
|
|
20
|
+
const body = input.body;
|
|
21
|
+
if (noticesFile !== undefined && body !== undefined && typeof body.message === "string") {
|
|
22
|
+
appendNotice(noticesFile, { title: body.title ?? "๐ฅ LIT", message: body.message, variant: (body.variant ?? "info"), duration: body.duration ?? 8000 });
|
|
23
|
+
}
|
|
24
|
+
return { data: true };
|
|
25
|
+
}
|
|
26
|
+
},
|
|
16
27
|
session: {
|
|
17
28
|
async promptAsync(input) {
|
|
18
29
|
const text = [input.body.system, ...(input.body.parts ?? []).map((part) => part.text)].filter((part) => typeof part === "string" && part !== "").join("\n\n");
|
|
@@ -50,7 +61,7 @@ export function createV2Events(options) {
|
|
|
50
61
|
const autoHandoff = createAutoHandoff({
|
|
51
62
|
projectRoot: options.projectRoot,
|
|
52
63
|
paths: options.paths,
|
|
53
|
-
client: handoffClient(options.session, options.model, statuses),
|
|
64
|
+
client: handoffClient(options.session, options.model, statuses, options.paths.noticesFile),
|
|
54
65
|
config: options.autoHandoffConfig,
|
|
55
66
|
getSession: options.getSession
|
|
56
67
|
});
|
|
@@ -88,6 +99,11 @@ export function createV2Events(options) {
|
|
|
88
99
|
throw error;
|
|
89
100
|
}
|
|
90
101
|
}
|
|
102
|
+
async function idle(sessionID) {
|
|
103
|
+
statuses.set(sessionID, { type: "idle" });
|
|
104
|
+
await markLiveness(options.projectRoot, sessionID, "idle");
|
|
105
|
+
await toHandoff("session.idle", { sessionID });
|
|
106
|
+
}
|
|
91
107
|
async function handle(event) {
|
|
92
108
|
const data = isRecord(event.data) ? event.data : {};
|
|
93
109
|
const sessionID = str(data.sessionID);
|
|
@@ -135,13 +151,23 @@ export function createV2Events(options) {
|
|
|
135
151
|
});
|
|
136
152
|
return;
|
|
137
153
|
}
|
|
154
|
+
// OpenCode 2.2.5 publishes no `session.status` or `session.idle`. A turn runs from
|
|
155
|
+
// `session.execution.started` to one of three endings, and the handoff reads them as busy and idle.
|
|
156
|
+
case "session.execution.started":
|
|
157
|
+
statuses.set(sessionID, { type: "busy" });
|
|
158
|
+
await toHandoff("session.status", { sessionID, status: { type: "busy" } });
|
|
159
|
+
return;
|
|
160
|
+
case "session.execution.succeeded":
|
|
161
|
+
case "session.execution.failed":
|
|
162
|
+
case "session.execution.interrupted":
|
|
163
|
+
await idle(sessionID);
|
|
164
|
+
return;
|
|
138
165
|
case "session.status":
|
|
139
166
|
statuses.set(sessionID, data.status);
|
|
140
167
|
await toHandoff("session.status", { sessionID, status: data.status });
|
|
141
168
|
return;
|
|
142
169
|
case "session.idle":
|
|
143
|
-
await
|
|
144
|
-
await toHandoff("session.idle", { sessionID });
|
|
170
|
+
await idle(sessionID);
|
|
145
171
|
return;
|
|
146
172
|
case "session.compaction.ended":
|
|
147
173
|
await toHandoff("session.compacted", { sessionID });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type NoticeVariant = "info" | "success" | "warning" | "error";
|
|
2
|
+
export type TuiNotice = {
|
|
3
|
+
readonly title: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly variant: NoticeVariant;
|
|
6
|
+
readonly duration: number;
|
|
7
|
+
};
|
|
8
|
+
/** Leaves one popup for the TUI. Best effort: a place that cannot be written loses the notice, never the caller. */
|
|
9
|
+
export declare function appendNotice(file: string, notice: TuiNotice): void;
|
|
10
|
+
/** Reads the notices that appear after it was made. Each call returns the new ones once, oldest first. */
|
|
11
|
+
export declare function createNoticeTail(file: string): {
|
|
12
|
+
readonly poll: () => readonly TuiNotice[];
|
|
13
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The way a server-side popup reaches the screen on OpenCode 2. The server plugin's context only subscribes to
|
|
2
|
+
// events, so it cannot raise a toast; the TUI plugin can, but it knows nothing of the server's state machines
|
|
3
|
+
// (the automatic handoff). The server therefore appends each popup it would have shown to a small file under
|
|
4
|
+
// the project's .litopencode, and the TUI reads the lines that appear after it started and toasts them. Both
|
|
5
|
+
// halves ship in this package; neither needs the other to be there. A symbolic link in place of the file is
|
|
6
|
+
// never followed, a torn line waits for its newline, and the file is rewritten to its newest lines past a cap.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
const variants = new Set(["info", "success", "warning", "error"]);
|
|
10
|
+
const titleCells = 80;
|
|
11
|
+
const messageCells = 400;
|
|
12
|
+
const minDuration = 1000;
|
|
13
|
+
const maxDuration = 15_000;
|
|
14
|
+
const defaultDuration = 8000;
|
|
15
|
+
const capBytes = 64_000;
|
|
16
|
+
const keepBytes = 24_000;
|
|
17
|
+
const readCapBytes = 128_000;
|
|
18
|
+
const seenLimit = 64;
|
|
19
|
+
let counter = 0;
|
|
20
|
+
function clip(value, max) {
|
|
21
|
+
const chars = [...value];
|
|
22
|
+
return chars.length <= max ? value : `${chars.slice(0, max - 1).join("")}โฆ`;
|
|
23
|
+
}
|
|
24
|
+
function clean(value) {
|
|
25
|
+
if (typeof value !== "object" || value === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
const entry = value;
|
|
28
|
+
if (typeof entry.title !== "string" || typeof entry.message !== "string" || entry.message === "")
|
|
29
|
+
return undefined;
|
|
30
|
+
if (typeof entry.variant !== "string" || !variants.has(entry.variant))
|
|
31
|
+
return undefined;
|
|
32
|
+
const duration = typeof entry.duration === "number" && Number.isFinite(entry.duration) ? Math.min(maxDuration, Math.max(minDuration, entry.duration)) : defaultDuration;
|
|
33
|
+
return { title: clip(entry.title, titleCells), message: clip(entry.message, messageCells), variant: entry.variant, duration };
|
|
34
|
+
}
|
|
35
|
+
function plainFile(file) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.lstatSync(file);
|
|
38
|
+
return stat.isFile() ? stat : undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function trimToNewest(file) {
|
|
45
|
+
const text = fs.readFileSync(file, "utf8");
|
|
46
|
+
const lines = text.split("\n").filter((line) => line !== "");
|
|
47
|
+
const kept = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
50
|
+
size += Buffer.byteLength(lines[index]) + 1;
|
|
51
|
+
if (size > keepBytes)
|
|
52
|
+
break;
|
|
53
|
+
kept.unshift(lines[index]);
|
|
54
|
+
}
|
|
55
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
56
|
+
fs.writeFileSync(temp, kept.length === 0 ? "" : `${kept.join("\n")}\n`);
|
|
57
|
+
fs.renameSync(temp, file);
|
|
58
|
+
}
|
|
59
|
+
/** Leaves one popup for the TUI. Best effort: a place that cannot be written loses the notice, never the caller. */
|
|
60
|
+
export function appendNotice(file, notice) {
|
|
61
|
+
const cleaned = clean(notice);
|
|
62
|
+
if (cleaned === undefined)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
const existing = (() => {
|
|
67
|
+
try {
|
|
68
|
+
return fs.lstatSync(file);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
if (existing !== undefined && !existing.isFile())
|
|
75
|
+
return;
|
|
76
|
+
counter += 1;
|
|
77
|
+
const stored = { id: `${Date.now().toString(36)}-${process.pid}-${counter}`, at: Date.now(), ...cleaned };
|
|
78
|
+
fs.appendFileSync(file, `${JSON.stringify(stored)}\n`);
|
|
79
|
+
if ((plainFile(file)?.size ?? 0) > capBytes)
|
|
80
|
+
trimToNewest(file);
|
|
81
|
+
}
|
|
82
|
+
catch { // no-excuse-ok: catch -- a popup is best-effort feedback; the handoff itself must never stop for it.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Reads the notices that appear after it was made. Each call returns the new ones once, oldest first. */
|
|
86
|
+
export function createNoticeTail(file) {
|
|
87
|
+
let offset = plainFile(file)?.size ?? 0;
|
|
88
|
+
const seen = [];
|
|
89
|
+
return {
|
|
90
|
+
poll() {
|
|
91
|
+
const stat = plainFile(file);
|
|
92
|
+
if (stat === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
// A smaller file was rewritten to its newest lines; read it again and let the ids drop what was shown.
|
|
95
|
+
if (stat.size < offset)
|
|
96
|
+
offset = 0;
|
|
97
|
+
if (stat.size === offset)
|
|
98
|
+
return [];
|
|
99
|
+
const length = Math.min(stat.size - offset, readCapBytes);
|
|
100
|
+
const buffer = Buffer.alloc(length);
|
|
101
|
+
let fd;
|
|
102
|
+
try {
|
|
103
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
104
|
+
fs.readSync(fd, buffer, 0, length, offset);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (fd !== undefined)
|
|
111
|
+
fs.closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
const text = buffer.toString("utf8");
|
|
114
|
+
const end = text.lastIndexOf("\n");
|
|
115
|
+
if (end < 0)
|
|
116
|
+
return length === readCapBytes ? (offset += length, []) : [];
|
|
117
|
+
offset += Buffer.byteLength(text.slice(0, end + 1));
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const line of text.slice(0, end).split("\n")) {
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(line);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const id = typeof parsed?.id === "string" ? parsed.id : undefined;
|
|
128
|
+
if (id !== undefined && seen.includes(id))
|
|
129
|
+
continue;
|
|
130
|
+
const entry = clean(parsed);
|
|
131
|
+
if (entry === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
if (id !== undefined) {
|
|
134
|
+
seen.push(id);
|
|
135
|
+
if (seen.length > seenLimit)
|
|
136
|
+
seen.shift();
|
|
137
|
+
}
|
|
138
|
+
out.push(entry);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|