omp-vcc 0.1.4 → 0.1.6
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/README.md +59 -166
- package/extensions/main.ts +5 -24
- package/extensions/vcc-core/core/migrate-stale.ts +162 -0
- package/extensions/vcc-core/core/settings.ts +20 -3
- package/extensions/vcc-core/hook.ts +32 -1
- package/package.json +14 -10
- package/scripts/e2e.ts +131 -0
- package/scripts/smoke.ts +10 -2
- package/scripts/uninstall-reset.js +251 -22
- package/skills/omp-vcc/SKILL.md +1 -2
- package/commands/omp-vcc.md +0 -19
- package/commands/vcc-recall.md +0 -21
package/README.md
CHANGED
|
@@ -1,48 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Algorithmic VCC Compaction for oh-my-pi
|
|
2
2
|
|
|
3
|
-
> Fast, deterministic, lossless compaction
|
|
3
|
+
> Fast, deterministic, lossless compaction. No LLM calls. Port of [`sting8k/pi-vcc@0.7.0`](https://github.com/sting8k/pi-vcc) into [oh-my-pi](https://github.com/can1357/oh-my-pi). Paper [`arxiv:2603.29678`](https://arxiv.org/pdf/2603.29678) (Zhang & Agrawala).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
> **New here?** → [`docs/setup.md`](docs/setup.md)
|
|
8
|
-
|
|
9
7
|
```sh
|
|
10
|
-
|
|
11
|
-
omp plugin
|
|
8
|
+
omp plugin link /Users/zhu/code/projects/omp-vcc # local
|
|
9
|
+
omp plugin install omp-vcc # npmjs, no auth
|
|
10
|
+
omp plugin install @zhulinchng/omp-vcc # GitHub Packages, needs PAT
|
|
11
|
+
omp plugin install github:zhulinchng/omp-vcc # git, latest main
|
|
12
12
|
omp plugin list --json | jq '.[] | select(.name|contains("omp-vcc"))'
|
|
13
|
-
|
|
14
|
-
# from npm (unscoped)
|
|
15
|
-
omp plugin install omp-vcc
|
|
16
|
-
|
|
17
|
-
# from GitHub Packages (scoped, requires auth — see below)
|
|
18
|
-
omp plugin install @zhulinchng/omp-vcc
|
|
19
|
-
|
|
20
|
-
# from git
|
|
21
|
-
omp plugin install github:zhulinchng/omp-vcc
|
|
22
13
|
```
|
|
23
14
|
|
|
24
|
-
|
|
15
|
+
New here → [`docs/setup.md`](docs/setup.md).
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
- **Manual** `/omp-vcc [keep:N] [focus]` (and `/pi-vcc` alias) — e.g. `/omp-vcc keep:2 fix auth` keeps last 2 user turns.
|
|
28
|
-
- **Recall** `vcc_recall({query:"redis cache", scope:"all", page:1})` or `/vcc-recall hook|inject scope:all page:2` — ranked regex → TF-IDF OR, 5/page, `mode:'touched'` and `#N:path` drill-down.
|
|
29
|
-
- **Savings observability** — toast `90.0k→22.0k (76% saved, ~68.0k)`, divider `── compacted · 90K→22K ·`, `vcc_stats` tool + `/vcc-stats` table + `details.savings` + `/tmp/omp-vcc-debug.json` (authoritative `tokensAfter` from host).
|
|
17
|
+
## What you get
|
|
30
18
|
|
|
19
|
+
- **Manual** `/omp-vcc [keep:N] [focus]` (`/pi-vcc` alias) — compact only, single-line toast `90k→22k` — e.g. `/omp-vcc keep:2 fix auth` (detailed savings via `/vcc-stats`).
|
|
20
|
+
- **Recall** `vcc_recall({query, scope, page, mode, expand})` + `/vcc-recall` — regex → TF-IDF OR, 5/page, `mode:'touched'`, `#N:path` drill-down.
|
|
21
|
+
- **Savings** toast `90k→22k (76% saved)` single line (inline `Last compaction` detail removed; use `/vcc-stats`), divider, `/vcc-stats` + `vcc_stats` tool (50-capped history, per-pi), `details.savings` persisted, `/tmp/omp-vcc-debug.json`.
|
|
31
22
|
## Commands
|
|
32
23
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
| `/
|
|
36
|
-
| `/pi-vcc` |
|
|
37
|
-
| `/vcc-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| `/omp-vcc-stats` | Alias for `/vcc-stats` |
|
|
24
|
+
| `/omp-vcc [keep:N] [focus]` | Compact only. `keep:0` = all. Smart-keep boosts `keep:1` (5k→25k) unless explicit. Toast single line; detail via `/vcc-stats`. |
|
|
25
|
+
| `/pi-vcc` | Legacy alias for `/omp-vcc` |
|
|
26
|
+
| `/vcc-recall [query] [scope:all] [page:N] [mode:touched]` | Search compacted history |
|
|
27
|
+
| `/pi-vcc-recall` | Legacy alias for `/vcc-recall` |
|
|
28
|
+
| `/vcc-stats [history\|all]` | Last savings + history table (single, no alias) |
|
|
29
|
+
|
|
30
|
+
Tools: `vcc_recall`, `vcc_stats` (approval `read`). Extension-only — no `commands/*.md` file slash commands (avoids duplicate `/omp-vcc`).
|
|
41
31
|
|
|
42
|
-
Tool `vcc_recall` mirrors the command, plus `expand:[indices]` and `mode:'touched'` for file index. Tool `vcc_stats({history:true})` mirrors `/vcc-stats` (approval `read`), same 50-capped table.
|
|
43
32
|
## Configuration
|
|
44
33
|
|
|
45
|
-
File `~/.omp/omp-vcc/config.json`
|
|
34
|
+
File `~/.omp/omp-vcc/config.json` — XDG: `$OMP_VCC_CONFIG_PATH` > `$PI_VCC_CONFIG_PATH` > `$OMP_DIR`/`$PI_CODING_AGENT_DIR` > `~/.omp/omp-vcc/config.json` (migrates `~/.pi/agent/pi-vcc-config.json` once).
|
|
46
35
|
|
|
47
36
|
```json
|
|
48
37
|
{
|
|
@@ -50,170 +39,74 @@ File `~/.omp/omp-vcc/config.json` (XDG-aware: `$OMP_VCC_CONFIG_PATH` > `$PI_VCC_
|
|
|
50
39
|
"overrideDefaultCompaction": true,
|
|
51
40
|
"smartKeepTail": true,
|
|
52
41
|
"continueAfterThresholdCompact": true,
|
|
53
|
-
"debug": false
|
|
42
|
+
"debug": false,
|
|
43
|
+
"chainShakeHint": false
|
|
54
44
|
}
|
|
55
45
|
```
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`omp-vcc` implements `V_ui` as the structured summary (5 sections + ranked brief transcript) and `V_adapt` as `vcc_recall`. Pointer invariant `V_ui → V_full[s:e]` holds structurally.
|
|
66
|
-
|
|
67
|
-
### VCC algorithm (30–470 ms)
|
|
68
|
-
|
|
69
|
-
Traditional compaction ships the whole history to a remote LLM and waits seconds. `omp-vcc` never calls a model: it reuses `branchEntries` already in memory, calibrates token size, cuts, normalizes, and ranks locally.
|
|
70
|
-
|
|
71
|
-
```mermaid
|
|
72
|
-
flowchart TB
|
|
73
|
-
subgraph VCC["omp-vcc — local, deterministic"]
|
|
74
|
-
A["branchEntries\nin memory"] --> B["calibrate\ncpt = chars / tokensBefore"]
|
|
75
|
-
B --> C["buildOwnCut + smartKeep\nkeep tailored to tail size"]
|
|
76
|
-
C --> D["normalize + filter\nstrip ANSI, 123 arrow, harness XML"]
|
|
77
|
-
D --> E["rank TF-IDF\n5 sections + brief transcript"]
|
|
78
|
-
E --> F["summary 1.1k tok\n+ kept tail\n30-470 ms, zero cost"]
|
|
79
|
-
end
|
|
80
|
-
subgraph LLM["native remote LLM compaction"]
|
|
81
|
-
L1["branchEntries"] --> L2["serialize history\nHTTP to LLM"]
|
|
82
|
-
L2 --> L3["wait seconds\n+ token cost\n+ nondeterministic"]
|
|
83
|
-
end
|
|
84
|
-
F -. "next turn sees only F" .-> A
|
|
85
|
-
classDef vcc fill:#e8f5e9,stroke:#2e7d32
|
|
86
|
-
class F vcc
|
|
87
|
-
classDef llm fill:#fce4ec,stroke:#c2185b
|
|
88
|
-
class L3 llm
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
What `omp-vcc` adds vs intercepts in the harness (hooks, tools, commands, settings) and why — see [`docs/harness.md`](docs/harness.md) §§2–3.
|
|
92
|
-
|
|
93
|
-
**Example** — a 80-turn session at 90k tokens, `keep:1` tail is only 3k (wastes budget):
|
|
94
|
-
|
|
95
|
-
- raw tail `3k` → `smartKeepTail` grows to `keep:4` with tail `21k` (still ≤ 25k cap)
|
|
96
|
-
- older 76 turns are compiled into `V_ui`:
|
|
97
|
-
|
|
98
|
-
```txt
|
|
99
|
-
[Session Goal] Fix auth token refresh
|
|
100
|
-
[Files And Changes] src/auth.ts, src/app.ts
|
|
101
|
-
[Brief transcript] (ranked, TF-IDF, 78 lines)
|
|
102
|
-
(#12) Read src/auth.ts — missing refresh on expiry
|
|
103
|
-
(#18) Edit src/auth.ts:12-34 — add refreshToken()
|
|
104
|
-
(#33) Test auth flow — 2 failed, 1 passed
|
|
105
|
-
---
|
|
106
|
-
Recall is the same idea: `vcc_recall` runs local regex → TF-IDF `rank.ts` and preserves skeleton. `query: "hook|inject"` returns 5 hits with `(#N)` pointers like `(#33) hook registration`; `query: "#18:src/auth.ts"` drills to `V_full[18:e]` verbatim.
|
|
107
|
-
|
|
108
|
-
Docs: [`architecture`](docs/architecture.md) · [`configuration`](docs/configuration.md) · [`verification`](docs/verification.md) · [`harness`](docs/harness.md) · [`paper-notes`](docs/paper-notes.md) · [`setup`](docs/setup.md) · [`PUBLISHING`](docs/PUBLISHING.md) · pinned [`omp-compaction`](docs/omp-compaction.md) / [`omp-snapcompact`](docs/omp-snapcompact.md).
|
|
109
|
-
|
|
110
|
-
- **pi-vcc** — TypeScript algorithmic compactor, zero LLM, `RANKED_BRIEF_BUDGET_TOKENS=1100` ceil 2000, `charsPerBlock 15`
|
|
111
|
-
- **VCC** — Python `VCC.py` adaptive/transposed views, `SEP`, `match_lines`, `_tokenize`, `_trunc`, projection model
|
|
112
|
-
- **Paper** — AppWorld evaluation: +1.1–4.2 task_goal points, ½–⅔ token halving, smaller memory
|
|
113
|
-
|
|
114
|
-
## Best practices
|
|
47
|
+
| Flag | Default | Effect |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `vccEnabled` | `true` | Master switch. `false` = no interception. |
|
|
50
|
+
| `overrideDefaultCompaction` | `true` | `true` = VCC handles threshold/overflow. `false` = host `methodOrder` handles them; `/omp-vcc` still works. |
|
|
51
|
+
| `smartKeepTail` | `true` | Grow `keep:1` when tail <5k (cap 25k). Explicit `keep:N` never boosted. |
|
|
52
|
+
| `continueAfterThresholdCompact` | `true` | Invisible-continue (`omp-vcc-auto-continue`) after threshold compact. |
|
|
53
|
+
| `debug` | `false` | Write `/tmp/omp-vcc-debug.json` per compaction. |
|
|
54
|
+
| `chainShakeHint` | `false` | Eager post-VCC `shake` chain. Host rescue already auto-shakes on dead-end; this forces it. |
|
|
115
55
|
|
|
116
|
-
|
|
56
|
+
Toggle live: `omp config set plugins."@zhulinchng/omp-vcc".debug true` or `/settings` → `@zhulinchng/omp-vcc`. File is source of truth; `ctx.settings` overlays at runtime.
|
|
117
57
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- Keep `overrideDefaultCompaction:true` (default) — threshold/overflow compaction becomes deterministic and instant. Only set `false` if you explicitly want the remote LLM summarizer for `handoff` or you installed the optional native `vcc` dropdown patch and want to toggle per-session in `/settings`.
|
|
121
|
-
- Keep `smartKeepTail:true` and `continueAfterThresholdCompact:true` — the plugin grows `keep:1` to `keep:2…4` when the tail is tiny (5 k → 25 k) and auto-continues after a threshold compact so the agent doesn't stall mid-task.
|
|
122
|
-
- Don't spam `/omp-vcc` every few turns. Auto threshold (derived from your model's context window) already fires at the right moment. Manual compacts are for deliberate boundaries: finishing a sub-task, before a risky refactor, or when you feel the context getting noisy.
|
|
123
|
-
|
|
124
|
-
### 2) Pick the right `keep:N`
|
|
125
|
-
|
|
126
|
-
| Situation | Command | Why |
|
|
127
|
-
| --- | --- | --- |
|
|
128
|
-
| Default, happy path | `/omp-vcc` or `keep:1` | Smallest tail, max savings. `smartKeepTail` will still grow to `keep:3` if the last turn is only 3 k tok so you don't waste budget. |
|
|
129
|
-
| Actively iterating on last edits/tests | `/omp-vcc keep:2` or `keep:3` | Preserves the last 2–3 user turns verbatim (e.g. failing test output + fix). Costs more tokens but avoids recall. |
|
|
130
|
-
| Need maximal reduction (e.g. before context overflow) | `/omp-vcc keep:0` | Summarizes everything, no tail. Next turn starts from pure `V_ui`. Useful before pasting a huge spec. |
|
|
131
|
-
| With a focus prompt | `/omp-vcc keep:2 focus on auth refresh only` | Preserved tail + an injected follow-up prompt so the agent continues with the narrowed scope. |
|
|
132
|
-
|
|
133
|
-
Explicit `keep:N` always wins — smart-keep never overrides it.
|
|
134
|
-
|
|
135
|
-
### 3) Use recall instead of keeping more
|
|
136
|
-
|
|
137
|
-
Keeping a huge tail is the expensive alternative to recall. Prefer a small keep and search when you need history:
|
|
58
|
+
## How it works
|
|
138
59
|
|
|
139
|
-
|
|
140
|
-
- Regex when you know the pattern: `/vcc-recall hook|inject`, `/vcc-recall fail.*build`.
|
|
141
|
-
- Pagination: `page:2` (5 hits/page): `/vcc-recall auth scope:all page:2`.
|
|
142
|
-
- Scope: default is active lineage (what the current branch actually saw). Add `scope:all` to search abandoned branches/edits/retries.
|
|
143
|
-
- Drill-down: `/vcc-recall #18:src/auth.ts` expands that turn's file slice verbatim from `V_full` — fastest way to rehydrate an edit.
|
|
144
|
-
- Index mode: `vcc_recall({query:"", mode:"touched"})` lists touched files across the session.
|
|
60
|
+
VCC compiles JSONL trace via **lex → parse IR → monotonic line assignment → view lowering** into `V_full` (identity), `V_ui` (5 sections + ranked brief), `V_adapt(b,ρ)` (recall). Pointer invariant `V_ui → V_full[s:e]` via `(#N)` refs.
|
|
145
61
|
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
KEEP["keep small\nkeep:1 + smartKeep"] --> RECALL["need history?\n/vcc-recall keywords"]
|
|
149
|
-
RECALL --> DRILL["#N:path drill\nrehydrate file"]
|
|
150
|
-
KEEP --> FORGET["keep huge tail\nwastes 10-20k tok"]
|
|
151
|
-
style FORGET stroke-dasharray: 3 3
|
|
62
|
+
```
|
|
63
|
+
Calibrate cpt → SmartKeep → BuildOwnCut → Normalize → FilterNoise → BuildSections → Brief (1100→2000 tok, 120 lines) → Merge
|
|
152
64
|
```
|
|
153
65
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
The 5 sections (`[Session Goal]`, `[Files And Changes]`, `[Commits]`, `[Outstanding Context]`, `[User Preferences]`) are regex/heuristics, not an LLM. Make them work better:
|
|
157
|
-
|
|
158
|
-
- State the goal once, plainly, in the first user message: `Goal: fix auth token refresh in src/auth.ts`. That seeds `[Session Goal]` reliably.
|
|
159
|
-
- Declare preferences explicitly with cue words: `always run tests before committing`, `prefer concise diffs`, `never edit src/generated/`. Those go to `[User Preferences]`.
|
|
160
|
-
- Commit frequently — commits populate `[Commits]` and survive compaction better than bare edits.
|
|
161
|
-
- Mark outstanding items as questions/errors (`TODO:`, `failing:`, `why does …?`) so they land in `[Outstanding Context]` until resolved.
|
|
162
|
-
|
|
163
|
-
### 5) Know the difference: compact vs clear
|
|
164
|
-
|
|
165
|
-
- `/omp-vcc` (or `/compact`) **summarizes** — history becomes `V_ui` + searchable `V_full`. The divider `── compacted · 90k→22k · ctrl+o ──` stays in the transcript (expand with `ctrl+o`).
|
|
166
|
-
- `/clear` **erases** — inserts a `reset_boundary` after which even `V_full` is no longer compacted. Previous history stays on disk for `/vcc-recall scope:all` but the live context starts empty. Use `/clear` when you truly want a fresh session; use `/omp-vcc` when you want to keep the story.
|
|
66
|
+
See [`docs/harness.md`](docs/harness.md) for host impact and [`docs/architecture.md`](docs/architecture.md) for pipeline.
|
|
167
67
|
|
|
168
|
-
|
|
68
|
+
## Combining with shake and snapcompact
|
|
169
69
|
|
|
170
|
-
|
|
171
|
-
- To tune: set `debug:true` in `~/.omp/omp-vcc/config.json`, run `/omp-vcc`, then `cat /tmp/omp-vcc-debug.json` — check `usedOwnCut`, `tokensBefore`, `tokenEstimate {charsPerToken, mode}`, `summaryLength`, `sections`. Remember to flip `debug:false` after — the file is overwritten every compact.
|
|
172
|
-
- For overflow: if you hit `tokensBefore > 50k` and nothing happens, check `omp plugin doctor` and that `vccEnabled:true`.
|
|
70
|
+
VCC summarizes **history**; `shake` elides `artifact://` blocks in **kept tail** (disjoint); `snapcompact` is an alternative history archiver (vision bitmaps, needs `model.input includes "image"`). One `CompactionEntry` per trigger — combos are additive or sequential, never double-summarization.
|
|
173
71
|
|
|
174
|
-
|
|
72
|
+
| Trigger | `override` | Result |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| threshold, `override:true` (default) | VCC handles; host auto-shakes if still over band (`#rescueCompactionDeadEnd`) | VCC + shake if dead-end |
|
|
75
|
+
| threshold, `override:false` | Host walks `methodOrder` (`remote→snapcompact→handoff→shake→soft`) | VCC → snapcompact/shake fallback |
|
|
76
|
+
| `/omp-vcc keep:2` | Always VCC (sentinel `__omp_vcc__`) | VCC |
|
|
77
|
+
| `/omp-vcc` then `/compact snapcompact` | Sequential | VCC entry, then snapcompact entry |
|
|
175
78
|
|
|
176
|
-
|
|
177
|
-
- `snapcompact`/`shake`/`handoff` in `compaction.methodOrder` are orthogonal — `omp-vcc` only intercepts `context-full`. Leave them in the order if you use them; they run when `omp-vcc` explicitly defers (e.g. `vccEnabled:false`).
|
|
178
|
-
- Pin the plugin version in CI or shared dots: `omp plugin install github:zhulinchng/omp-vcc#v0.1.x` so the team shares the same `RANKED_BRIEF_BUDGET_TOKENS=1100` behavior.
|
|
79
|
+
Additive VCC+shake is automatic. Eager chain: `chainShakeHint:true`. Explicit mode (`snapcompact`/`shake`/`soft`/…) bypasses VCC even when `override:true` (`hook.ts:733`). Full table: [`docs/setup.md`](docs/setup.md) + [`docs/harness.md §8`](docs/harness.md#8-working-with-existing-compaction-strategies).
|
|
179
80
|
|
|
180
81
|
## Development
|
|
181
82
|
|
|
182
|
-
```
|
|
183
|
-
omp plugin link .
|
|
184
|
-
omp plugin doctor
|
|
83
|
+
```sh
|
|
185
84
|
bunx tsc --noEmit
|
|
186
|
-
bun
|
|
187
|
-
bun test
|
|
85
|
+
bun test # 515 tests, 48 files, 1443 expects, 0 fail
|
|
86
|
+
bun test tests/e2e --timeout 120000 # 111 E2E
|
|
87
|
+
bun run smoke # 9 checks: 3 hooks + 4 cmds + 2 tools + pipeline
|
|
88
|
+
omp plugin link . && omp plugin doctor
|
|
188
89
|
```
|
|
189
90
|
|
|
190
|
-
|
|
91
|
+
`type:module`, `allowImportingTsExtensions`, `// @ts-nocheck` on vendored `extensions/vcc-core/**`, zero `dist/`. `prepublishOnly` runs `tsc && test && smoke`.
|
|
191
92
|
|
|
192
93
|
## Verification
|
|
193
94
|
|
|
194
95
|
```sh
|
|
195
|
-
bunx tsc --noEmit
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
96
|
+
bunx tsc --noEmit && bun test && bun run smoke
|
|
97
|
+
omp -e @zhulinchng/omp-vcc
|
|
98
|
+
/omp-vcc keep:1 # expect [Session Goal] + toast omp-vcc: kept 1/2 turns
|
|
99
|
+
/vcc-stats # table + history
|
|
100
|
+
cat /tmp/omp-vcc-debug.json # when debug:true
|
|
199
101
|
```
|
|
200
102
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Docs: [`architecture`](docs/architecture.md) · [`configuration`](docs/configuration.md) · [`verification`](docs/verification.md) · [`harness`](docs/harness.md) · [`paper-notes`](docs/paper-notes.md) · [`setup`](docs/setup.md) · [`PUBLISHING`](docs/PUBLISHING.md) · pinned [`omp-compaction`](docs/omp-compaction.md) / [`omp-snapcompact`](docs/omp-snapcompact.md).
|
|
204
|
-
|
|
205
|
-
See [`docs/PUBLISHING.md`](docs/PUBLISHING.md) for the full checklist (package shape, gates, dual `omp-vcc` / `@zhulinchng/omp-vcc` flow, verification, deployment matrix, and troubleshooting). TL;DR:
|
|
103
|
+
Full matrix: [`docs/verification.md`](docs/verification.md). Harness map: [`docs/harness.md §9`](docs/harness.md#9-verification-map-claim--evidence).
|
|
206
104
|
|
|
207
|
-
|
|
208
|
-
- GitHub Packages (scoped): `gh release create vX.Y.Z` triggers `.github/workflows/publish-gpr.yml` → `@zhulinchng/omp-vcc` via `GITHUB_TOKEN` (`read:packages, write:packages`); manual fallback `npm pkg set name=@zhulinchng/omp-vcc && GITHUB_TOKEN=$(gh auth token) npm publish --userconfig /tmp/gpr-npmrc --access public` (see guide for the scoped-registry `/tmp/gpr-npmrc` pitfall)
|
|
209
|
-
- Consumer GPR auth: add to `~/.npmrc`:
|
|
105
|
+
## Docs
|
|
210
106
|
|
|
211
|
-
|
|
212
|
-
@zhulinchng:registry=https://npm.pkg.github.com
|
|
213
|
-
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
|
|
214
|
-
```
|
|
107
|
+
[`setup`](docs/setup.md) · [`configuration`](docs/configuration.md) · [`architecture`](docs/architecture.md) · [`harness`](docs/harness.md) · [`verification`](docs/verification.md) · [`omp-compaction`](docs/omp-compaction.md) · [`omp-snapcompact`](docs/omp-snapcompact.md) · [`paper-notes`](docs/paper-notes.md) · [`PUBLISHING`](docs/PUBLISHING.md)
|
|
215
108
|
|
|
216
|
-
|
|
109
|
+
Publishing: `npm publish --access public` (unscoped `omp-vcc`), `gh release create vX.Y.Z` → GPR (`@zhulinchng/omp-vcc`). Consumer GPR auth: `@zhulinchng:registry=https://npm.pkg.github.com`.
|
|
217
110
|
|
|
218
111
|
## License
|
|
219
112
|
|
package/extensions/main.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
8
8
|
import { scaffoldSettings } from "./vcc-core/core/settings";
|
|
9
|
+
import { migrateStalePluginEntries } from "./vcc-core/core/migrate-stale";
|
|
9
10
|
import { loadAllMessages } from "./vcc-core/core/load-messages";
|
|
10
11
|
import {
|
|
11
12
|
registerBeforeCompactHook,
|
|
@@ -45,9 +46,11 @@ const parseRecallCommandArgs = (
|
|
|
45
46
|
|
|
46
47
|
const DEFAULT_RECENT = 25;
|
|
47
48
|
const PAGE_SIZE = 5;
|
|
48
|
-
|
|
49
49
|
export default function (pi: ExtensionAPI): void {
|
|
50
50
|
scaffoldSettings();
|
|
51
|
+
try {
|
|
52
|
+
migrateStalePluginEntries();
|
|
53
|
+
} catch {}
|
|
51
54
|
registerBeforeCompactHook(pi);
|
|
52
55
|
|
|
53
56
|
// ── vcc_recall tool — implements VCC V_adapt via rho predicate (paper §2.1 eq.2) ──
|
|
@@ -160,40 +163,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
160
163
|
// ── vcc_stats tool — stats surface for savings (paper § verification) ──
|
|
161
164
|
registerVccStatsToolHook(pi);
|
|
162
165
|
|
|
163
|
-
|
|
164
166
|
pi.registerCommand("omp-vcc", {
|
|
165
|
-
description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)
|
|
167
|
+
description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
|
|
166
168
|
handler: async (args: string, ctx: unknown) => {
|
|
167
169
|
const c = ctx as {
|
|
168
170
|
compact: (instructions?: string) => Promise<void>;
|
|
169
171
|
ui: { notify: (msg: string, level?: string) => void };
|
|
170
172
|
sessionManager?: { getSessionFile?: () => string | undefined };
|
|
171
173
|
};
|
|
172
|
-
const trimmed = (args || "").trim();
|
|
173
|
-
const lower = trimmed.toLowerCase();
|
|
174
|
-
if (lower === "--stats" || lower === "stats" || lower.startsWith("--stats ") || lower.startsWith("stats ")) {
|
|
175
|
-
const wantHistory = lower.includes("history") || lower.includes("all");
|
|
176
|
-
const history = getCompactionHistory(pi);
|
|
177
|
-
const last = getLastCompactionStats(pi);
|
|
178
|
-
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
179
|
-
if (!last && history.length === 0) {
|
|
180
|
-
try { piAny.sendMessage?.({ customType: "vcc-stats", content: "No compactions yet. Run /omp-vcc to compact first.", display: true }, { triggerTurn: false }); } catch {}
|
|
181
|
-
try { c.ui.notify("No compactions yet.", "info"); } catch {}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const output = wantHistory
|
|
185
|
-
? `${formatStatsTable(history)}\n\n${last ? formatLastStatsDetail(last) : ""}`
|
|
186
|
-
: `${last ? formatLastStatsDetail(last) : "No last stats"}${history.length > 1 ? `\n\nHistory:\n${formatStatsTable(history)}` : ""}`;
|
|
187
|
-
try { piAny.sendMessage?.({ customType: "vcc-stats", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
188
|
-
try { c.ui.notify(`vcc_stats: ${history.length} compaction(s)`, "info"); } catch {}
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
174
|
const parsed = parseKeepAndPrompt(args);
|
|
192
175
|
const keep = parsed.keepUserTurns;
|
|
193
176
|
const followUpPrompt = parsed.followUpPrompt;
|
|
194
177
|
const customInstructions = buildOmpCustomInstructions(keep);
|
|
195
|
-
// Also accept pi sentinel for legacy: map keep via buildPiVcc if needed? Use omp.
|
|
196
|
-
// Notify before compact for UX parity with pi-vcc
|
|
197
178
|
try {
|
|
198
179
|
c.ui.notify(`omp-vcc: compacting with keep:${keep ?? 1}${followUpPrompt ? ` + focus` : ""}...`, "info");
|
|
199
180
|
} catch {}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// One-time migration: remove stale @zhu/omp-vcc lock+symlink left by renames.
|
|
3
|
+
// correctly installed `omp-vcc` or npm `dependencies` entries (real dirs).
|
|
4
|
+
// Mirrors scripts/uninstall-reset.js:cleanupStalePluginEntries.
|
|
5
|
+
|
|
6
|
+
import * as fsSync from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const HISTORIC = ["@zhu/omp-vcc", "@zhulinchng/omp-vcc"];
|
|
11
|
+
const CURRENT = "omp-vcc";
|
|
12
|
+
|
|
13
|
+
function readJson(p: string): unknown | null {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(fsSync.readFileSync(p, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSymlink(p: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return fsSync.lstatSync(p).isSymbolicLink();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function migrateStalePluginEntries(home?: string): string {
|
|
30
|
+
const baseHome = home || homedir();
|
|
31
|
+
const pluginsDir = join(baseHome, ".omp", "plugins");
|
|
32
|
+
const lockPath = join(pluginsDir, "omp-plugins.lock.json");
|
|
33
|
+
const pkgPath = join(pluginsDir, "package.json");
|
|
34
|
+
const nm = join(pluginsDir, "node_modules");
|
|
35
|
+
|
|
36
|
+
const lockRaw = readJson(lockPath) as { plugins?: Record<string, unknown>; settings?: Record<string, unknown> } | null;
|
|
37
|
+
if (!lockRaw || typeof lockRaw !== "object") return "no-lock";
|
|
38
|
+
|
|
39
|
+
const pkgRaw = readJson(pkgPath) as { dependencies?: Record<string, string> } | null;
|
|
40
|
+
const deps: Record<string, string> = (pkgRaw && typeof pkgRaw.dependencies === "object" && pkgRaw.dependencies) || {};
|
|
41
|
+
|
|
42
|
+
const pluginKeys: string[] = lockRaw.plugins ? Object.keys(lockRaw.plugins) : [];
|
|
43
|
+
const ompKeys = pluginKeys.filter((k) => k.includes("omp-vcc"));
|
|
44
|
+
const candidates = new Set([...ompKeys, ...HISTORIC, CURRENT]);
|
|
45
|
+
|
|
46
|
+
let removedLocks: string[] = [];
|
|
47
|
+
let removedLinks: string[] = [];
|
|
48
|
+
|
|
49
|
+
const getPkgName = (name: string): string | null => {
|
|
50
|
+
const j = readJson(join(nm, name, "package.json")) as { name?: string } | null;
|
|
51
|
+
return j && typeof j.name === "string" ? j.name : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getReal = (name: string): string | null => {
|
|
55
|
+
const p = join(nm, name);
|
|
56
|
+
if (!isSymlink(p)) return null;
|
|
57
|
+
try {
|
|
58
|
+
return fsSync.realpathSync(p);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const realToKeys = new Map<string, string[]>();
|
|
65
|
+
for (const k of ompKeys) {
|
|
66
|
+
const rp = getReal(k);
|
|
67
|
+
if (!rp) continue;
|
|
68
|
+
const list = realToKeys.get(rp) || [];
|
|
69
|
+
list.push(k);
|
|
70
|
+
realToKeys.set(rp, list);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const k of [...ompKeys]) {
|
|
74
|
+
if (k in deps) continue;
|
|
75
|
+
const pkgName = getPkgName(k);
|
|
76
|
+
if (pkgName && pkgName !== k) {
|
|
77
|
+
const isHistoric = HISTORIC.includes(k) || k !== CURRENT;
|
|
78
|
+
if (isHistoric) {
|
|
79
|
+
const p = join(nm, k);
|
|
80
|
+
if (isSymlink(p)) {
|
|
81
|
+
try {
|
|
82
|
+
fsSync.rmSync(p, { force: true });
|
|
83
|
+
removedLinks.push(k);
|
|
84
|
+
if (k.startsWith("@")) {
|
|
85
|
+
const scopeDir = join(nm, k.split("/")[0]!);
|
|
86
|
+
try {
|
|
87
|
+
if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
if (lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[k]) {
|
|
93
|
+
delete (lockRaw.plugins as Record<string, unknown>)[k];
|
|
94
|
+
removedLocks.push(k);
|
|
95
|
+
}
|
|
96
|
+
if (lockRaw.settings && (lockRaw.settings as Record<string, unknown>)[k]) delete (lockRaw.settings as Record<string, unknown>)[k];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const [, keys] of realToKeys) {
|
|
102
|
+
if (keys.length <= 1) continue;
|
|
103
|
+
let keeper: string | null = null;
|
|
104
|
+
for (const k of keys) {
|
|
105
|
+
if (getPkgName(k) === k) {
|
|
106
|
+
keeper = k;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!keeper) keeper = keys.includes(CURRENT) ? CURRENT : keys[0]!;
|
|
111
|
+
for (const k of keys) {
|
|
112
|
+
if (k === keeper) continue;
|
|
113
|
+
if (k in deps) continue;
|
|
114
|
+
const p = join(nm, k);
|
|
115
|
+
if (isSymlink(p)) {
|
|
116
|
+
try {
|
|
117
|
+
fsSync.rmSync(p, { force: true });
|
|
118
|
+
if (!removedLinks.includes(k)) removedLinks.push(k);
|
|
119
|
+
if (k.startsWith("@")) {
|
|
120
|
+
const scopeDir = join(nm, k.split("/")[0]!);
|
|
121
|
+
try {
|
|
122
|
+
if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
if (lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[k]) {
|
|
128
|
+
delete (lockRaw.plugins as Record<string, unknown>)[k];
|
|
129
|
+
if (!removedLocks.includes(k)) removedLocks.push(k);
|
|
130
|
+
}
|
|
131
|
+
if (lockRaw.settings && (lockRaw.settings as Record<string, unknown>)[k]) delete (lockRaw.settings as Record<string, unknown>)[k];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const cand of candidates) {
|
|
136
|
+
const p = join(nm, cand);
|
|
137
|
+
if (!isSymlink(p)) continue;
|
|
138
|
+
const hasLock = !!(lockRaw.plugins && (lockRaw.plugins as Record<string, unknown>)[cand]);
|
|
139
|
+
if (hasLock) continue;
|
|
140
|
+
const pkgName = getPkgName(cand);
|
|
141
|
+
if (pkgName === CURRENT || pkgName === "@zhu/omp-vcc" || pkgName === "@zhulinchng/omp-vcc") {
|
|
142
|
+
try {
|
|
143
|
+
fsSync.rmSync(p, { force: true });
|
|
144
|
+
if (!removedLinks.includes(cand)) removedLinks.push(cand);
|
|
145
|
+
if (cand.startsWith("@")) {
|
|
146
|
+
const scopeDir = join(nm, cand.split("/")[0]!);
|
|
147
|
+
try {
|
|
148
|
+
if (fsSync.readdirSync(scopeDir).length === 0) fsSync.rmSync(scopeDir, { force: true });
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (removedLocks.length > 0 || removedLinks.length > 0) {
|
|
156
|
+
try {
|
|
157
|
+
fsSync.writeFileSync(lockPath, JSON.stringify(lockRaw, null, 2));
|
|
158
|
+
} catch {}
|
|
159
|
+
return `migrated locks:${removedLocks.join(",") || "none"} links:${removedLinks.join(",") || "none"}`;
|
|
160
|
+
}
|
|
161
|
+
return "no-stale";
|
|
162
|
+
}
|
|
@@ -16,10 +16,16 @@ const settingsPath = (): string =>
|
|
|
16
16
|
/** Backwards-compat export. Resolves at access time, not import time. */
|
|
17
17
|
export const SETTINGS_PATH = settingsPath();
|
|
18
18
|
// For migration: if omp config missing but legacy pi config exists, we read legacy but write to new
|
|
19
|
+
// Also handles concurrent-test env shadowing: if OMP path is set by another test but file missing,
|
|
20
|
+
// fall back to PI path before default.
|
|
19
21
|
const fallbackReadPath = (): string | null => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
22
|
+
const candidates: string[] = [];
|
|
23
|
+
if (process.env.OMP_VCC_CONFIG_PATH) candidates.push(process.env.OMP_VCC_CONFIG_PATH);
|
|
24
|
+
if (process.env.PI_VCC_CONFIG_PATH) candidates.push(process.env.PI_VCC_CONFIG_PATH);
|
|
25
|
+
candidates.push(SETTINGS_PATH_DEFAULT);
|
|
26
|
+
if (!candidates.includes(legacyPiPath)) candidates.push(legacyPiPath);
|
|
27
|
+
for (const p of candidates) if (existsSync(p)) return p;
|
|
28
|
+
// No candidate exists — return primary for creation path (used by scaffold)
|
|
23
29
|
return null;
|
|
24
30
|
};
|
|
25
31
|
|
|
@@ -56,6 +62,16 @@ export interface PiVccSettings {
|
|
|
56
62
|
continueAfterThresholdCompact: boolean;
|
|
57
63
|
/** Write debug snapshot to /tmp/omp-vcc-debug.json on each compaction. */
|
|
58
64
|
debug: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* When true, after a successful VCC threshold/overflow compaction, eagerly
|
|
67
|
+
* trigger a follow-up shake via ctx.compact when the host rescue would not.
|
|
68
|
+
* Default false: host's #rescueCompactionDeadEnd already runs shake elide
|
|
69
|
+
* automatically when VCC didn't create enough headroom, and leaving shake in
|
|
70
|
+
* methodOrder covers that case without a second entry. Set true only if you
|
|
71
|
+
* want a chained shake even when VCC already made headroom (costs a second
|
|
72
|
+
* CompactionEntry).
|
|
73
|
+
*/
|
|
74
|
+
chainShakeHint: boolean;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
export const DEFAULT_SETTINGS: PiVccSettings = {
|
|
@@ -64,6 +80,7 @@ export const DEFAULT_SETTINGS: PiVccSettings = {
|
|
|
64
80
|
smartKeepTail: true,
|
|
65
81
|
continueAfterThresholdCompact: true,
|
|
66
82
|
debug: false,
|
|
83
|
+
chainShakeHint: false,
|
|
67
84
|
};
|
|
68
85
|
|
|
69
86
|
const readJson = (path: string): Record<string, unknown> | null => {
|
|
@@ -89,6 +89,8 @@ const perPi = new WeakMap<any, { lastStats: CompactionStats | null; lastCompactW
|
|
|
89
89
|
// Track strong refs for test helper clearCompactionHistoryForTests: WeakMap keys
|
|
90
90
|
// cannot be enumerated, so keep a Set for test-only cleanup.
|
|
91
91
|
const perPiKeys = new Set<any>();
|
|
92
|
+
// Guard eager chainShakeHint to avoid recursion: tracks pis currently chaining.
|
|
93
|
+
const pendingChainShake = new WeakSet<object>();
|
|
92
94
|
const getPerPi = (pi: any) => {
|
|
93
95
|
if (!pi || typeof pi !== "object") return null;
|
|
94
96
|
let s = perPi.get(pi);
|
|
@@ -730,6 +732,17 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
730
732
|
// Otherwise, only handle when user opted in via settings.
|
|
731
733
|
const { isPiVcc, keepUserTurns, keepUserTurnsExplicit, followUpPrompt } = parseCompactionInstructions(customInstructions);
|
|
732
734
|
setPendingFollowUpPrompt(pi, null);
|
|
735
|
+
// Explicit host mode bypass: when the host signals an explicit compact mode
|
|
736
|
+
// (e.g. /compact snapcompact or --mode shake), let the host walker handle it
|
|
737
|
+
// even though overrideDefaultCompaction is true. This enables sequential
|
|
738
|
+
// VCC → snapcompact/shake combinations. The event field is only present when
|
|
739
|
+
// the optional native vcc patch is applied or a future host exposes it; when
|
|
740
|
+
// absent this branch is no-op and the existing override semantics remain.
|
|
741
|
+
const explicitMode = (event as any).compactMode ?? (event as any).explicitMode ?? (event as any).mode;
|
|
742
|
+
if (!isPiVcc && typeof explicitMode === "string" && explicitMode) {
|
|
743
|
+
const m = explicitMode.toLowerCase();
|
|
744
|
+
if (m === "snapcompact" || m === "shake" || m === "soft" || m === "remote" || m === "handoff") return;
|
|
745
|
+
}
|
|
733
746
|
if (!isPiVcc && !settings.overrideDefaultCompaction) return;
|
|
734
747
|
|
|
735
748
|
const calibrationCut = buildOwnCut(branchEntries as any[], 0);
|
|
@@ -1058,6 +1071,25 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
1058
1071
|
const isLargeCompaction = (stats.summarized > 10) || (stats.kept > 5) || (stats.keptTokensEst > 2000);
|
|
1059
1072
|
const shouldContinueAfterAutoCompact = (reason === "threshold" || reason === "overflow" || (reason == null && isLargeCompaction)) && loadSettings(ctx).continueAfterThresholdCompact;
|
|
1060
1073
|
scheduleCompactionStatsNotify(ctx, stats);
|
|
1074
|
+
// Eager post-VCC shake chain (chainShakeHint). Host rescue already handles
|
|
1075
|
+
// dead-end; this forces a second shake entry even when headroom was made.
|
|
1076
|
+
try {
|
|
1077
|
+
const cfgChain = loadSettings(ctx);
|
|
1078
|
+
const ctxMaybe = ctx as unknown as Record<string, unknown>;
|
|
1079
|
+
const compactFn = ctxMaybe["compact"];
|
|
1080
|
+
if (cfgChain.chainShakeHint && typeof compactFn === "function" && !pendingChainShake.has(pi as unknown as object) && !willRetry && !isPiVccLast) {
|
|
1081
|
+
pendingChainShake.add(pi as unknown as object);
|
|
1082
|
+
const maybePromise = (compactFn as unknown as (o: unknown) => Promise<void>).call(ctx, { mode: "shake" } as unknown);
|
|
1083
|
+
const asPromise = maybePromise as unknown as Promise<void> | void;
|
|
1084
|
+
if (asPromise && typeof (asPromise as unknown as Promise<void>).catch === "function") {
|
|
1085
|
+
(asPromise as unknown as Promise<void>).catch(() => {}).finally(() => {
|
|
1086
|
+
setTimeout(() => { try { pendingChainShake.delete(pi as unknown as object); } catch {} }, 2000);
|
|
1087
|
+
});
|
|
1088
|
+
} else {
|
|
1089
|
+
setTimeout(() => { try { pendingChainShake.delete(pi as unknown as object); } catch {} }, 2000);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} catch {}
|
|
1061
1093
|
if (followUpPrompt) {
|
|
1062
1094
|
try {
|
|
1063
1095
|
await pi.sendUserMessage(followUpPrompt);
|
|
@@ -1288,5 +1320,4 @@ export const registerVccStatsCommand = (pi: any) => {
|
|
|
1288
1320
|
try { ctx?.ui?.notify?.(`vcc_stats: ${history.length} compaction(s)`, "info"); } catch {}
|
|
1289
1321
|
};
|
|
1290
1322
|
pi.registerCommand("vcc-stats", { description: "Show omp-vcc compaction savings (last + history)", handler });
|
|
1291
|
-
pi.registerCommand("omp-vcc-stats", { description: "Alias for /vcc-stats", handler });
|
|
1292
1323
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-vcc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Algorithmic VCC compaction for omp - fast lossless no-LLM",
|
|
5
6
|
"author": "Zhu Lin <zhulin@czl.my>",
|
|
6
7
|
"repository": {
|
|
@@ -25,7 +26,6 @@
|
|
|
25
26
|
"files": [
|
|
26
27
|
"extensions",
|
|
27
28
|
"skills",
|
|
28
|
-
"commands",
|
|
29
29
|
"scripts",
|
|
30
30
|
"types.d.ts"
|
|
31
31
|
],
|
|
@@ -34,10 +34,6 @@
|
|
|
34
34
|
"extensions": [
|
|
35
35
|
"./extensions/main.ts"
|
|
36
36
|
],
|
|
37
|
-
"commands": [
|
|
38
|
-
"./commands/omp-vcc.md",
|
|
39
|
-
"./commands/vcc-recall.md"
|
|
40
|
-
],
|
|
41
37
|
"settings": {
|
|
42
38
|
"vccEnabled": {
|
|
43
39
|
"type": "boolean",
|
|
@@ -63,6 +59,11 @@
|
|
|
63
59
|
"type": "boolean",
|
|
64
60
|
"default": false,
|
|
65
61
|
"description": "Write debug snapshot to /tmp/omp-vcc-debug.json"
|
|
62
|
+
},
|
|
63
|
+
"chainShakeHint": {
|
|
64
|
+
"type": "boolean",
|
|
65
|
+
"default": false,
|
|
66
|
+
"description": "Eager post-VCC shake chain (host rescue is automatic; this forces a second shake even when VCC created headroom)"
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
},
|
|
@@ -71,10 +72,6 @@
|
|
|
71
72
|
"extensions": [
|
|
72
73
|
"./extensions/main.ts"
|
|
73
74
|
],
|
|
74
|
-
"commands": [
|
|
75
|
-
"./commands/omp-vcc.md",
|
|
76
|
-
"./commands/vcc-recall.md"
|
|
77
|
-
],
|
|
78
75
|
"settings": {
|
|
79
76
|
"vccEnabled": {
|
|
80
77
|
"type": "boolean",
|
|
@@ -100,6 +97,11 @@
|
|
|
100
97
|
"type": "boolean",
|
|
101
98
|
"default": false,
|
|
102
99
|
"description": "Write debug snapshot to /tmp/omp-vcc-debug.json"
|
|
100
|
+
},
|
|
101
|
+
"chainShakeHint": {
|
|
102
|
+
"type": "boolean",
|
|
103
|
+
"default": false,
|
|
104
|
+
"description": "Eager post-VCC shake chain (host rescue is automatic; this forces a second shake even when VCC created headroom)"
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
},
|
|
@@ -107,6 +109,8 @@
|
|
|
107
109
|
"typecheck": "bunx tsc --noEmit",
|
|
108
110
|
"test": "bun test",
|
|
109
111
|
"smoke": "bun run scripts/smoke.ts",
|
|
112
|
+
"e2e": "bun run scripts/e2e.ts",
|
|
113
|
+
"e2e:direct": "bun test tests/e2e --timeout 120000",
|
|
110
114
|
"postuninstall": "node scripts/uninstall-reset.js || true",
|
|
111
115
|
"prepublishOnly": "npm run typecheck && npm test && npm run smoke"
|
|
112
116
|
}
|
package/scripts/e2e.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Usage: bun run e2e or bun run scripts/e2e.ts [--timeout 120000]
|
|
4
|
+
|
|
5
|
+
import { mkdtempSync, existsSync, mkdirSync, cpSync, rmSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
const timeoutArg = process.argv.find((a) => a.startsWith("--timeout"));
|
|
10
|
+
const timeout = timeoutArg ? Number(timeoutArg.split("=")[1] ?? 120000) : 120000;
|
|
11
|
+
const verbose = process.argv.includes("--verbose");
|
|
12
|
+
|
|
13
|
+
console.log("== omp-vcc E2E runner ==");
|
|
14
|
+
|
|
15
|
+
const ompDir = mkdtempSync(join(tmpdir(), "omp-vcc-e2e-runner-"));
|
|
16
|
+
const configPath = join(ompDir, "config.json");
|
|
17
|
+
console.log(`OMP_DIR=${ompDir}`);
|
|
18
|
+
console.log(`OMP_VCC_CONFIG_PATH=${configPath}`);
|
|
19
|
+
|
|
20
|
+
let failures = 0;
|
|
21
|
+
|
|
22
|
+
async function probeOmp(): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const proc = Bun.spawn(["omp", "--help"], { stdout: "pipe", stderr: "pipe" });
|
|
25
|
+
await proc.exited;
|
|
26
|
+
const out = await new Response(proc.stdout).text().catch(() => "");
|
|
27
|
+
const err = await new Response(proc.stderr).text().catch(() => "");
|
|
28
|
+
const help = out + err;
|
|
29
|
+
if (verbose) console.log(help.slice(0, 2000));
|
|
30
|
+
const hasPrint = /--print\b/.test(help);
|
|
31
|
+
const hasExtension = /--extension\b|-e\b/.test(help);
|
|
32
|
+
const hasPlugin = /\bplugin\b/.test(help);
|
|
33
|
+
console.log(`probe omp --help: hasPrint=${hasPrint} hasExtension=${hasExtension} hasPlugin=${hasPlugin}`);
|
|
34
|
+
if (!hasPlugin) console.log("note: omp plugin subcommand not found — isolated plugin link test will be skipped");
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.log(`probe failed (omp not on PATH?): ${e}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await probeOmp();
|
|
41
|
+
|
|
42
|
+
// Try plugin link in isolated dir (best effort, skip if omp missing)
|
|
43
|
+
try {
|
|
44
|
+
const linkProc = Bun.spawn(["omp", "plugin", "link", process.cwd()], {
|
|
45
|
+
env: { ...process.env, OMP_DIR: ompDir, PI_CODING_AGENT_DIR: ompDir, OMP_VCC_CONFIG_PATH: configPath, PI_VCC_CONFIG_PATH: configPath },
|
|
46
|
+
stdout: "pipe",
|
|
47
|
+
stderr: "pipe",
|
|
48
|
+
});
|
|
49
|
+
await linkProc.exited;
|
|
50
|
+
const code = linkProc.exitCode ?? 0;
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
console.log("omp plugin link ok (isolated)");
|
|
53
|
+
const doctor = Bun.spawn(["omp", "plugin", "doctor"], {
|
|
54
|
+
env: { ...process.env, OMP_DIR: ompDir, PI_CODING_AGENT_DIR: ompDir, OMP_VCC_CONFIG_PATH: configPath },
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "pipe",
|
|
57
|
+
});
|
|
58
|
+
await doctor.exited;
|
|
59
|
+
const out = await new Response(doctor.stdout).text().catch(() => "");
|
|
60
|
+
console.log(out.slice(0, 1500));
|
|
61
|
+
} else {
|
|
62
|
+
const err = await new Response(linkProc.stderr).text().catch(() => "");
|
|
63
|
+
console.log(`omp plugin link skipped or failed (code ${code}): ${err.slice(0, 500)}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log(`omp plugin link probe skipped: ${e}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`\n== running bun test tests/e2e --timeout ${timeout} ==`);
|
|
70
|
+
const env = {
|
|
71
|
+
...process.env,
|
|
72
|
+
OMP_DIR: ompDir,
|
|
73
|
+
PI_CODING_AGENT_DIR: ompDir,
|
|
74
|
+
OMP_VCC_CONFIG_PATH: configPath,
|
|
75
|
+
PI_VCC_CONFIG_PATH: configPath,
|
|
76
|
+
};
|
|
77
|
+
const testProc = Bun.spawn(["bun", "test", "tests/e2e", "--timeout", String(timeout)], {
|
|
78
|
+
env,
|
|
79
|
+
stdout: "pipe",
|
|
80
|
+
stderr: "pipe",
|
|
81
|
+
});
|
|
82
|
+
const stdoutChunks: string[] = [];
|
|
83
|
+
const stderrChunks: string[] = [];
|
|
84
|
+
// Stream
|
|
85
|
+
const outReader = testProc.stdout.getReader();
|
|
86
|
+
const errReader = testProc.stderr.getReader();
|
|
87
|
+
async function drain(reader: ReadableStreamDefaultReader<Uint8Array>, store: string[], isErr: boolean) {
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done) break;
|
|
92
|
+
const text = new TextDecoder().decode(value);
|
|
93
|
+
store.push(text);
|
|
94
|
+
if (isErr) process.stderr.write(text);
|
|
95
|
+
else process.stdout.write(text);
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
await Promise.all([drain(outReader as any, stdoutChunks, false), drain(errReader as any, stderrChunks, true)]);
|
|
100
|
+
const exitCode = await testProc.exited;
|
|
101
|
+
|
|
102
|
+
console.log(`\n== bun test exit code: ${exitCode} ==`);
|
|
103
|
+
|
|
104
|
+
// collect debug artifacts
|
|
105
|
+
const artifactsDir = join(process.cwd(), "artifacts", "e2e-debug");
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
108
|
+
for (const p of ["/tmp/omp-vcc-debug.json", "/tmp/pi-vcc-debug.json"]) {
|
|
109
|
+
if (existsSync(p)) {
|
|
110
|
+
const dest = join(artifactsDir, p.split("/").pop()!);
|
|
111
|
+
cpSync(p, dest);
|
|
112
|
+
console.log(`artifact collected: ${p} -> ${dest}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (existsSync(configPath)) {
|
|
116
|
+
cpSync(configPath, join(artifactsDir, "isolated-config.json"));
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.log(`artifact collection warning: ${e}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// cleanup isolated dir (keep artifacts)
|
|
123
|
+
try { rmSync(ompDir, { recursive: true, force: true }); } catch {}
|
|
124
|
+
console.log(`isolated OMP_DIR removed: ${ompDir}`);
|
|
125
|
+
|
|
126
|
+
if (exitCode !== 0) {
|
|
127
|
+
console.log("\nE2E FAILED");
|
|
128
|
+
process.exit(exitCode ?? 1);
|
|
129
|
+
} else {
|
|
130
|
+
console.log("\nAll E2E checks passed.");
|
|
131
|
+
}
|
package/scripts/smoke.ts
CHANGED
|
@@ -76,8 +76,16 @@ try {
|
|
|
76
76
|
commands.some((c) => c.name === "vcc-stats"),
|
|
77
77
|
);
|
|
78
78
|
check(
|
|
79
|
-
"omp-vcc-stats
|
|
80
|
-
commands.some((c) => c.name === "omp-vcc-stats"),
|
|
79
|
+
"no omp-vcc-stats duplicate",
|
|
80
|
+
!commands.some((c) => c.name === "omp-vcc-stats"),
|
|
81
|
+
);
|
|
82
|
+
check(
|
|
83
|
+
"pi-vcc-recall alias registered",
|
|
84
|
+
commands.some((c) => c.name === "pi-vcc-recall"),
|
|
85
|
+
);
|
|
86
|
+
check(
|
|
87
|
+
"no file slash command duplication (only extension commands)",
|
|
88
|
+
commands.filter((c) => c.name === "omp-vcc").length === 1,
|
|
81
89
|
);
|
|
82
90
|
} catch (e) {
|
|
83
91
|
check("extension loads", false, String(e));
|
|
@@ -1,73 +1,302 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* postuninstall hook: reset ownership marker
|
|
4
|
-
*
|
|
5
|
-
* Marker: ~/.config/@zhulinchng/omp-vcc/.ownership.json with { state: "owned"
|
|
3
|
+
* postuninstall hook: reset ownership marker and clean stale plugin entries.
|
|
4
|
+
*
|
|
5
|
+
* 1. Marker: ~/.config/@zhulinchng/omp-vcc/.ownership.json with { state: "owned" }
|
|
6
|
+
* When the plugin set `startup.quiet: true` in the global config, restore.
|
|
7
|
+
* No-op when marker missing or not owned.
|
|
8
|
+
*
|
|
9
|
+
* 2. Stale plugin entries: historic `package.json:name` renames left
|
|
10
|
+
* `@zhu/omp-vcc` → `@zhulinchng/omp-vcc` → `omp-vcc`. `omp plugin link .`
|
|
11
|
+
* under an old name creates a symlink+lock entry that survives the rename
|
|
12
|
+
* (host `PluginManager.link` did not clean same-realpath stale keys, and
|
|
13
|
+
* `PluginManager.uninstall` for linked plugins left the symlink behind).
|
|
14
|
+
* This script removes those stale symlinks+lock entries without touching
|
|
15
|
+
* correctly installed `omp-vcc` (or npm `dependencies` entries which are
|
|
16
|
+
* real directories, not symlinks).
|
|
17
|
+
* Safe to run repeatedly; also runs on extension activation (see
|
|
18
|
+
* extensions/vcc-core/core/migrate-stale.ts).
|
|
6
19
|
*/
|
|
7
|
-
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { readFileSync, rmSync, writeFileSync, existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
21
|
+
import { readFile, lstat, readdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
8
22
|
import { homedir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
23
|
+
import { join, resolve, dirname } from "node:path";
|
|
10
24
|
import { pathToFileURL } from "node:url";
|
|
11
25
|
|
|
26
|
+
const HISTORIC_NAMES = ["@zhu/omp-vcc", "@zhulinchng/omp-vcc"];
|
|
27
|
+
const CURRENT_NAME = "omp-vcc";
|
|
28
|
+
|
|
12
29
|
export function resetOwnedQuiet(home) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
".
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
const baseHome = home || homedir();
|
|
31
|
+
const markerPath = join(baseHome, ".config", "@zhulinchng/omp-vcc", ".ownership.json");
|
|
32
|
+
const configCandidates = [
|
|
33
|
+
join(baseHome, ".omp", "agent", "config.yml"),
|
|
34
|
+
join(baseHome, ".omp", "config.yml"),
|
|
35
|
+
];
|
|
36
|
+
|
|
20
37
|
let marker;
|
|
21
38
|
try {
|
|
22
39
|
marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
23
40
|
} catch {
|
|
24
41
|
return "no-marker";
|
|
25
42
|
}
|
|
43
|
+
|
|
26
44
|
const clearMarker = () => {
|
|
27
45
|
try {
|
|
28
46
|
rmSync(markerPath, { force: true });
|
|
29
47
|
} catch {}
|
|
30
48
|
};
|
|
49
|
+
|
|
31
50
|
if (marker?.state !== "owned") {
|
|
32
51
|
clearMarker();
|
|
33
52
|
return "not-owned";
|
|
34
53
|
}
|
|
54
|
+
|
|
35
55
|
let content;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
let configPath;
|
|
57
|
+
for (const p of configCandidates) {
|
|
58
|
+
try {
|
|
59
|
+
content = readFileSync(p, "utf8");
|
|
60
|
+
configPath = p;
|
|
61
|
+
break;
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
if (!configPath || content === undefined) {
|
|
39
65
|
clearMarker();
|
|
40
66
|
return "config-missing";
|
|
41
67
|
}
|
|
68
|
+
|
|
42
69
|
const lines = content.split("\n");
|
|
43
70
|
let inStartup = false;
|
|
44
71
|
let changed = false;
|
|
45
72
|
for (let i = 0; i < lines.length; i++) {
|
|
46
73
|
const line = lines[i] ?? "";
|
|
47
74
|
if (/^startup:\s*$/.test(line)) inStartup = true;
|
|
48
|
-
else if (inStartup && /^[^ \t]/.test(line) && line.trim() !== "")
|
|
49
|
-
inStartup = false;
|
|
75
|
+
else if (inStartup && /^[^ \t]/.test(line) && line.trim() !== "") inStartup = false;
|
|
50
76
|
if (inStartup && /quiet:\s*true/.test(line)) {
|
|
51
77
|
lines[i] = line.replace("true", "false");
|
|
52
78
|
changed = true;
|
|
53
79
|
break;
|
|
54
80
|
}
|
|
55
81
|
}
|
|
82
|
+
|
|
56
83
|
if (changed) {
|
|
57
84
|
try {
|
|
58
85
|
writeFileSync(configPath, lines.join("\n"), "utf8");
|
|
59
86
|
} catch {
|
|
87
|
+
clearMarker();
|
|
60
88
|
return "write-failed";
|
|
61
89
|
}
|
|
62
90
|
}
|
|
91
|
+
|
|
63
92
|
clearMarker();
|
|
64
93
|
return changed ? "restored" : "already-default";
|
|
65
94
|
}
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
// --- stale plugin cleanup (in-plugin, no host changes) ---
|
|
97
|
+
|
|
98
|
+
function isSymlinkSync(p) {
|
|
99
|
+
try {
|
|
100
|
+
return lstatSync(p).isSymbolicLink();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readJsonSync(p, fallback = null) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
109
|
+
} catch {
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove stale `omp-vcc` plugin entries left by historic package.json renames
|
|
116
|
+
* and by `omp plugin uninstall` for linked plugins (which left the symlink
|
|
117
|
+
* behind on old hosts).
|
|
118
|
+
*
|
|
119
|
+
* - Only touches lockfile-only entries (not in `package.json:dependencies`)
|
|
120
|
+
* where `package.json:name` mismatches the lock key, or where two keys
|
|
121
|
+
* point at the same realpath.
|
|
122
|
+
* - Only removes symlinks, never real directories (npm installs).
|
|
123
|
+
* - Returns a summary string for logging.
|
|
124
|
+
*/
|
|
125
|
+
export function cleanupStalePluginEntries(home) {
|
|
126
|
+
const baseHome = home || homedir();
|
|
127
|
+
const pluginsDir = join(baseHome, ".omp", "plugins");
|
|
128
|
+
const lockPath = join(pluginsDir, "omp-plugins.lock.json");
|
|
129
|
+
const pkgPath = join(pluginsDir, "package.json");
|
|
130
|
+
const nm = join(pluginsDir, "node_modules");
|
|
131
|
+
|
|
132
|
+
const lock = readJsonSync(lockPath, null);
|
|
133
|
+
if (!lock || typeof lock !== "object") return "no-lock";
|
|
134
|
+
|
|
135
|
+
const pkg = readJsonSync(pkgPath, { dependencies: {} });
|
|
136
|
+
const deps = (pkg && typeof pkg.dependencies === "object" && pkg.dependencies) || {};
|
|
137
|
+
|
|
138
|
+
const pluginKeys = lock.plugins && typeof lock.plugins === "object" ? Object.keys(lock.plugins) : [];
|
|
139
|
+
const ompKeys = pluginKeys.filter((k) => k.includes("omp-vcc"));
|
|
140
|
+
|
|
141
|
+
// If no omp-vcc keys at all, still check for orphaned symlinks at historic
|
|
142
|
+
// paths that may remain after `omp plugin uninstall` (old host left symlink).
|
|
143
|
+
const candidates = new Set([...ompKeys, ...HISTORIC_NAMES, CURRENT_NAME]);
|
|
144
|
+
|
|
145
|
+
let removedLocks = [];
|
|
146
|
+
let removedLinks = [];
|
|
147
|
+
|
|
148
|
+
// Helper to get package.json:name for a node_modules entry, without throwing
|
|
149
|
+
const getPkgName = (name) => {
|
|
150
|
+
try {
|
|
151
|
+
const jp = readJsonSync(join(nm, name, "package.json"), null);
|
|
152
|
+
return jp && typeof jp.name === "string" ? jp.name : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Helper to get realpath for dedup, null if not exists or not symlink
|
|
159
|
+
const getReal = (name) => {
|
|
160
|
+
const p = join(nm, name);
|
|
161
|
+
if (!isSymlinkSync(p)) return null;
|
|
162
|
+
try {
|
|
163
|
+
return realpathSync(p);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Map realpath -> list of lock keys that resolve to it (for dedup)
|
|
170
|
+
const realToKeys = new Map();
|
|
171
|
+
for (const k of ompKeys) {
|
|
172
|
+
const rp = getReal(k);
|
|
173
|
+
if (!rp) continue;
|
|
174
|
+
const list = realToKeys.get(rp) || [];
|
|
175
|
+
list.push(k);
|
|
176
|
+
realToKeys.set(rp, list);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 1. lock-only mismatch: lock key !== pkg name -> stale (e.g. @zhu/omp-vcc -> omp-vcc)
|
|
180
|
+
for (const k of [...ompKeys]) {
|
|
181
|
+
if (k in deps) continue; // npm-declared, keep even if name mismatches (dual publish)
|
|
182
|
+
const pkgName = getPkgName(k);
|
|
183
|
+
if (pkgName && pkgName !== k) {
|
|
184
|
+
// Historic rename leftover: lock key is old name, package is new name
|
|
185
|
+
const isHistoric = HISTORIC_NAMES.includes(k) || k !== CURRENT_NAME;
|
|
186
|
+
if (isHistoric) {
|
|
187
|
+
// Remove symlink if it exists
|
|
188
|
+
const p = join(nm, k);
|
|
189
|
+
if (isSymlinkSync(p)) {
|
|
190
|
+
try {
|
|
191
|
+
rmSync(p, { force: true });
|
|
192
|
+
removedLinks.push(k);
|
|
193
|
+
// try to remove empty scope dir e.g. .../node_modules/@zhu
|
|
194
|
+
if (k.startsWith("@")) {
|
|
195
|
+
const scopeDir = join(nm, k.split("/")[0]);
|
|
196
|
+
try {
|
|
197
|
+
if (readdirSync(scopeDir).length === 0) rmSync(scopeDir, { force: true });
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
// Remove lock entry
|
|
203
|
+
if (lock.plugins[k]) {
|
|
204
|
+
delete lock.plugins[k];
|
|
205
|
+
removedLocks.push(k);
|
|
206
|
+
}
|
|
207
|
+
if (lock.settings && lock.settings[k]) delete lock.settings[k];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. duplicate realpath: two lock keys point at same directory -> keep the
|
|
213
|
+
// one where lock key === pkg name, remove the other(s)
|
|
214
|
+
for (const [, keys] of realToKeys) {
|
|
215
|
+
if (keys.length <= 1) continue;
|
|
216
|
+
// Determine keeper: prefer CURRENT_NAME or key that matches pkg name
|
|
217
|
+
let keeper = null;
|
|
218
|
+
for (const k of keys) {
|
|
219
|
+
const pkgName = getPkgName(k);
|
|
220
|
+
if (pkgName === k) {
|
|
221
|
+
keeper = k;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!keeper) keeper = keys.includes(CURRENT_NAME) ? CURRENT_NAME : keys[0];
|
|
226
|
+
for (const k of keys) {
|
|
227
|
+
if (k === keeper) continue;
|
|
228
|
+
// Only remove if lock-only (not deps) and symlink
|
|
229
|
+
if (k in deps) continue;
|
|
230
|
+
const p = join(nm, k);
|
|
231
|
+
if (isSymlinkSync(p)) {
|
|
232
|
+
try {
|
|
233
|
+
rmSync(p, { force: true });
|
|
234
|
+
if (!removedLinks.includes(k)) removedLinks.push(k);
|
|
235
|
+
if (k.startsWith("@")) {
|
|
236
|
+
const scopeDir = join(nm, k.split("/")[0]);
|
|
237
|
+
try {
|
|
238
|
+
if (readdirSync(scopeDir).length === 0) rmSync(scopeDir, { force: true });
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
if (lock.plugins[k]) {
|
|
244
|
+
delete lock.plugins[k];
|
|
245
|
+
if (!removedLocks.includes(k)) removedLocks.push(k);
|
|
246
|
+
}
|
|
247
|
+
if (lock.settings && lock.settings[k]) delete lock.settings[k];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 3. orphaned symlinks at candidate paths that have no lock entry (old
|
|
252
|
+
// host left symlink after uninstall). Remove them if they are symlinks
|
|
253
|
+
// and their package name is omp-vcc (so we don't delete unrelated).
|
|
254
|
+
for (const cand of candidates) {
|
|
255
|
+
const p = join(nm, cand);
|
|
256
|
+
if (!isSymlinkSync(p)) continue;
|
|
257
|
+
const hasLock = !!lock.plugins[cand];
|
|
258
|
+
if (hasLock) continue; // already handled
|
|
259
|
+
const pkgName = getPkgName(cand);
|
|
260
|
+
// Only remove orphaned omp-vcc symlinks, not other plugins
|
|
261
|
+
if (pkgName === CURRENT_NAME || pkgName === "@zhu/omp-vcc" || pkgName === "@zhulinchng/omp-vcc") {
|
|
262
|
+
try {
|
|
263
|
+
rmSync(p, { force: true });
|
|
264
|
+
if (!removedLinks.includes(cand)) removedLinks.push(cand);
|
|
265
|
+
if (cand.startsWith("@")) {
|
|
266
|
+
const scopeDir = join(nm, cand.split("/")[0]);
|
|
267
|
+
try {
|
|
268
|
+
if (readdirSync(scopeDir).length === 0) rmSync(scopeDir, { force: true });
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (removedLocks.length > 0 || removedLinks.length > 0) {
|
|
276
|
+
try {
|
|
277
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
278
|
+
} catch {}
|
|
279
|
+
return `cleaned locks:${removedLocks.join(",") || "none"} links:${removedLinks.join(",") || "none"}`;
|
|
280
|
+
}
|
|
281
|
+
return "no-stale";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const isDirectRun =
|
|
68
285
|
process.argv[1] &&
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
286
|
+
(() => {
|
|
287
|
+
try {
|
|
288
|
+
return import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
if (isDirectRun) {
|
|
295
|
+
try {
|
|
296
|
+
const r1 = resetOwnedQuiet(homedir());
|
|
297
|
+
const r2 = cleanupStalePluginEntries(homedir());
|
|
298
|
+
console.log(`uninstall-reset: ${r1} ${r2}`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.log(`uninstall-reset: error ${err?.message ?? err}`);
|
|
301
|
+
}
|
|
73
302
|
}
|
package/skills/omp-vcc/SKILL.md
CHANGED
|
@@ -48,8 +48,7 @@ Use `vcc_recall` to search for prior work … Do not redo work already completed
|
|
|
48
48
|
|
|
49
49
|
| Task | How | Notes |
|
|
50
50
|
|---|---|---|
|
|
51
|
-
| **
|
|
52
|
-
| **Check savings** | `/omp-vcc --stats` · `/vcc-stats` · `/omp-vcc-stats` · `vcc_stats({history:true})` | Last + history table (50-capped). Use to confirm headroom before long edits. |
|
|
51
|
+
| **Check savings** | `/vcc-stats` · `vcc_stats({history:true})` | Last + history table (50-capped, no `omp-vcc-stats` alias). `/omp-vcc` toast is single line only; detailed savings via `/vcc-stats`. Use to confirm headroom before long edits. |
|
|
53
52
|
| **Recall search** | `/vcc-recall <query> [scope:all] [page:2]` · alias `/pi-vcc-recall` · tool `vcc_recall({query, scope, mode, page, expand})` | 5 hits/page, up to 50 total. See cookbook below. |
|
|
54
53
|
| **Stats tool** | `vcc_stats({history?: boolean})` | Same as `/vcc-stats`. `history:true` = full 50-row table. |
|
|
55
54
|
|
package/commands/omp-vcc.md
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Compact conversation with omp-vcc structured summary (keep:N + optional focus)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# /omp-vcc
|
|
6
|
-
|
|
7
|
-
Algorithmic VCC compaction — fast lossless no-LLM. Registered by `@zhulinchng/omp-vcc` extension.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
|
|
11
|
-
- `/omp-vcc` — compact with default keep:1 (smart-keep may boost to keep:N up to 25k tokens)
|
|
12
|
-
- `/omp-vcc keep:2` — keep last 2 user turns, summarize the rest
|
|
13
|
-
- `/omp-vcc keep:0` — compact all (no tail, sentinel firstKeptEntryId="")
|
|
14
|
-
- `/omp-vcc some prompt text` — compact with additional prompt/focus for the summary
|
|
15
|
-
- `/omp-vcc keep:2 some prompt text` — both keep and prompt
|
|
16
|
-
|
|
17
|
-
This command is handled by the `omp-vcc` extension (`extensions/main.ts`). It delegates to the VCC pipeline (calibrate → smart-keep → budget-cut → normalize → filter-noise → build-sections → brief transcript → format → merge) and writes a structured summary with `[Session Goal]` / `[Files And Changes]` / `[Brief transcript]` sections.
|
|
18
|
-
|
|
19
|
-
Arguments: `$ARGUMENTS`
|
package/commands/vcc-recall.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Recall earlier parts of this session via ranked search (V_adapt)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# /vcc-recall
|
|
6
|
-
|
|
7
|
-
Search compacted history — implements VCC `V_adapt` with rho predicate (regex or BM25-like ranked OR).
|
|
8
|
-
|
|
9
|
-
Registered by `@zhulinchng/omp-vcc` extension.
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
|
|
13
|
-
- `/vcc-recall` — show 25 most recent entries
|
|
14
|
-
- `/vcc-recall auth token` — keyword search (OR-ranked, TF-IDF)
|
|
15
|
-
- `/vcc-recall "hook|inject" scope:all` — regex search across all branches
|
|
16
|
-
- `/vcc-recall cache page:2` — paginated results (5 per page)
|
|
17
|
-
- `vcc_recall` tool: `{"query":"redis cache","scope":"all","page":1}` — same engine, also supports `mode:'touched'` for file index and `expand:[12,34]` or `#12:path` drill-down
|
|
18
|
-
|
|
19
|
-
The tool preserves VCC invariants: role tags, line range pointers `(#N)`, and progressive disclosure `V_ui → V_adapt → V_full[s:e]`.
|
|
20
|
-
|
|
21
|
-
Arguments: `$ARGUMENTS`
|