@litfamily/litopencode 1.0.13 → 1.0.15
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 +11 -0
- package/README-Ko-KR.md +11 -15
- package/README.md +11 -15
- package/dist/activation.d.ts +2 -0
- package/dist/activation.js +20 -0
- package/dist/auto-handoff-hooks.d.ts +31 -0
- package/dist/auto-handoff-hooks.js +313 -0
- package/dist/auto-handoff.d.ts +73 -0
- package/dist/auto-handoff.js +322 -0
- package/dist/cli/doctor.js +21 -1
- package/dist/cli/host-limits.d.ts +5 -0
- package/dist/cli/host-limits.js +12 -0
- package/dist/cli/model-catalog.d.ts +10 -1
- package/dist/cli/model-catalog.js +9 -2
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +22 -0
- package/dist/config-parser.js +23 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +5 -0
- package/dist/features.js +25 -2
- package/dist/index.d.ts +1 -1
- package/dist/model-route-policy.js +2 -2
- package/dist/server.js +12 -0
- package/dist/skills.js +3 -2
- package/dist/state.d.ts +1 -0
- package/dist/state.js +1 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +39 -2
- package/docs/reference.md +39 -2
- package/package.json +1 -1
- package/skills/lit-handoff/SKILL.md +30 -1
- package/tools/check-pack-payload.mjs +2 -2
- package/docs/ab/S11/baseline-desktop.webp +0 -0
- package/docs/ab/S11/baseline-phone.webp +0 -0
- package/docs/ab/S11/lit-desktop.webp +0 -0
- package/docs/ab/S11/lit-phone.webp +0 -0
- package/docs/ab/S3/baseline-desktop.webp +0 -0
- package/docs/ab/S3/baseline-phone.webp +0 -0
- package/docs/ab/S3/lit-desktop.webp +0 -0
- package/docs/ab/S3/lit-phone.webp +0 -0
- package/docs/ab/S4/baseline-desktop.webp +0 -0
- package/docs/ab/S4/baseline-phone.webp +0 -0
- package/docs/ab/S4/lit-desktop.webp +0 -0
- package/docs/ab/S4/lit-phone.webp +0 -0
- package/docs/ab/S5/lit-slides.webp +0 -0
- package/docs/ab/S7/lit-diagram.webp +0 -0
- package/docs/ab/S8/baseline-slides.webp +0 -0
- package/docs/ab/S8/lit-slides.webp +0 -0
- package/docs/ab/S9/lit-pages.webp +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ are tracked separately from this product history.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 1.0.15 - 2026-09-30
|
|
9
|
+
|
|
10
|
+
- `gpt-6.1-sol` is now the recommended coding-lead alternative, because OpenAI lists `gpt-6-sol` as the previous generation. The installer model menu offers it as the coding lead and keeps `gpt-6-sol` listed as the previous generation, and the reference describes both. Routes that already use `gpt-6-sol` keep working, and `litopencode doctor` still accepts them.
|
|
11
|
+
|
|
12
|
+
## 1.0.14 - 2026-09-30
|
|
13
|
+
|
|
14
|
+
- Add an optional automatic handoff. It is off until you turn it on, and you choose the context percent; LitOpenCode has no default percent. When the conversation reaches your percent and the turn ends, LitOpenCode asks the model to write a handoff, checks that the file was saved, compacts the conversation, and puts the start of the handoff back in front of the model once. On OpenCode every one of those steps runs by itself. If OpenCode cannot start the compaction, a toast says "Handoff saved. Run /compact now."
|
|
15
|
+
- Turn automatic handoff on with `lit-handoff auto on 70` (use your own number), or with `LITOPENCODE_AUTO_HANDOFF=1` and `LITOPENCODE_AUTO_HANDOFF_PERCENT`. `lit-handoff auto status` shows where it stands, and `litopencode doctor` warns when your percent is at or above the point where OpenCode compacts on its own.
|
|
16
|
+
- The README no longer shows the A/B comparison; one run per side was too little to support its verdicts. The same section is gone from the npm page.
|
|
17
|
+
- The GitHub pages (English and Korean) now show terminal pictures of what LitOpenCode prints, and the motion film was remade in Pretendard with a Korean version.
|
|
18
|
+
|
|
8
19
|
## 1.0.13 - 2026-09-30
|
|
9
20
|
|
|
10
21
|
- The GitHub pages (English and Korean) now show what Jev looks like: the `litopencode doctor` line and the toasts when Jev is off, on, or missing its key. Jev stays off unless you turn it on, and the reference lists what each toast says.
|
package/README-Ko-KR.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.15/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitOpenCode 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
3
|
+
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/ascii-readme.svg" width="480" alt="LIT ASCII B 마크" /></p>
|
|
4
4
|
|
|
5
5
|
<details>
|
|
6
6
|
<summary>ASCII 로고 복사</summary>
|
|
@@ -32,21 +32,21 @@
|
|
|
32
32
|
|
|
33
33
|
# LitOpenCode
|
|
34
34
|
|
|
35
|
-
[English](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
35
|
+
[English](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/README.md) · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/README-Ko-KR.md)
|
|
36
36
|
|
|
37
37
|
> **불씨를 건네받았다.**<br>
|
|
38
38
|
> **이제, 당신의 작업에 옮길 차례다.**
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
42
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
41
|
+
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/badge-version.svg" alt="1.0.15" /></a>
|
|
42
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
45
|
LitOpenCode는 OpenCode에 워크플로 agent, 슬래시 명령, 로컬 근거 기록을 더합니다.
|
|
46
46
|
|
|
47
47
|
프롬프트에 `lit`을 붙이면 계획하고, 만들고, 확인한 뒤 다음 할 일을 프로젝트에 적어 둡니다. 그래서 다음 세션이 멈춘 자리에서 바로 이어 갈 수 있습니다. 모델을 돌리고 권한을 묻는 일은 지금처럼 OpenCode가 합니다.
|
|
48
48
|
|
|
49
|
-
**[전체
|
|
49
|
+
**[전체 안내와 스킬 갤러리는 GitHub에서 →](https://github.com/wjgoarxiv/litopencode/blob/master/README-Ko-KR.md)**
|
|
50
50
|
|
|
51
51
|
## 설치
|
|
52
52
|
|
|
@@ -88,10 +88,6 @@ lit 외부 의존성 없이 index.html 하나로 할 일 목록을 만들고,
|
|
|
88
88
|
|
|
89
89
|
이 밖에도 디버깅, 리팩터링, 코드 검토, 조사 스킬이 있고, 워드 보고서(`lit-docx`)와 파워포인트 발표자료(`lit-pptx`), 다이어그램, 과학 그림, 화면 구현, README, 글 다듬기(`/lit-humanizer`) 스킬도 함께 설치됩니다. 스킬마다 무엇을 만드는지는 그림과 함께 [GitHub 스킬 갤러리](https://github.com/wjgoarxiv/litopencode/blob/master/README-Ko-KR.md#스킬-한눈에-보기)에 있습니다.
|
|
90
90
|
|
|
91
|
-
## 비교해 보니
|
|
92
|
-
|
|
93
|
-
가볍게 던지는 한국어 프롬프트 열 개를 순정 OpenCode와 LitOpenCode에 똑같이 보내고, LitOpenCode 쪽 문장 끝에만 ` lit`을 붙였습니다. 최종 판정은 열 개 모두 LitOpenCode 승이었습니다. 아홉 개는 메인테이너가 두 결과를 나란히 보고 판정했고, 메인테이너가 눈으로 검토하지 않은 한 개는 블라인드 심사 결과를 그대로 썼습니다. 다만 제품 표식을 지우고 비교한 블라인드 심사만 따로 보면 LitOpenCode는 4승 3무 3패였습니다. 과제, 두 판정, 양쪽 화면은 [GitHub의 A/B 결과](https://github.com/wjgoarxiv/litopencode/blob/master/README-Ko-KR.md#ab-결과)에 있습니다.
|
|
94
|
-
|
|
95
91
|
## 설치하면 바뀌는 것
|
|
96
92
|
|
|
97
93
|
많지 않습니다. OpenCode 설정에 플러그인을 등록하고, LitOpenCode의 명령과 스킬 파일을 OpenCode 설정 폴더에 복사합니다. 어떤 일에 어떤 모델을 쓸지 정한 route는 `~/.config/opencode/litopencode.json`에 저장됩니다. `XDG_CONFIG_HOME`을 지정하면 설정 폴더가 그쪽으로 옮겨 가고, 이미 손봐 둔 route는 건드리지 않습니다. 작업 기록은 프로젝트마다 `.litopencode/litgoal/`에 쌓이고, 다음 세션은 이 기록을 읽고 이어 갑니다.
|
|
@@ -103,7 +99,7 @@ OpenAI 제공자를 쓰면 새로 설치했을 때 계획과 검토는 GPT-6 Ast
|
|
|
103
99
|
- planner는 계획만 세웁니다. `lit-plan`은 `edit`, `bash`, `task` 권한이 거부되어 있고, 더 느슨한 `balanced`나 `yolo` 모드를 직접 골라도 그대로입니다.
|
|
104
100
|
- LitOpenCode는 대화형으로 시작할 때와 install이나 doctor가 성공한 뒤에 스스로 업데이트할 수 있습니다. 끄려면 `--no-auto-update`를 붙이거나 `LITOPENCODE_NO_AUTO_UPDATE=1`을 설정하세요.
|
|
105
101
|
- 스킬 학습 기능은 제거되었습니다. 이전 릴리스가 프로젝트의 `.litopencode` 폴더에 남긴 학습 기록은 더 이상 쓰이지 않으니, 남겨 두든 지우든 편한 대로 하면 됩니다.
|
|
106
|
-
- Jev 스킬 힌트는 선택 기능이고 기본값은 꺼짐입니다. 켜면 조건에 맞는 프롬프트가 TypeSafe로 전송되니, 먼저 [Jev 스킬 힌트 참조 문서](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
102
|
+
- Jev 스킬 힌트는 선택 기능이고 기본값은 꺼짐입니다. 켜면 조건에 맞는 프롬프트가 TypeSafe로 전송되니, 먼저 [Jev 스킬 힌트 참조 문서](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference-Ko-KR.md#jev-스킬-힌트-선택)를 읽어 보세요. 알림과 doctor 줄이 어떻게 보이는지는 [GitHub 페이지](https://github.com/wjgoarxiv/litopencode/blob/master/README-Ko-KR.md#화면에서-보이는-것)에서 볼 수 있습니다.
|
|
107
103
|
- 뭔가 이상하면 `npm exec --package @litfamily/litopencode@latest -- litopencode doctor`로 패키지와 설정 상태를 확인하세요.
|
|
108
104
|
|
|
109
105
|
## 제거
|
|
@@ -114,11 +110,11 @@ OpenAI 제공자를 쓰면 새로 설치했을 때 계획과 검토는 GPT-6 Ast
|
|
|
114
110
|
npm uninstall -g @litfamily/litopencode
|
|
115
111
|
```
|
|
116
112
|
|
|
117
|
-
남겨 둘 만한 설치 파일과 기록은 [제거 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
113
|
+
남겨 둘 만한 설치 파일과 기록은 [제거 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference-Ko-KR.md#제거)에 정리되어 있습니다.
|
|
118
114
|
|
|
119
115
|
## 더 보기
|
|
120
116
|
|
|
121
117
|
- [GitHub 전체 안내](https://github.com/wjgoarxiv/litopencode/blob/master/README-Ko-KR.md)
|
|
122
|
-
- [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
123
|
-
- [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
124
|
-
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
118
|
+
- [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference.md)
|
|
119
|
+
- [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/privacy.md)
|
|
120
|
+
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/LICENSE) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/JetBrainsMono-OFL.txt)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.15/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five 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
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
3
|
+
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/ascii-readme.svg" width="480" alt="LIT ASCII B mark" /></p>
|
|
4
4
|
|
|
5
5
|
<details>
|
|
6
6
|
<summary>Copy ASCII logo</summary>
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
|
|
33
33
|
# LitOpenCode
|
|
34
34
|
|
|
35
|
-
[English](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
35
|
+
[English](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/README.md) · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/README-Ko-KR.md)
|
|
36
36
|
|
|
37
37
|
**Keep the work lit.**
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
41
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
40
|
+
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/badge-version.svg" alt="1.0.15" /></a>
|
|
41
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
42
42
|
</p>
|
|
43
43
|
|
|
44
44
|
LitOpenCode adds workflow agents, slash commands, and a local evidence ledger to OpenCode.
|
|
45
45
|
|
|
46
46
|
Add `lit` to a prompt and it plans, builds, checks, and writes down the next step in your project, so a later session can pick up where this one stopped. OpenCode still runs the model and asks for permission the way it always has.
|
|
47
47
|
|
|
48
|
-
**[Full guide
|
|
48
|
+
**[Full guide and skills gallery on GitHub →](https://github.com/wjgoarxiv/litopencode#readme)**
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
@@ -87,10 +87,6 @@ You get an `index.html` to open and try. Next time, `/lit-recap` shows where you
|
|
|
87
87
|
|
|
88
88
|
Beyond these, the package installs skills for debugging, refactoring, code review, research, Word reports (`lit-docx`), PowerPoint decks (`lit-pptx`), diagrams, scientific figures, interfaces, READMEs, and prose editing (`/lit-humanizer`). Each one, with a picture of what it produces, is in the [skills gallery on GitHub](https://github.com/wjgoarxiv/litopencode#skills-at-a-glance).
|
|
89
89
|
|
|
90
|
-
## How it compared
|
|
91
|
-
|
|
92
|
-
Ten casual Korean prompts went to plain OpenCode and to LitOpenCode; the LitOpenCode side only added ` lit` to the same line. The final verdict went to LitOpenCode on all ten. For nine of them that was the maintainer's call after comparing both outputs side by side; the one the maintainer didn't review by eye keeps the blind judge's verdict. The blind judge on its own, comparing outputs with product markings removed, scored LitOpenCode 4 wins, 3 ties, and 3 losses. The tasks, both verdicts, and screenshots of each side are in the [A/B results on GitHub](https://github.com/wjgoarxiv/litopencode#ab-results).
|
|
93
|
-
|
|
94
90
|
## What install changes
|
|
95
91
|
|
|
96
92
|
Not much. Install adds the plugin to OpenCode's config and copies LitOpenCode's commands and skills into your OpenCode config folder. Your routes, which say which model handles each kind of work, are kept in `~/.config/opencode/litopencode.json`. Setting `XDG_CONFIG_HOME` moves that folder, and routes you've already customized are left as they are. Each project keeps its work record in `.litopencode/litgoal/`, and that record is what a later session reads to carry on.
|
|
@@ -102,7 +98,7 @@ With the OpenAI provider, a fresh install uses GPT-6 Astra (`gpt-6-astra`) at `x
|
|
|
102
98
|
- The planner only plans. `lit-plan` has `edit`, `bash` and `task` denied, and it stays that way even if you choose the looser `balanced` or `yolo` mode.
|
|
103
99
|
- LitOpenCode can update itself when it starts interactively and after an install or doctor run succeeds. To turn that off, pass `--no-auto-update` or set `LITOPENCODE_NO_AUTO_UPDATE=1`.
|
|
104
100
|
- Skill learning has been removed. Learning records an earlier release left in a project's `.litopencode` folder are no longer used; keep or delete them as you like.
|
|
105
|
-
- The Jev skill hint is optional and off by default. Turning it on sends eligible prompts to TypeSafe, so read the [Jev skill hint reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
101
|
+
- The Jev skill hint is optional and off by default. Turning it on sends eligible prompts to TypeSafe, so read the [Jev skill hint reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference.md#jev-skill-hint-optional) first. The GitHub page shows [what the toast and the doctor line look like](https://github.com/wjgoarxiv/litopencode#what-you-will-see).
|
|
106
102
|
- If something seems off, `npm exec --package @litfamily/litopencode@latest -- litopencode doctor` checks the package and your configuration.
|
|
107
103
|
|
|
108
104
|
## Uninstall
|
|
@@ -113,11 +109,11 @@ There's no uninstall command. Remove the `@litfamily/litopencode` entry from the
|
|
|
113
109
|
npm uninstall -g @litfamily/litopencode
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
The [removal reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
112
|
+
The [removal reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference.md#remove) covers the installed files and records you may want to keep.
|
|
117
113
|
|
|
118
114
|
## More
|
|
119
115
|
|
|
120
116
|
- [Full guide on GitHub](https://github.com/wjgoarxiv/litopencode#readme)
|
|
121
|
-
- [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
122
|
-
- [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
123
|
-
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.
|
|
117
|
+
- [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference.md) · [한국어 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/reference-Ko-KR.md)
|
|
118
|
+
- [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/privacy.md)
|
|
119
|
+
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/LICENSE) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.15/docs/assets/readme/JetBrainsMono-OFL.txt)
|
package/dist/activation.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
2
|
import type { SessionLookup } from "./session-lineage.ts";
|
|
3
3
|
import type { BoundedAuthorityOptions } from "./bounded-authority.ts";
|
|
4
|
+
import { type AutoHandoffRouteMode } from "./auto-handoff.ts";
|
|
4
5
|
import { type JevSkillHintPart } from "./jev-skill-hint.ts";
|
|
5
6
|
export { litHandoffPromptInjection, scientificVisualizationPromptInjection } from "./activation-managed-prompts.ts";
|
|
6
7
|
export { litActivationBanner } from "./activation-prompt-utils.ts";
|
|
@@ -21,6 +22,7 @@ export type ChatMessageActivationOptions = {
|
|
|
21
22
|
readonly hostArgv?: readonly string[];
|
|
22
23
|
readonly boundedAuthority?: BoundedAuthorityOptions;
|
|
23
24
|
readonly knowledgeCaptureEnabled?: boolean;
|
|
25
|
+
readonly autoHandoffRoute?: (text: string, mode: AutoHandoffRouteMode) => Promise<string | undefined>;
|
|
24
26
|
readonly onScientificVisualizationActivation?: (sessionID: string) => void;
|
|
25
27
|
readonly onRootUserTurn?: (sessionID: string) => void | Promise<void>;
|
|
26
28
|
readonly onSkillActivation?: (sessionID: string, skillId: string, appendedLit?: boolean) => void | Promise<void>;
|
package/dist/activation.js
CHANGED
|
@@ -3,6 +3,7 @@ import { detectChatActivationMode, endsWithStandaloneLitInvocation, unwrapOpenCo
|
|
|
3
3
|
import { createLitGoalOperations } from "./ledger.js";
|
|
4
4
|
import { applyTrustedStartWorkDirective, parseStartWorkLifecycleDirective } from "./bounded-authority-hooks.js";
|
|
5
5
|
import { startWorkPromptInjection } from "./activation-workflow-prompts.js";
|
|
6
|
+
import { autoHandoffSettingText } from "./auto-handoff.js";
|
|
6
7
|
import { isReadOnlyWorkflowFamilyPlan } from "./workflow-families.js";
|
|
7
8
|
import { queryKnowledge } from "./knowledge.js";
|
|
8
9
|
import { activationDiscipline } from "./activation-probe.js";
|
|
@@ -105,6 +106,25 @@ export function createChatMessageActivationHook(projectRoot, options = {}) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
const nonEmptyTextParts = output.parts.filter((part) => part.type === "text" && part.text.trim().length > 0).map((part) => ({ ...part, text: unwrapOpenCodeRunMessage(part.text, options.hostArgv ?? process.argv) }));
|
|
109
|
+
if (!childSession &&
|
|
110
|
+
options.autoHandoffRoute !== undefined &&
|
|
111
|
+
nonEmptyTextParts.length === 1 &&
|
|
112
|
+
output.parts.every((part) => part.type === "text")) {
|
|
113
|
+
const reply = await options.autoHandoffRoute(nonEmptyTextParts[0].text, "chat");
|
|
114
|
+
if (reply !== undefined) {
|
|
115
|
+
output.parts.push({
|
|
116
|
+
id: `prt_litopencode_lit_handoff_auto_setting_${idSuffix(input.messageID ?? output.message.id)}`,
|
|
117
|
+
sessionID: input.sessionID,
|
|
118
|
+
messageID: input.messageID ?? output.message.id,
|
|
119
|
+
type: "text",
|
|
120
|
+
text: autoHandoffSettingText(reply),
|
|
121
|
+
// Sent to the model like any text part, but not shown or copied as the user's own words.
|
|
122
|
+
synthetic: true,
|
|
123
|
+
metadata: { litopencodeAutoHandoff: { surface: "chat.message" } }
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
const exactBareMode = output.parts.every((part) => part.type === "text") &&
|
|
109
129
|
nonEmptyTextParts.length === 1 &&
|
|
110
130
|
detectChatActivationMode(nonEmptyTextParts[0].text, agent);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { type AutoHandoffRouteMode, type AutoHandoffResolution } from "./auto-handoff.ts";
|
|
3
|
+
import type { AutoHandoffConfig } from "./config.ts";
|
|
4
|
+
import { type SessionLookup } from "./session-lineage.ts";
|
|
5
|
+
import type { RuntimePaths } from "./state.ts";
|
|
6
|
+
type OpenCodeClient = PluginInput["client"];
|
|
7
|
+
type EventHook = NonNullable<Hooks["event"]>;
|
|
8
|
+
type SystemTransformHook = NonNullable<Hooks["experimental.chat.system.transform"]>;
|
|
9
|
+
export declare const compactReminder = "Handoff saved. Run /compact now.";
|
|
10
|
+
export type AutoHandoffOptions = {
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
readonly paths: Pick<RuntimePaths, "autoHandoffFile">;
|
|
13
|
+
readonly client: OpenCodeClient | undefined;
|
|
14
|
+
readonly config: AutoHandoffConfig;
|
|
15
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
16
|
+
readonly getSession?: SessionLookup;
|
|
17
|
+
};
|
|
18
|
+
export type AutoHandoff = {
|
|
19
|
+
readonly event: EventHook;
|
|
20
|
+
readonly systemTransform: SystemTransformHook;
|
|
21
|
+
readonly route: (text: string, mode: AutoHandoffRouteMode) => Promise<string | undefined>;
|
|
22
|
+
readonly resolve: () => Promise<AutoHandoffResolution>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Opt-in automatic handoff for OpenCode. The plugin watches each assistant message's token count,
|
|
26
|
+
* waits for the turn to end once the user's percent is reached, asks the model to save a handoff
|
|
27
|
+
* that carries a session-bound marker, compacts the session itself after a verified handoff, and
|
|
28
|
+
* hands the handoff back through the system prompt once, after the compaction.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createAutoHandoff(options: AutoHandoffOptions): AutoHandoff;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { litHandoffPromptInjection } from "./activation-managed-prompts.js";
|
|
2
|
+
import { assistantTokenTotal, autoHandoffDirective, autoHandoffMarker, createAutoHandoffSettings, handoffDigestText, readFreshHandoff } from "./auto-handoff.js";
|
|
3
|
+
import { createSessionLookup } from "./session-lineage.js";
|
|
4
|
+
export const compactReminder = "Handoff saved. Run /compact now.";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function failed(result) {
|
|
9
|
+
return isRecord(result) && result.error !== undefined && result.error !== null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Opt-in automatic handoff for OpenCode. The plugin watches each assistant message's token count,
|
|
13
|
+
* waits for the turn to end once the user's percent is reached, asks the model to save a handoff
|
|
14
|
+
* that carries a session-bound marker, compacts the session itself after a verified handoff, and
|
|
15
|
+
* hands the handoff back through the system prompt once, after the compaction.
|
|
16
|
+
*/
|
|
17
|
+
export function createAutoHandoff(options) {
|
|
18
|
+
const settings = createAutoHandoffSettings({
|
|
19
|
+
stateFile: options.paths.autoHandoffFile,
|
|
20
|
+
config: options.config,
|
|
21
|
+
...(options.env === undefined ? {} : { env: options.env })
|
|
22
|
+
});
|
|
23
|
+
const client = options.client;
|
|
24
|
+
const getSession = options.getSession ?? createSessionLookup(client);
|
|
25
|
+
const directory = options.projectRoot;
|
|
26
|
+
const sessions = new Map();
|
|
27
|
+
const windows = new Map();
|
|
28
|
+
function stateFor(sessionID) {
|
|
29
|
+
let state = sessions.get(sessionID);
|
|
30
|
+
if (state === undefined) {
|
|
31
|
+
state = {
|
|
32
|
+
phase: "armed",
|
|
33
|
+
root: undefined,
|
|
34
|
+
model: undefined,
|
|
35
|
+
observedPercent: 0,
|
|
36
|
+
sentAt: undefined,
|
|
37
|
+
marker: undefined,
|
|
38
|
+
sawActivity: false,
|
|
39
|
+
digestArmed: false
|
|
40
|
+
};
|
|
41
|
+
sessions.set(sessionID, state);
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
function toast(message, variant = "info") {
|
|
46
|
+
try {
|
|
47
|
+
const pending = client?.tui?.showToast({ body: { title: "LitOpenCode auto-handoff", message, variant, duration: 8000 } });
|
|
48
|
+
void pending?.catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
catch { // no-excuse-ok: catch -- a missing or failing TUI must never stop the handoff.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function isRootSession(state, sessionID) {
|
|
54
|
+
if (state.root !== undefined)
|
|
55
|
+
return state.root;
|
|
56
|
+
if (getSession === undefined)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
const session = await getSession(sessionID);
|
|
60
|
+
if (session === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
state.root = session.parentID === undefined;
|
|
63
|
+
return state.root;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function contextWindow(providerID, modelID) {
|
|
70
|
+
const key = `${providerID}/${modelID}`;
|
|
71
|
+
const known = windows.get(key);
|
|
72
|
+
if (known !== undefined)
|
|
73
|
+
return known;
|
|
74
|
+
if (client?.config?.providers === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
try {
|
|
77
|
+
const result = await client.config.providers({ query: { directory } });
|
|
78
|
+
const data = isRecord(result) ? result.data : undefined;
|
|
79
|
+
const providers = isRecord(data) && Array.isArray(data.providers) ? data.providers : [];
|
|
80
|
+
for (const provider of providers) {
|
|
81
|
+
if (!isRecord(provider) || provider.id !== providerID || !isRecord(provider.models))
|
|
82
|
+
continue;
|
|
83
|
+
const model = provider.models[modelID];
|
|
84
|
+
const limit = isRecord(model) && isRecord(model.limit) ? model.limit.context : undefined;
|
|
85
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
|
|
86
|
+
windows.set(key, limit);
|
|
87
|
+
return limit;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { // no-excuse-ok: catch -- an unknown window only means no percent and no action.
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
async function onMessage(info) {
|
|
96
|
+
if (!isRecord(info) || info.role !== "assistant" || typeof info.sessionID !== "string")
|
|
97
|
+
return;
|
|
98
|
+
const sessionID = info.sessionID;
|
|
99
|
+
const state = stateFor(sessionID);
|
|
100
|
+
const created = isRecord(info.time) ? info.time.created : undefined;
|
|
101
|
+
if (state.phase === "sent" && state.sentAt !== undefined && typeof created === "number" && created >= state.sentAt) {
|
|
102
|
+
state.sawActivity = true;
|
|
103
|
+
}
|
|
104
|
+
if (info.summary === true || (info.error !== undefined && info.error !== null))
|
|
105
|
+
return;
|
|
106
|
+
const tokens = assistantTokenTotal(info);
|
|
107
|
+
if (tokens === 0 || typeof info.providerID !== "string" || typeof info.modelID !== "string")
|
|
108
|
+
return;
|
|
109
|
+
state.model = {
|
|
110
|
+
providerID: info.providerID,
|
|
111
|
+
modelID: info.modelID,
|
|
112
|
+
agent: typeof info.mode === "string" && info.mode !== "" ? info.mode : undefined
|
|
113
|
+
};
|
|
114
|
+
const resolved = await settings.resolve();
|
|
115
|
+
if (!resolved.enabled || resolved.percent === null)
|
|
116
|
+
return;
|
|
117
|
+
if (!(await isRootSession(state, sessionID)))
|
|
118
|
+
return;
|
|
119
|
+
const window = await contextWindow(info.providerID, info.modelID);
|
|
120
|
+
if (window === undefined)
|
|
121
|
+
return;
|
|
122
|
+
const reached = tokens * 100 >= resolved.percent * window;
|
|
123
|
+
if (state.phase === "done" && !reached)
|
|
124
|
+
state.phase = "armed";
|
|
125
|
+
if (state.phase === "armed" && reached) {
|
|
126
|
+
state.phase = "crossed";
|
|
127
|
+
state.observedPercent = Math.floor((tokens * 100) / window);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function sendHandoffRequest(sessionID, state) {
|
|
131
|
+
const sentAt = Date.now();
|
|
132
|
+
state.phase = "sent";
|
|
133
|
+
state.sentAt = sentAt;
|
|
134
|
+
state.marker = autoHandoffMarker(sessionID, sentAt);
|
|
135
|
+
state.sawActivity = false;
|
|
136
|
+
state.digestArmed = false;
|
|
137
|
+
try {
|
|
138
|
+
if (client === undefined)
|
|
139
|
+
throw new Error("no client");
|
|
140
|
+
const result = await client.session.promptAsync({
|
|
141
|
+
path: { id: sessionID },
|
|
142
|
+
query: { directory },
|
|
143
|
+
body: {
|
|
144
|
+
...(state.model?.agent === undefined ? {} : { agent: state.model.agent }),
|
|
145
|
+
...(state.model === undefined ? {} : { model: { providerID: state.model.providerID, modelID: state.model.modelID } }),
|
|
146
|
+
system: litHandoffPromptInjection,
|
|
147
|
+
parts: [{ type: "text", text: autoHandoffDirective(state.observedPercent, state.marker) }]
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (failed(result))
|
|
151
|
+
throw new Error("promptAsync refused");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
state.phase = "done";
|
|
155
|
+
state.marker = undefined;
|
|
156
|
+
toast("The automatic handoff could not be started. The conversation was left as it is.", "warning");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function sessionIsBusy(sessionID) {
|
|
160
|
+
try {
|
|
161
|
+
const result = await client?.session.status({ query: { directory } });
|
|
162
|
+
const data = isRecord(result) ? result.data : undefined;
|
|
163
|
+
const status = isRecord(data) ? data[sessionID] : undefined;
|
|
164
|
+
return isRecord(status) && typeof status.type === "string" && status.type !== "idle";
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function startCompaction(sessionID, state) {
|
|
171
|
+
state.phase = "compacting";
|
|
172
|
+
toast("Handoff saved. Compacting the conversation now.");
|
|
173
|
+
try {
|
|
174
|
+
if (client === undefined || state.model === undefined)
|
|
175
|
+
throw new Error("no model");
|
|
176
|
+
const result = await client.session.summarize({
|
|
177
|
+
path: { id: sessionID },
|
|
178
|
+
query: { directory },
|
|
179
|
+
body: { providerID: state.model.providerID, modelID: state.model.modelID }
|
|
180
|
+
});
|
|
181
|
+
if (failed(result))
|
|
182
|
+
throw new Error("summarize refused");
|
|
183
|
+
if (state.phase === "compacting") {
|
|
184
|
+
state.phase = "done";
|
|
185
|
+
state.digestArmed = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
if (state.phase === "compacting") {
|
|
190
|
+
state.phase = "done";
|
|
191
|
+
toast(compactReminder);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function afterHandoffReply(sessionID, state) {
|
|
196
|
+
if (!state.sawActivity || state.marker === undefined || state.sentAt === undefined)
|
|
197
|
+
return;
|
|
198
|
+
if (await sessionIsBusy(sessionID))
|
|
199
|
+
return;
|
|
200
|
+
if (state.phase !== "sent")
|
|
201
|
+
return;
|
|
202
|
+
const fresh = await readFreshHandoff(options.projectRoot, state.marker, state.sentAt);
|
|
203
|
+
if (state.phase !== "sent")
|
|
204
|
+
return;
|
|
205
|
+
if (fresh === undefined) {
|
|
206
|
+
state.phase = "done";
|
|
207
|
+
state.marker = undefined;
|
|
208
|
+
toast("No handoff carrying this session's marker was found, so the conversation was not compacted and nothing was lost.", "warning");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Compaction runs in the background: the host answers the request only when it has finished.
|
|
212
|
+
void startCompaction(sessionID, state);
|
|
213
|
+
}
|
|
214
|
+
async function onIdle(sessionID) {
|
|
215
|
+
const state = sessions.get(sessionID);
|
|
216
|
+
if (state === undefined)
|
|
217
|
+
return;
|
|
218
|
+
if (state.phase === "crossed") {
|
|
219
|
+
const resolved = await settings.resolve();
|
|
220
|
+
if (state.phase !== "crossed")
|
|
221
|
+
return;
|
|
222
|
+
if (!resolved.enabled || resolved.percent === null) {
|
|
223
|
+
state.phase = "armed";
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await sendHandoffRequest(sessionID, state);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (state.phase === "sent")
|
|
230
|
+
await afterHandoffReply(sessionID, state);
|
|
231
|
+
}
|
|
232
|
+
function onCompacted(sessionID) {
|
|
233
|
+
const state = sessions.get(sessionID);
|
|
234
|
+
if (state === undefined)
|
|
235
|
+
return;
|
|
236
|
+
// A compaction that arrives before any request was sent has already freed the context.
|
|
237
|
+
if (state.phase === "crossed") {
|
|
238
|
+
state.phase = "armed";
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (state.marker === undefined)
|
|
242
|
+
return;
|
|
243
|
+
if (state.phase === "sent" || state.phase === "compacting")
|
|
244
|
+
state.phase = "done";
|
|
245
|
+
if (state.phase === "done")
|
|
246
|
+
state.digestArmed = true;
|
|
247
|
+
}
|
|
248
|
+
async function handle(hostEvent) {
|
|
249
|
+
const properties = hostEvent.properties;
|
|
250
|
+
if (!isRecord(properties))
|
|
251
|
+
return;
|
|
252
|
+
switch (hostEvent.type) {
|
|
253
|
+
case "message.updated":
|
|
254
|
+
await onMessage(properties.info);
|
|
255
|
+
return;
|
|
256
|
+
case "session.status": {
|
|
257
|
+
const status = properties.status;
|
|
258
|
+
const sessionID = properties.sessionID;
|
|
259
|
+
if (typeof sessionID !== "string" || !isRecord(status))
|
|
260
|
+
return;
|
|
261
|
+
if (status.type === "busy") {
|
|
262
|
+
const state = sessions.get(sessionID);
|
|
263
|
+
if (state?.phase === "sent")
|
|
264
|
+
state.sawActivity = true;
|
|
265
|
+
}
|
|
266
|
+
else if (status.type === "idle") {
|
|
267
|
+
await onIdle(sessionID);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
case "session.idle":
|
|
272
|
+
if (typeof properties.sessionID === "string")
|
|
273
|
+
await onIdle(properties.sessionID);
|
|
274
|
+
return;
|
|
275
|
+
case "session.compacted":
|
|
276
|
+
if (typeof properties.sessionID === "string")
|
|
277
|
+
onCompacted(properties.sessionID);
|
|
278
|
+
return;
|
|
279
|
+
case "session.deleted": {
|
|
280
|
+
const info = properties.info;
|
|
281
|
+
if (isRecord(info) && typeof info.id === "string")
|
|
282
|
+
sessions.delete(info.id);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
default:
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// The host publishes a turn's final usage update and its idle event in the same millisecond and does
|
|
290
|
+
// not wait for a hook to finish, so handling them in parallel would let the idle event overtake the
|
|
291
|
+
// usage update it depends on.
|
|
292
|
+
let queue = Promise.resolve();
|
|
293
|
+
const event = ({ event: hostEvent }) => {
|
|
294
|
+
const run = queue.then(() => handle(hostEvent));
|
|
295
|
+
queue = run.catch(() => undefined);
|
|
296
|
+
return run;
|
|
297
|
+
};
|
|
298
|
+
const systemTransform = async (input, output) => {
|
|
299
|
+
if (typeof input.sessionID !== "string" || input.sessionID === "")
|
|
300
|
+
return;
|
|
301
|
+
const state = sessions.get(input.sessionID);
|
|
302
|
+
if (state === undefined || !state.digestArmed || state.marker === undefined || state.sentAt === undefined)
|
|
303
|
+
return;
|
|
304
|
+
const { marker, sentAt } = state;
|
|
305
|
+
state.digestArmed = false;
|
|
306
|
+
state.marker = undefined;
|
|
307
|
+
const fresh = await readFreshHandoff(options.projectRoot, marker, sentAt);
|
|
308
|
+
if (fresh === undefined || !Array.isArray(output.system))
|
|
309
|
+
return;
|
|
310
|
+
output.system.push(handoffDigestText(fresh.relativePath, fresh.text));
|
|
311
|
+
};
|
|
312
|
+
return { event, systemTransform, route: settings.route, resolve: settings.resolve };
|
|
313
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AutoHandoffConfig } from "./config.ts";
|
|
2
|
+
export declare const autoHandoffEnvEnabled = "LITOPENCODE_AUTO_HANDOFF";
|
|
3
|
+
export declare const autoHandoffEnvPercent = "LITOPENCODE_AUTO_HANDOFF_PERCENT";
|
|
4
|
+
export type AutoHandoffSource = "env" | "command" | "config" | "default" | "none";
|
|
5
|
+
export type AutoHandoffResolution = {
|
|
6
|
+
readonly enabled: boolean;
|
|
7
|
+
readonly percent: number | null;
|
|
8
|
+
readonly lastPercent: number | null;
|
|
9
|
+
readonly warnings: readonly string[];
|
|
10
|
+
readonly sources: {
|
|
11
|
+
readonly enabled: AutoHandoffSource;
|
|
12
|
+
readonly percent: AutoHandoffSource;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function isValidAutoHandoffPercent(value: unknown): value is number;
|
|
16
|
+
/**
|
|
17
|
+
* Environment beats the saved command setting, which beats the config file, which beats the default
|
|
18
|
+
* (OFF, no percent). A percent that is not a whole number from 1 to 99 switches the feature OFF and
|
|
19
|
+
* says which source carried it; nothing falls back to a quieter source behind the user's back.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAutoHandoff(input: {
|
|
22
|
+
readonly config: AutoHandoffConfig;
|
|
23
|
+
readonly state: AutoHandoffConfig | undefined;
|
|
24
|
+
readonly env: NodeJS.ProcessEnv;
|
|
25
|
+
readonly stateProblem?: string;
|
|
26
|
+
}): AutoHandoffResolution;
|
|
27
|
+
export declare function readAutoHandoffState(stateFile: string): Promise<{
|
|
28
|
+
readonly state?: AutoHandoffConfig;
|
|
29
|
+
readonly problem?: string;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function writeAutoHandoffState(stateFile: string, state: AutoHandoffConfig): Promise<void>;
|
|
32
|
+
export type AutoHandoffRoute = {
|
|
33
|
+
readonly action: "on";
|
|
34
|
+
readonly percent: string | undefined;
|
|
35
|
+
} | {
|
|
36
|
+
readonly action: "off";
|
|
37
|
+
} | {
|
|
38
|
+
readonly action: "status";
|
|
39
|
+
} | {
|
|
40
|
+
readonly action: "invalid";
|
|
41
|
+
};
|
|
42
|
+
export type AutoHandoffRouteMode = "command" | "chat";
|
|
43
|
+
/**
|
|
44
|
+
* Slash arguments arrive without the command name (`auto on 70`); a chat message carries it
|
|
45
|
+
* (`lit-handoff auto on 70`) and must be that whole single line, so a sentence that merely mentions
|
|
46
|
+
* the route never changes a setting.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseAutoHandoffRoute(text: string, mode: AutoHandoffRouteMode): AutoHandoffRoute | undefined;
|
|
49
|
+
export declare function autoHandoffSettingText(reply: string): string;
|
|
50
|
+
export type AutoHandoffSettings = {
|
|
51
|
+
readonly resolve: () => Promise<AutoHandoffResolution>;
|
|
52
|
+
readonly route: (text: string, mode: AutoHandoffRouteMode) => Promise<string | undefined>;
|
|
53
|
+
};
|
|
54
|
+
export declare function createAutoHandoffSettings(options: {
|
|
55
|
+
readonly stateFile: string;
|
|
56
|
+
readonly config: AutoHandoffConfig;
|
|
57
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
58
|
+
}): AutoHandoffSettings;
|
|
59
|
+
/** The host's own count: its total when present, otherwise input + output + cache read + cache write. */
|
|
60
|
+
export declare function assistantTokenTotal(info: unknown): number;
|
|
61
|
+
export declare function autoHandoffMarker(sessionID: string, firedAt: number): string;
|
|
62
|
+
export declare function autoHandoffDirective(percent: number, marker: string): string;
|
|
63
|
+
export type FreshHandoff = {
|
|
64
|
+
readonly relativePath: string;
|
|
65
|
+
readonly text: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A handoff counts only when it carries the marker this session's trigger produced and the file was
|
|
69
|
+
* written after the trigger. A file left by an earlier run, another session, or a person does not.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readFreshHandoff(projectRoot: string, marker: string, firedAt: number): Promise<FreshHandoff | undefined>;
|
|
72
|
+
export declare const handoffDigestLimitBytes = 6144;
|
|
73
|
+
export declare function handoffDigestText(relativePath: string, text: string): string;
|