optbrew 0.1.1 → 0.1.3
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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/plugin.json +2 -2
- package/AGENT_INSTALL.md +92 -0
- package/CHANGELOG.md +51 -0
- package/README.ko.md +111 -36
- package/README.md +111 -36
- package/docs/platforms.md +118 -5
- package/docs/release.md +3 -2
- package/lib/cli.mjs +15 -2
- package/lib/doctor.mjs +1 -0
- package/lib/reinstall.mjs +92 -0
- package/lib/update.mjs +10 -2
- package/package.json +26 -8
- package/plugin.json +2 -2
- package/skills/optbrew/SKILL.md +2 -2
- package/skills/optbrew/bundle-manifest.json +39 -15
- package/skills/optbrew/modules/naver-searchadvisor/scripts/audit.mjs +29 -1
- package/skills/optbrew/references/ai-crawlers.json +21 -0
- package/skills/optbrew/references/ai-crawlers.md +24 -0
- package/skills/optbrew/references/api.md +2 -2
- package/skills/optbrew/references/report.md +4 -0
- package/skills/optbrew/references/scoring.md +1 -1
- package/skills/optbrew/scripts/audit.mjs +25 -30
- package/skills/optbrew/scripts/core/bundle.mjs +1 -1
- package/skills/optbrew/scripts/core/collector.mjs +40 -16
- package/skills/optbrew/scripts/core/crawlers.mjs +11 -0
- package/skills/optbrew/scripts/core/derive.mjs +193 -0
- package/skills/optbrew/scripts/core/facts.mjs +215 -0
- package/skills/optbrew/scripts/core/html.mjs +53 -0
- package/skills/optbrew/scripts/core/report.mjs +154 -43
- package/skills/optbrew/source-lock.json +1 -1
- package/source-lock.json +1 -1
package/README.md
CHANGED
|
@@ -1,73 +1,148 @@
|
|
|
1
1
|
# optbrew
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/optbrew)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
3
6
|
[한국어](README.ko.md)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
A search-readiness audit skill for coding agents. optbrew checks a site for Google, Naver and AI answer engines, fixes the local files you approve, checks again under the same conditions, and writes a scored report built from replayable evidence.
|
|
9
|
+
|
|
10
|
+
- One skill for five profiles: **SEO**, **NEO** (Naver), **AEO**, **GEO** and **LLMO**.
|
|
11
|
+
- Five pinned audit guide bundles merged into 22 check families. An issue that several guides report is counted once.
|
|
12
|
+
- Every verdict in the report can be recomputed from the raw responses that were collected and hashed.
|
|
13
|
+
- No headline score when the evidence is thin: below 80% coverage the report shows the measured part and its bounds.
|
|
14
|
+
- Works with Codex, Claude Code, Cursor, OpenCode and Pi. Scoring and reports need no API account.
|
|
15
|
+
|
|
16
|
+
## Why optbrew
|
|
17
|
+
|
|
18
|
+
Being found now means three different things: ranking in Google, showing in Naver, and being cited in AI answers. Each has its own guides, and the guides overlap and disagree. An agent asked to "improve SEO" usually picks one checklist, edits files, and reports success without proof.
|
|
19
|
+
|
|
20
|
+
optbrew gives the agent one workflow and one scoring rule for all five profiles. It counts what it measured, marks what it could not measure as unknown instead of passing it, and keeps a local fix separate from a production result. A high readiness score is not a promise of traffic or citations, and the report says so.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
1. **Audit.** You give the URL, the goal and the scope. A collector makes at most 50 requests (pages, redirects and robots or sitemap files together; depth 3, two at a time) and stores every response with its hash.
|
|
25
|
+
2. **Fix.** The agent proposes changes to local files. Nothing is edited, deployed or submitted without your approval.
|
|
26
|
+
3. **Re-check.** The same collection runs again under the same conditions, so the before and after are comparable.
|
|
27
|
+
4. **Report.** A score per profile with its coverage, blockers, open core checks, and separate tables for readiness, fixes, real outcomes and remaining work.
|
|
28
|
+
|
|
29
|
+
What the script measures by itself, and what still needs the guides and the agent's judgment:
|
|
30
|
+
|
|
31
|
+
| Profile | Target | Measured by the script | Left to judgment |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| SEO | Google and web search | Fetch status, `noindex` in meta and `X-Robots-Tag`, titles, robots.txt for Googlebot, canonical, redirect chains, discovery through links and sitemap, JSON-LD syntax | Content quality and intent fit |
|
|
34
|
+
| NEO | Naver | robots.txt for Yeti, Open Graph, description, canonical, viewport, title repetition, blocked CSS or JS | Naver-specific content guidance |
|
|
35
|
+
| AEO, GEO | Answer engines and AI search | robots.txt for the AI search bots in [ai-crawlers.md](skills/optbrew/references/ai-crawlers.md) (training bots are never scored) | Answer structure, claims, authorship, freshness |
|
|
36
|
+
| LLMO | How models describe you | Whether name, address, phone and founding year agree across pages, and whether the pages describe one business | Whether those facts are true |
|
|
6
37
|
|
|
7
|
-
|
|
38
|
+
Engine-specific checks run for the engines you name as targets. Say that Naver matters, or name the AI answer engines you care about, such as ChatGPT, Claude or Perplexity. The scoring rule is in [scoring.md](skills/optbrew/references/scoring.md). The 80% coverage threshold is optbrew's own rule, not a search engine standard.
|
|
8
39
|
|
|
9
40
|
## Install
|
|
10
41
|
|
|
11
|
-
Requires Node.js 22 or later.
|
|
42
|
+
Requires [Node.js](https://nodejs.org) 22 or later. Python 3.10 or later is optional; without it, the internal Python audit utilities stay unrun. Pick one of the five methods. The examples install for Codex and Claude Code at user level; the agent IDs are `codex`, `claude-code`, `cursor`, `opencode` and `pi`.
|
|
12
43
|
|
|
13
|
-
|
|
44
|
+
In Windows PowerShell, if `npx` or `optbrew` reports that running scripts is disabled, type `npx.cmd` or `optbrew.cmd` instead.
|
|
14
45
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
46
|
+
### 1. npm
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g optbrew@0.1.3
|
|
50
|
+
optbrew install --agents codex,claude-code --scope user --dry-run
|
|
51
|
+
optbrew install --agents codex,claude-code --scope user
|
|
52
|
+
optbrew doctor --agents codex,claude-code --scope user
|
|
19
53
|
```
|
|
20
54
|
|
|
21
|
-
|
|
55
|
+
The dry run prints the folders it will write. `doctor` checks the files and their hashes.
|
|
22
56
|
|
|
23
|
-
|
|
57
|
+
### 2. npx
|
|
24
58
|
|
|
25
|
-
|
|
59
|
+
Runs the same CLI without a global install:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx optbrew@0.1.3 install --agents codex,claude-code --scope user
|
|
63
|
+
npx optbrew@0.1.3 doctor --agents codex,claude-code --scope user
|
|
64
|
+
```
|
|
26
65
|
|
|
27
|
-
|
|
66
|
+
### 3. PowerShell (Windows)
|
|
28
67
|
|
|
29
|
-
|
|
68
|
+
```powershell
|
|
69
|
+
irm https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/install.ps1 | iex
|
|
70
|
+
```
|
|
30
71
|
|
|
31
|
-
|
|
72
|
+
The script is pinned to this release. It checks Node.js, shows the target folders, installs for Codex and Claude Code, and stops with an error if `doctor` finds a problem. To read it first, or to choose agents or a project folder:
|
|
32
73
|
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
node bin/optbrew.mjs report --input ./remote-only.json --output-dir ./report-output
|
|
74
|
+
```powershell
|
|
75
|
+
irm https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/install.ps1 -OutFile install.ps1
|
|
76
|
+
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Agents codex,claude-code,cursor
|
|
37
77
|
```
|
|
38
78
|
|
|
39
|
-
|
|
79
|
+
Add `-Scope project -Project C:\path\to\site` to install into one project.
|
|
40
80
|
|
|
41
|
-
|
|
81
|
+
### 4. Plugin
|
|
42
82
|
|
|
43
|
-
|
|
83
|
+
Claude Code:
|
|
44
84
|
|
|
45
|
-
|
|
85
|
+
```bash
|
|
86
|
+
claude plugin marketplace add nowwcastle-sudo/optbrew
|
|
87
|
+
claude plugin install optbrew@optbrew
|
|
88
|
+
```
|
|
46
89
|
|
|
47
|
-
|
|
90
|
+
Codex:
|
|
48
91
|
|
|
49
|
-
|
|
92
|
+
```bash
|
|
93
|
+
codex plugin marketplace add nowwcastle-sudo/optbrew
|
|
94
|
+
codex plugin add optbrew@optbrew
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Plugins follow this repository's `main` branch and update through the host's plugin commands.
|
|
98
|
+
|
|
99
|
+
### 5. Ask your agent
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
Paste this into Codex, Claude Code or another coding agent:
|
|
52
102
|
|
|
53
|
-
|
|
103
|
+
```text
|
|
104
|
+
Install the optbrew skill for me. Read https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/AGENT_INSTALL.md and follow it step by step. Ask me which agents and which scope to use, and show me the planned changes before you install.
|
|
105
|
+
```
|
|
54
106
|
|
|
55
|
-
|
|
107
|
+
[AGENT_INSTALL.md](AGENT_INSTALL.md) gives the agent a command and a pass check for each step, and the cases where it must stop and ask you.
|
|
56
108
|
|
|
57
|
-
|
|
109
|
+
Installing from a source checkout, the Agent Skills CLI, Pi packages and Claude web uploads are covered in [platform support](docs/platforms.md).
|
|
58
110
|
|
|
59
|
-
|
|
111
|
+
## Quick start
|
|
112
|
+
|
|
113
|
+
Start a new agent session and ask, for example:
|
|
60
114
|
|
|
61
115
|
```text
|
|
62
|
-
|
|
63
|
-
node bin/optbrew.mjs update --scope project --project ./site --json
|
|
64
|
-
node bin/optbrew.mjs rollback --scope project --project ./site --release-version 0.1.0 --json
|
|
116
|
+
Run optbrew on https://example.com. Goal: show up in Naver and get cited in AI answers.
|
|
65
117
|
```
|
|
66
118
|
|
|
67
|
-
|
|
119
|
+
optbrew asks for anything it is missing: site ownership or access, which local changes are allowed, and which evidence you already have. Raw evidence goes to `.optbrew/runs`. Private network addresses are fetched only when you declare a local target.
|
|
120
|
+
|
|
121
|
+
## Update
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm install -g optbrew@latest
|
|
125
|
+
optbrew update --scope user
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
With npx, run `npx optbrew@latest update --scope user`. `optbrew check-updates --package optbrew` tells you, without changing anything, whether npm has a newer release than the CLI you are running; `optbrew doctor --json` shows the `release_version` installed in your agent folders. `npm update` alone does not touch copies already placed in agent folders; the `update` command does.
|
|
129
|
+
|
|
130
|
+
An install from 0.1.2 or earlier stops with `migration_required`, because 0.1.3 scores with a new evaluation version. Scores from different evaluation versions are not comparable. Follow [Reinstall after a migration_required update](docs/platforms.md#reinstall-after-a-migration_required-update); the steps delete only paths that a read-only check printed and never touch your audit evidence.
|
|
131
|
+
|
|
132
|
+
## APIs and cost
|
|
133
|
+
|
|
134
|
+
Nothing here requires a paid account. A direct Anthropic API call or an [SE Ranking](https://seranking.com/api.html) call needs your approval, a recorded budget and a stop condition first; a Claude subscription does not include API billing. Keys stay in your environment variables and never go into a report. Details: [API boundaries](skills/optbrew/references/api.md).
|
|
135
|
+
|
|
136
|
+
## Documentation
|
|
137
|
+
|
|
138
|
+
- [Platform support](docs/platforms.md): install paths per agent, checkout commands, reinstall steps
|
|
139
|
+
- [Scoring](skills/optbrew/references/scoring.md), [measurement](skills/optbrew/references/measurement.md) and [report evidence](skills/optbrew/references/report.md)
|
|
140
|
+
- [Changelog](CHANGELOG.md) and [release process](docs/release.md)
|
|
141
|
+
|
|
142
|
+
## Contributing
|
|
68
143
|
|
|
69
|
-
|
|
144
|
+
Contributions are welcome: bug reports, false positives from a real site, guide updates, translations and code. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
70
145
|
|
|
71
|
-
|
|
146
|
+
## License
|
|
72
147
|
|
|
73
|
-
|
|
148
|
+
optbrew's own code is [MIT licensed](LICENSE). The bundled guides keep their own licenses, listed in [third-party notices](skills/optbrew/THIRD_PARTY_NOTICES.md). The Naver bundle keeps an index of the official [Search Advisor guides](https://searchadvisor.naver.com/guide) but not the 55 article bodies; follow the official links while online.
|
package/docs/platforms.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Platform installation and update paths
|
|
2
2
|
|
|
3
|
-
Optbrew v0.1.
|
|
3
|
+
Optbrew v0.1.3 contains one skill source at `skills/optbrew`. The managed CLI can place it at five hosts' documented default paths. A file in a host path proves placement, not that the host discovered, loaded, or invoked the skill. Confirm those three steps in a fresh host session before reporting host support as exercised.
|
|
4
4
|
|
|
5
5
|
| Host | Managed project target | Managed user target | Discovery and invocation boundary |
|
|
6
6
|
|---|---|---|---|
|
|
@@ -14,17 +14,130 @@ The CLI maps only these default targets. Custom roots, host policies, remote age
|
|
|
14
14
|
|
|
15
15
|
## Managed checkout and npm paths
|
|
16
16
|
|
|
17
|
-
The [README](../README.md) shows
|
|
17
|
+
The [README](../README.md) shows the npm, npx, PowerShell, plugin and agent install routes. This section holds the parser-checked commands for a source checkout. Clone `https://github.com/nowwcastle-sudo/optbrew`, create an empty `site` folder, and run them from the repository root. With the npm package installed, replace `node bin/optbrew.mjs` with `optbrew`.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Install into an isolated project folder, after a dry run:
|
|
20
|
+
|
|
21
|
+
Bash and PowerShell use the same commands:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --dry-run --json
|
|
25
|
+
node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --json
|
|
26
|
+
node bin/optbrew.mjs doctor --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--strategy link` is the default; `copy` also works when links are unavailable. The managed record lives under that project's `.optbrew/install`. For one host, replace the comma-separated list with its exact ID. Use `--scope user` only when you intend to install into user-level host directories. The install command checks the bundle manifest and refuses to replace an unmanaged target.
|
|
30
|
+
|
|
31
|
+
Replay the bundled synthetic fixtures without any provider call. Copy `tests/fixtures/scoring/before.json` and `after.json`, and `tests/fixtures/reports/remote-only.json` with its sibling evidence files, into the current directory first:
|
|
32
|
+
|
|
33
|
+
Bash and PowerShell use the same commands:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
node bin/optbrew.mjs score --input ./before.json --output ./score.json
|
|
37
|
+
node bin/optbrew.mjs compare --before ./before.json --after ./after.json --output ./comparison.json
|
|
38
|
+
node bin/optbrew.mjs report --input ./remote-only.json --output-dir ./report-output
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Check for updates, update, and roll back a managed project install:
|
|
42
|
+
|
|
43
|
+
Bash and PowerShell use the same commands:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
node bin/optbrew.mjs check-updates --json
|
|
47
|
+
node bin/optbrew.mjs update --scope project --project ./site --json
|
|
48
|
+
node bin/optbrew.mjs rollback --scope project --project ./site --release-version 0.1.0 --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The shown rollback fails on a fresh 0.1.3 install, because no earlier release is retained; it applies to a managed install that retains local 0.1.0.
|
|
52
|
+
|
|
53
|
+
The CLI validates bundle hashes before an install or update. `update` replaces every target in one managed record as a unit. An edited target makes the default update stop; the explicit `--local-changes backup-and-replace` policy retains a readable backup. `rollback --release-version VERSION` works only when that version is in the record's retained prior releases. A fresh installation has no older release.
|
|
54
|
+
|
|
55
|
+
After a public npm release, `npm install -g optbrew` and `npm update -g optbrew` manage the package in npm. Neither command rewrites an old copied skill target. Run the installed CLI's managed `update` after the package update, then `doctor` and a new-session invocation check. `check-updates --package optbrew --json` is read-only and compares the installed release with the npm registry; without `--package` it returns `local_only`.
|
|
56
|
+
|
|
57
|
+
## Reinstall after a migration_required update
|
|
58
|
+
|
|
59
|
+
Release 0.1.3 uses evaluation version 0.3.0; 0.1.2 used 0.2.0. The managed updater refuses a change of evaluation version, so `optbrew update` on a managed 0.1.2 install stops with `migration_required` and changes nothing. There is no `uninstall` command. Replace the install by hand: the blocks below delete the install's targets and then its managed root, and run `install` again from the 0.1.3 checkout. Scores and baselines made with 0.1.2 are not comparable with 0.1.3; `compare` returns `not_comparable` (`evaluation_version_changed`). Run the baseline audit again with 0.1.3.
|
|
60
|
+
|
|
61
|
+
The blocks do not trust the install record. Each one first runs the read-only `node bin/optbrew.mjs reinstall-plan`, which resolves the home directory the way the CLI does (`HOME`, else the operating system's home directory), checks that the record belongs to this scope, that every recorded target is exactly the agent's `skills/optbrew` path (absolute, no `..`, no linked folder on the way, inside the scope or project), and that an existing target is a link to the managed bundle or a real folder holding a bundle manifest. If any check fails it prints the reason, exits with a nonzero code, and the block deletes nothing. Otherwise the shell deletes only the paths it printed: the targets first, then `<scope root>/.optbrew/installs/user` (user scope) or `<project>/.optbrew/install` (project scope), which holds the install record. Because the record goes last, you can run the same block again after a failure. The commands never touch `<scope root>/.optbrew/runs`, where your audit evidence lives; do not delete the whole `.optbrew` folder. Each agent is installed again with the strategy (`link` or `copy`) the record had, then `doctor` runs. Run the blocks from the 0.1.3 checkout root. If `reinstall-plan` refuses the record, fix or remove the named path by hand and look at it first; the CLI also refuses to overwrite a target it does not own.
|
|
62
|
+
|
|
63
|
+
User scope:
|
|
64
|
+
|
|
65
|
+
```powershell user
|
|
66
|
+
$ErrorActionPreference = 'Stop'
|
|
67
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
68
|
+
$plan = @(node bin/optbrew.mjs reinstall-plan --scope user)
|
|
69
|
+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
|
|
70
|
+
foreach ($line in $plan) {
|
|
71
|
+
$f = $line -split "`t"
|
|
72
|
+
if ($f[0] -eq 'delete-link') { [IO.Directory]::Delete($f[1]) }
|
|
73
|
+
elseif ($f[0] -eq 'delete-dir') { Remove-Item -Recurse -Force -LiteralPath $f[1] }
|
|
74
|
+
}
|
|
75
|
+
foreach ($line in $plan) {
|
|
76
|
+
$f = $line -split "`t"
|
|
77
|
+
if ($f[0] -eq 'install') { node bin/optbrew.mjs install --agents $f[1] --scope user --strategy $f[2]; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
|
|
78
|
+
elseif ($f[0] -eq 'agents') { node bin/optbrew.mjs doctor --agents $f[1] --scope user; exit $LASTEXITCODE }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```bash user
|
|
83
|
+
set -e
|
|
84
|
+
plan=$(node bin/optbrew.mjs reinstall-plan --scope user)
|
|
85
|
+
while IFS=$'\t' read -r kind a b; do
|
|
86
|
+
case "$kind" in delete-link | delete-dir) rm -r -- "$a" ;; esac
|
|
87
|
+
done <<< "$plan"
|
|
88
|
+
while IFS=$'\t' read -r kind a b; do
|
|
89
|
+
case "$kind" in
|
|
90
|
+
install) node bin/optbrew.mjs install --agents "$a" --scope user --strategy "$b" ;;
|
|
91
|
+
agents) node bin/optbrew.mjs doctor --agents "$a" --scope user ;;
|
|
92
|
+
esac
|
|
93
|
+
done <<< "$plan"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Project scope (replace `./site` with your project folder; it appears once):
|
|
97
|
+
|
|
98
|
+
```powershell project
|
|
99
|
+
$ErrorActionPreference = 'Stop'
|
|
100
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
101
|
+
$project = './site'
|
|
102
|
+
$plan = @(node bin/optbrew.mjs reinstall-plan --scope project --project $project)
|
|
103
|
+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
|
|
104
|
+
foreach ($line in $plan) {
|
|
105
|
+
$f = $line -split "`t"
|
|
106
|
+
if ($f[0] -eq 'delete-link') { [IO.Directory]::Delete($f[1]) }
|
|
107
|
+
elseif ($f[0] -eq 'delete-dir') { Remove-Item -Recurse -Force -LiteralPath $f[1] }
|
|
108
|
+
}
|
|
109
|
+
foreach ($line in $plan) {
|
|
110
|
+
$f = $line -split "`t"
|
|
111
|
+
if ($f[0] -eq 'install') { node bin/optbrew.mjs install --agents $f[1] --scope project --project $project --strategy $f[2]; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
|
|
112
|
+
elseif ($f[0] -eq 'agents') { node bin/optbrew.mjs doctor --agents $f[1] --scope project --project $project; exit $LASTEXITCODE }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```bash project
|
|
117
|
+
set -e
|
|
118
|
+
project='./site'
|
|
119
|
+
plan=$(node bin/optbrew.mjs reinstall-plan --scope project --project "$project")
|
|
120
|
+
while IFS=$'\t' read -r kind a b; do
|
|
121
|
+
case "$kind" in delete-link | delete-dir) rm -r -- "$a" ;; esac
|
|
122
|
+
done <<< "$plan"
|
|
123
|
+
while IFS=$'\t' read -r kind a b; do
|
|
124
|
+
case "$kind" in
|
|
125
|
+
install) node bin/optbrew.mjs install --agents "$a" --scope project --project "$project" --strategy "$b" ;;
|
|
126
|
+
agents) node bin/optbrew.mjs doctor --agents "$a" --scope project --project "$project" ;;
|
|
127
|
+
esac
|
|
128
|
+
done <<< "$plan"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Inside 0.1.3 and later releases that keep evaluation version 0.3.0, `update` and `rollback` work as before. The same refusal and steps apply to any future release that changes the evaluation version.
|
|
20
132
|
|
|
21
133
|
## Native host routes
|
|
22
134
|
|
|
23
135
|
These are alternatives to the managed CLI. Do not mix their ownership records for the same target path.
|
|
24
136
|
|
|
25
137
|
- **Agent Skills tool:** Current [Vercel Skills CLI](https://github.com/vercel-labs/skills) documents `npx skills add ./` for a local source and `npx skills update optbrew` for an installation it owns. Run from a checkout containing the skill. Confirm its host selection and destination before writing. The public repository is `https://github.com/nowwcastle-sudo/optbrew`.
|
|
26
|
-
- **Claude Code plugin:**
|
|
27
|
-
- **
|
|
138
|
+
- **Claude Code plugin:** `.claude-plugin/marketplace.json` and `plugin.json` name the plugin `optbrew@optbrew` at version 0.1.3. Run `claude plugin marketplace add nowwcastle-sudo/optbrew`, then `claude plugin install optbrew@optbrew` (add `--scope project` to keep it in one project). Later, `claude plugin update optbrew@optbrew` applies the marketplace's newer version and requires a restart. `claude plugin validate .` checks both files from a checkout.
|
|
139
|
+
- **Codex plugin:** `.agents/plugins/marketplace.json` lists the same `optbrew` plugin. Run `codex plugin marketplace add nowwcastle-sudo/optbrew`, then `codex plugin add optbrew@optbrew`, or choose it in `/plugins`. Start a new Codex task afterwards. [Claude Code plugin instructions](https://code.claude.com/docs/en/plugins) also describe the interactive `/plugin` path. Marketplace auto-update is a user setting; the marketplace file does not enable it. Verify the selected release version and a fresh-session invocation after a native update.
|
|
140
|
+
- **Pi package:** Current [Pi package instructions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) document `pi install ./local-package`, `pi install npm:package-name@version`, `pi list`, and `pi update --extensions`. `pi install ./` installs from a local checkout when Pi is installed; `pi install npm:optbrew@0.1.3` installs the published npm package. The package's `pi.skills` entry points to `./skills/optbrew`. A project-local package declaration uses Pi's `--local` option and is subject to project trust. Check the installed Pi version's help and package list before updating.
|
|
28
141
|
- **Claude web:** Upload the built skill ZIP manually in the supported web interface. Upload a new ZIP again for each update. There is no verified automatic update of that upload.
|
|
29
142
|
- **Ordinary ChatGPT chat:** Attach the relevant skill files or ZIP and give the assistant the target and scope. This is attachment-guided use. It is not the Codex installation path and does not establish Work/desktop plugin discovery.
|
|
30
143
|
|
package/docs/release.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Release procedure
|
|
2
2
|
|
|
3
|
-
This checkout is version 0.1.
|
|
3
|
+
This checkout is version 0.1.3. A package name, local manifest, or generated ZIP does not establish a public release. Publication, a Git tag, a push, and account changes require their own authorization. This page describes the reviewable local release artifacts and the checks to run before that decision.
|
|
4
4
|
|
|
5
5
|
1. Confirm the source commit and clean intended diff. Match `package.json`, the portable plugin, Codex plugin, Claude plugin, marketplace entry, and ZIP name to one release version. Keep `evaluation_version` and `protocol_version` separate from the release version.
|
|
6
6
|
2. Run `npm test` and `python -m unittest discover -s tests/python` on Python 3.10 or later. Run the focused `node --test tests/docs-commands.test.mjs` check when changing examples. The command check runs optbrew against isolated local fixtures; it makes no provider call.
|
|
7
|
-
3. Run `npm run build-bundle`, `node scripts/build-release.mjs --version 0.1.
|
|
7
|
+
3. Run `npm run build-bundle`, `node scripts/build-release.mjs --version 0.1.3 --output-dir dist`, `npm pack --pack-destination dist`, `node scripts/verify-release.mjs --dist dist`, and `npm pack --dry-run --json` from the repository root. Rebuild after changing the README or packaged files. Do not treat an older tarball as containing a later documentation edit.
|
|
8
8
|
4. Inspect the npm file list, the ZIP file list, `bundle-manifest.json`, `source-lock.json`, licenses, and secret scan output. The same skill bytes and version must be traceable across the npm package and ZIP. Source notices belong in both. User run evidence, account configuration, keys, private cards, and authoring materials must stay outside both.
|
|
9
9
|
5. Test a fresh isolated managed install, `doctor`, update from a retained earlier release, rollback, and fresh host discovery or invocation where a host is available. A dry run or manifest check does not prove invocation. Record the untested hosts and capabilities.
|
|
10
10
|
6. Only after a reviewed release decision, publish or upload the approved artifacts and verify the public npm and repository readback. Do not infer publication from successful local builds.
|
|
11
|
+
7. `main` is protected by a repository ruleset: no force push, no deletion, changes only through a pull request, linear history. Tags matching `v*` cannot be moved or deleted. Merge the release branch through a pull request, then tag the merge commit `vX.Y.Z`. The README's PowerShell installer and agent prompt load `install.ps1` and `AGENT_INSTALL.md` from that tag, so test both raw URLs after the tag is pushed.
|
|
11
12
|
|
|
12
13
|
The bundle includes five pinned source bundles plus seven selected marketing guides. The Naver source articles under `references/guide/` (55 original bodies) remain excluded; the index links to official pages and full offline article lookup is unavailable. The published notice must retain upstream MIT/ISC permission texts and modifications. Optbrew's own code uses MIT. These code licenses do not grant service subscriptions or API data rights.
|
|
13
14
|
|
package/lib/cli.mjs
CHANGED
|
@@ -8,10 +8,11 @@ import { checkUpdates, update, rollback } from './update.mjs';
|
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
9
|
import { readInstallRecord } from './records.mjs';
|
|
10
10
|
import { runCli } from '../skills/optbrew/scripts/cli.mjs';
|
|
11
|
+
import { reinstallPlan } from './reinstall.mjs';
|
|
11
12
|
|
|
12
13
|
const defaultBundle = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../skills/optbrew');
|
|
13
14
|
class UsageError extends Error {}
|
|
14
|
-
const usage = 'usage: optbrew <install|doctor|check-updates|update|rollback|score|compare|report> [options]';
|
|
15
|
+
const usage = 'usage: optbrew <install|doctor|check-updates|update|rollback|reinstall-plan|score|compare|report> [options]';
|
|
15
16
|
|
|
16
17
|
function parseOptions(argv, allowed) {
|
|
17
18
|
const options = {};
|
|
@@ -61,7 +62,19 @@ export async function main(argv, { stdout = process.stdout, stderr = process.std
|
|
|
61
62
|
stdout.write(`${command} complete\n`);
|
|
62
63
|
return 0;
|
|
63
64
|
}
|
|
64
|
-
if (!['install', 'doctor', 'check-updates', 'update', 'rollback'].includes(command)) throw new UsageError(usage);
|
|
65
|
+
if (!['install', 'doctor', 'check-updates', 'update', 'rollback', 'reinstall-plan'].includes(command)) throw new UsageError(usage);
|
|
66
|
+
if (command === 'reinstall-plan') {
|
|
67
|
+
// Read-only: validates the install record and prints what the documented manual reinstall may delete.
|
|
68
|
+
const parsed = parseOptions(rest, ['--scope', '--home', '--project']);
|
|
69
|
+
const scope = parsed['--scope'] ?? 'user';
|
|
70
|
+
if (!['user', 'project'].includes(scope)) throw new UsageError('--scope must be user or project');
|
|
71
|
+
if (scope === 'project' && !parsed['--project']) throw new UsageError('project scope requires --project');
|
|
72
|
+
if (scope === 'user' && parsed['--project']) throw new UsageError('--project requires project scope');
|
|
73
|
+
const rows = await reinstallPlan({ scope, home: parsed['--home'] ? path.resolve(cwd, parsed['--home']) : env?.HOME ?? os.homedir(),
|
|
74
|
+
projectRoot: parsed['--project'] ? path.resolve(cwd, parsed['--project']) : undefined });
|
|
75
|
+
stdout.write(`${rows.map(row => row.join('\t')).join('\n')}\n`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
65
78
|
if (command === 'check-updates') {
|
|
66
79
|
const parsed = parseOptions(rest, ['--package', '--json']);
|
|
67
80
|
const manifest = JSON.parse(await readFile(path.join(bundleRoot, 'bundle-manifest.json'), 'utf8'));
|
package/lib/doctor.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export async function doctor({ agents, scope, home, projectRoot }) {
|
|
|
30
30
|
if (saved.strategy === 'copy' && (await lstat(target.path)).isSymbolicLink()) throw new Error('copy replaced by link');
|
|
31
31
|
const manifest = JSON.parse(await readFile(path.join(target.path, 'bundle-manifest.json'), 'utf8'));
|
|
32
32
|
const check = await verifyBundle({ bundleRoot: target.path, manifest });
|
|
33
|
+
if (check.errors.some(e => e.code === 'migration_required')) warnings.push(`${target.agent}: installed release ${manifest.release_version} was built for another evaluation version; update is refused (migration_required). See docs/platforms.md, "Reinstall after a migration_required update".`);
|
|
33
34
|
hashOk = !recoveryPending && record.transaction.state === 'committed' && check.status === 'valid' && check.bundle_hash === record.bundle_hash && manifest.release_version === record.release_version;
|
|
34
35
|
} catch (error) { warnings.push(`${target.agent}: ${error.message}`); }
|
|
35
36
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readInstallRecord } from './records.mjs';
|
|
4
|
+
import { scopePaths } from './paths.mjs';
|
|
5
|
+
import { validateScope, statOrNull } from './transaction.mjs';
|
|
6
|
+
|
|
7
|
+
const powershell = value => `'${String(value).replaceAll("'", "''")}'`;
|
|
8
|
+
const posix = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read-only plan for the manual reinstall of a managed scope. It validates the install record before anything is
|
|
12
|
+
* deleted: the record belongs to this scope, every recorded target is exactly the expected skills/optbrew path of its
|
|
13
|
+
* agent (absolute, no "..", no linked ancestor, inside the scope), and every target that exists is what the installer
|
|
14
|
+
* created (a link to the managed bundle, or a real directory holding a bundle manifest). Any failed check throws, so a
|
|
15
|
+
* caller that deletes only what this returns never deletes anything for a tampered record.
|
|
16
|
+
*
|
|
17
|
+
* Rows: `delete-link|delete-dir <path>` (recorded targets first, the managed root last), then `install <agent> <strategy>`
|
|
18
|
+
* per recorded agent in record order, then `agents <csv>` for the final doctor call.
|
|
19
|
+
*/
|
|
20
|
+
export async function reinstallPlan({ scope, home, projectRoot }) {
|
|
21
|
+
const refuse = message => { throw new Error(`reinstall plan refused: ${message}`); };
|
|
22
|
+
// One output line per path: a line break, tab or other control character would split a row or inject another one.
|
|
23
|
+
const plain = value => typeof value === 'string' && [...value].every(ch => { const c = ch.codePointAt(0); return c > 31 && (c < 127 || c > 159) && c !== 8232 && c !== 8233; });
|
|
24
|
+
const { scopeRoot, managedRoot } = scopePaths({ scope, home, projectRoot });
|
|
25
|
+
if (!plain(scopeRoot) || !plain(managedRoot)) refuse('the scope path contains a control character (a line break, tab or similar)');
|
|
26
|
+
const record = await readInstallRecord(managedRoot).catch(error => refuse(`install record unreadable (${error.message})`));
|
|
27
|
+
if (!record) refuse(`no install record in ${managedRoot}`);
|
|
28
|
+
if (record.scope_id !== `${scope}:${scopeRoot}` || record.managed_root !== managedRoot) refuse('the install record belongs to another scope');
|
|
29
|
+
if (!record.targets.length) refuse('the install record lists no target');
|
|
30
|
+
for (const target of record.targets) {
|
|
31
|
+
if (!plain(target.path) && typeof target.path === 'string') refuse('a recorded target path contains a control character (a line break, tab or similar)');
|
|
32
|
+
if (typeof target.path !== 'string' || !path.isAbsolute(target.path) || target.path.split(/[\\/]/).includes('..'))
|
|
33
|
+
refuse(`recorded target is not a plain absolute path: ${JSON.stringify(target.path)}`);
|
|
34
|
+
}
|
|
35
|
+
try { await validateScope(managedRoot, record); }
|
|
36
|
+
catch (error) { refuse(`${error.message} (expected exactly the agent's skills/optbrew path inside ${scopeRoot})`); }
|
|
37
|
+
const managedBundle = path.join(managedRoot, 'bundle');
|
|
38
|
+
const rows = [];
|
|
39
|
+
for (const target of record.targets) {
|
|
40
|
+
const found = await statOrNull(target.path);
|
|
41
|
+
if (!found) continue;
|
|
42
|
+
if (found.isSymbolicLink()) {
|
|
43
|
+
if (target.strategy !== 'link' || await realpath(target.path) !== await realpath(managedBundle))
|
|
44
|
+
refuse(`${target.path} is a link that does not lead to the managed bundle`);
|
|
45
|
+
rows.push(['delete-link', target.path]);
|
|
46
|
+
} else if (found.isDirectory()) {
|
|
47
|
+
if (target.strategy !== 'copy') refuse(`${target.path} was recorded as a link but is a directory`);
|
|
48
|
+
try { await lstat(path.join(target.path, 'bundle-manifest.json')); }
|
|
49
|
+
catch { refuse(`${target.path} holds no optbrew bundle manifest`); }
|
|
50
|
+
rows.push(['delete-dir', target.path]);
|
|
51
|
+
} else refuse(`${target.path} is neither a link nor a directory`);
|
|
52
|
+
}
|
|
53
|
+
rows.push(['delete-dir', managedRoot]);
|
|
54
|
+
for (const target of record.targets) rows.push(['install', target.agent, target.strategy]);
|
|
55
|
+
rows.push(['agents', record.targets.map(target => target.agent).join(',')]);
|
|
56
|
+
if (!rows.every(row => row.every(plain))) refuse('a path or name in the plan contains a control character (a line break, tab or similar)');
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Exact manual reinstall steps for a managed scope whose update was refused with migration_required.
|
|
62
|
+
* Targets are deleted first and the managed root (which holds the install record) last, so the steps can be repeated
|
|
63
|
+
* after a partial failure. `.optbrew/runs` is never deleted.
|
|
64
|
+
*/
|
|
65
|
+
export function reinstallGuidance({ record, recordRoot, from, to }) {
|
|
66
|
+
const separator = record.scope_id.indexOf(':');
|
|
67
|
+
const scope = record.scope_id.slice(0, separator), scopeRoot = record.scope_id.slice(separator + 1);
|
|
68
|
+
const agents = record.targets.map(target => target.agent).join(',');
|
|
69
|
+
const place = quote => scope === 'user' ? `--scope user --home ${quote(scopeRoot)}` : `--scope project --project ${quote(scopeRoot)}`;
|
|
70
|
+
const removePowershell = record.targets.map(target => target.strategy === 'link' ?
|
|
71
|
+
`[IO.Directory]::Delete(${powershell(target.path)})` : `Remove-Item -Recurse -Force -LiteralPath ${powershell(target.path)}`);
|
|
72
|
+
removePowershell.push(`Remove-Item -Recurse -Force -LiteralPath ${powershell(recordRoot)}`);
|
|
73
|
+
const removePosix = `rm -r -- ${[...record.targets.map(target => target.path), recordRoot].map(posix).join(' ')}`;
|
|
74
|
+
const install = quote => record.targets.map(target =>
|
|
75
|
+
`optbrew install --agents ${target.agent} ${place(quote)} --strategy ${target.strategy}`);
|
|
76
|
+
const doctor = quote => `optbrew doctor --agents ${agents} ${place(quote)}`;
|
|
77
|
+
return [
|
|
78
|
+
`Release ${to} uses a new evaluation contract, so a managed update from ${from} cannot carry this install over, and scores from the two releases are not comparable. Reinstall instead; your runs folder (${scopeRoot}/.optbrew/runs) is not touched, so never delete it.`,
|
|
79
|
+
'1. Check the install record first. This read-only command refuses a tampered record and prints exactly what will be deleted:',
|
|
80
|
+
` optbrew reinstall-plan ${place(posix)}`,
|
|
81
|
+
'2. Delete only those paths, the targets first and the managed root last (repeat the same steps if one fails):',
|
|
82
|
+
' PowerShell:',
|
|
83
|
+
...removePowershell.map(line => ` ${line}`),
|
|
84
|
+
` POSIX shell: ${removePosix}`,
|
|
85
|
+
'3. Install again, one command per recorded agent, from the checkout of the new release:',
|
|
86
|
+
...['PowerShell', 'POSIX shell'].flatMap((label, i) => [` ${label}:`, ...install(i ? posix : powershell).map(line => ` ${line}`)]),
|
|
87
|
+
'4. Verify:',
|
|
88
|
+
` PowerShell: ${doctor(powershell)}`,
|
|
89
|
+
` POSIX shell: ${doctor(posix)}`,
|
|
90
|
+
'The same steps as copy-paste blocks that do these checks for you: docs/platforms.md, "Reinstall after a migration_required update".',
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
package/lib/update.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createBundleManifest } from '../skills/optbrew/scripts/core/bundle.mjs';
|
|
5
5
|
import { readInstallRecord, writeDurableJson, writeInstallRecord } from './records.mjs';
|
|
6
|
+
import { reinstallGuidance } from './reinstall.mjs';
|
|
6
7
|
import { assertManagedPath, copyBundle, fingerprint, finishJournal, managedRename, operationPaths,
|
|
7
8
|
readJournal, recoverTransaction, statOrNull, validateScope, verifiedBundle, withScopeLock } from './transaction.mjs';
|
|
8
9
|
|
|
@@ -110,9 +111,16 @@ async function applyRelease({ bundleRoot, recordRoot, localChanges, faultHook, k
|
|
|
110
111
|
const managed = path.join(recordRoot, 'bundle');
|
|
111
112
|
let previousRoot = managed;
|
|
112
113
|
if (conflicts.some(c => c.path === managed)) previousRoot = before.source;
|
|
113
|
-
|
|
114
|
+
// The guard stays as is. A refusal for another evaluation version carries the exact manual reinstall steps.
|
|
115
|
+
const refuse = error => {
|
|
116
|
+
if (!/migration_required/.test(error.message)) throw error;
|
|
117
|
+
const reason = error.message.startsWith('migration_required') ? error.message
|
|
118
|
+
: 'migration_required: evaluation_version changed (the installed bundle was built for another evaluation version)';
|
|
119
|
+
throw new Error(`${reason}\n${reinstallGuidance({ record: before, recordRoot, from: before.release_version, to: source.manifest.release_version })}`);
|
|
120
|
+
};
|
|
121
|
+
const previous = await verifiedBundle(previousRoot).catch(refuse);
|
|
114
122
|
if (!matchesRecord(previous.manifest, before)) throw new Error('previous verified bundle unavailable; preserve edits and recover original release first');
|
|
115
|
-
await compatible(previous.manifest, source.manifest);
|
|
123
|
+
await compatible(previous.manifest, source.manifest).catch(refuse);
|
|
116
124
|
const id = randomUUID();
|
|
117
125
|
const snapshotRoot = path.join(recordRoot, 'releases', id, 'bundle');
|
|
118
126
|
await assertManagedPath(scopeRoot, snapshotRoot);
|
package/package.json
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "optbrew",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pinned search audit and improvement skill bundle",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nowwcastle-sudo/optbrew.git"
|
|
10
|
+
},
|
|
8
11
|
"homepage": "https://github.com/nowwcastle-sudo/optbrew#readme",
|
|
9
|
-
"bugs": {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nowwcastle-sudo/optbrew/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"seo",
|
|
18
|
+
"agent-skill"
|
|
19
|
+
],
|
|
20
|
+
"pi": {
|
|
21
|
+
"skills": [
|
|
22
|
+
"./skills/optbrew"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"optbrew": "bin/optbrew.mjs"
|
|
30
|
+
},
|
|
14
31
|
"files": [
|
|
15
32
|
"bin/",
|
|
16
33
|
"lib/",
|
|
@@ -20,6 +37,7 @@
|
|
|
20
37
|
".claude-plugin/",
|
|
21
38
|
"README.md",
|
|
22
39
|
"README.ko.md",
|
|
40
|
+
"AGENT_INSTALL.md",
|
|
23
41
|
"CHANGELOG.md",
|
|
24
42
|
"docs/platforms.md",
|
|
25
43
|
"docs/release.md",
|
|
@@ -34,7 +52,7 @@
|
|
|
34
52
|
],
|
|
35
53
|
"scripts": {
|
|
36
54
|
"test": "node --test",
|
|
37
|
-
"build-bundle": "node scripts/build-bundle.mjs --bundle skills/optbrew --version 0.1.
|
|
55
|
+
"build-bundle": "node scripts/build-bundle.mjs --bundle skills/optbrew --version 0.1.3",
|
|
38
56
|
"vendor": "node scripts/vendor.mjs",
|
|
39
57
|
"scan-secrets": "node scripts/scan-secrets.mjs --tracked-and-staged"
|
|
40
58
|
}
|