opensocrates 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to OpenSocrates are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [1.0.0] - 2026-07-31
7
+
8
+ ### Added
9
+
10
+ - Local Codex selector and runtime with a 30-second, no-retry, fail-open
11
+ execution contract.
12
+ - An authored catalog of 48 reasoning systems with English and Korean
13
+ user-facing messages.
14
+ - Evidence, completion, routing, privacy, trace, and host-contract validation.
15
+ - Reproducible Python 3.12 builds, native `darwin-arm64` packaging, checksums,
16
+ SBOM generation, security scanning, and release gates.
17
+ - GitHub Release and dependency-free Node.js installation paths, including
18
+ `npx github:ParkerHwang/OpenSocrates#v1.0.0 install`.
19
+ - The public `opensocrates` npm package for the shorter
20
+ `npx opensocrates@1.0.0 install` path.
21
+ - English and Korean README files and standard open-source community health
22
+ documents.
23
+
24
+ ### Security and privacy
25
+
26
+ - The selector uses the existing local Codex OAuth session; no API key or
27
+ hosted OpenSocrates backend is required.
28
+ - Temporary instruction artifacts contain authored OpenSocrates material only.
29
+ - Installer downloads are checked against release and package-level SHA-256
30
+ manifests before installation.
31
+
32
+ ### Known limitations
33
+
34
+ - The packaged runtime is validated only for Apple-silicon macOS.
35
+ - Signing, notarization, clean-machine installation, and live host hook
36
+ delivery remain explicitly unvalidated.
37
+
38
+ [1.0.0]: https://github.com/ParkerHwang/OpenSocrates/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parker Hwang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,182 @@
1
+ # OpenSocrates
2
+
3
+ [English](README.md) | **한국어**
4
+
5
+ [![CI](https://github.com/ParkerHwang/OpenSocrates/actions/workflows/ci.yml/badge.svg)](https://github.com/ParkerHwang/OpenSocrates/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/opensocrates)](https://www.npmjs.com/package/opensocrates)
7
+ [![Release](https://img.shields.io/github/v/release/ParkerHwang/OpenSocrates)](https://github.com/ParkerHwang/OpenSocrates/releases)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ OpenSocrates는 AI 에이전트 호스트를 위한 로컬 추론 프레임워크입니다. 의도적인
11
+ 추론이 필요한 요청을 감지하고, 별도의 Codex 컨텍스트에서 적합한 추론 시스템을
12
+ 선택한 뒤, 저작된 이론과 예제를 활성 작업에 추가합니다. 단순한 사실 확인과
13
+ 기계적인 작업은 그대로 통과할 수 있습니다.
14
+
15
+ 버전 `1.0.0`에는 48개의 추론 시스템이 들어 있으며 Apple Silicon macOS
16
+ (`darwin-arm64`)의 Codex Desktop과 Codex CLI를 지원합니다. 다른 플랫폼,
17
+ 바이너리 서명, 공증, 클린 머신 설치는 아직 검증됐다고 주장하지 않습니다.
18
+
19
+ ## 설치
20
+
21
+ 설치 전에 OAuth로 Codex에 로그인하고 `codex` 명령을 사용할 수 있는지
22
+ 확인하세요.
23
+
24
+ ### 방법 1: npm에서 npx로 설치
25
+
26
+ Node.js 20 이상이 필요합니다. npm 패키지는 외부 의존성이 없는 작은 설치
27
+ 프로그램이며, 일치하는 패키지를 GitHub Releases에서 내려받아 검증합니다.
28
+
29
+ ```bash
30
+ npx --yes opensocrates@1.0.0 install
31
+ ```
32
+
33
+ 라이프사이클 명령:
34
+
35
+ ```bash
36
+ npx --yes opensocrates@1.0.0 status
37
+ npx --yes opensocrates@1.0.0 update
38
+ npx --yes opensocrates@1.0.0 remove
39
+ ```
40
+
41
+ ### 방법 2: GitHub에서 npx로 설치
42
+
43
+ Node.js 20 이상이 필요합니다. npm 레지스트리에 별도 패키지를 게시하지 않아도
44
+ 태그가 고정된 GitHub 저장소에서 직접 설치합니다.
45
+
46
+ ```bash
47
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 install
48
+ ```
49
+
50
+ 라이프사이클 명령:
51
+
52
+ ```bash
53
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 status
54
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 update
55
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 remove
56
+ ```
57
+
58
+ 설치 프로그램은 일치하는 GitHub Release 자산을 내려받고 릴리스 체크섬과
59
+ 패키지 내부의 모든 체크섬을 검증한 다음, 현재 Codex 홈 아래의 비공개 관리형
60
+ 마켓플레이스에 등록합니다.
61
+
62
+ ### 방법 3: GitHub Releases에서 직접 다운로드
63
+
64
+ [v1.0.0 릴리스](https://github.com/ParkerHwang/OpenSocrates/releases/tag/v1.0.0)에서
65
+ `opensocrates.mjs`를 내려받은 뒤 실행합니다.
66
+
67
+ ```bash
68
+ node opensocrates.mjs install
69
+ ```
70
+
71
+ 파일과 체크섬을 직접 내려받아 확인하려면:
72
+
73
+ ```bash
74
+ curl -fLO https://github.com/ParkerHwang/OpenSocrates/releases/download/v1.0.0/opensocrates-1.0.0-codex-plugin.zip
75
+ curl -fLO https://github.com/ParkerHwang/OpenSocrates/releases/download/v1.0.0/opensocrates-1.0.0-codex-plugin.zip.sha256
76
+ shasum -a 256 -c opensocrates-1.0.0-codex-plugin.zip.sha256
77
+ node opensocrates.mjs install \
78
+ --asset opensocrates-1.0.0-codex-plugin.zip \
79
+ --checksum opensocrates-1.0.0-codex-plugin.zip.sha256
80
+ ```
81
+
82
+ ### 방법 4: 소스에서 빌드해 설치
83
+
84
+ [uv](https://docs.astral.sh/uv/)와 Python 3.12가 필요합니다.
85
+
86
+ ```bash
87
+ git clone https://github.com/ParkerHwang/OpenSocrates.git
88
+ cd OpenSocrates
89
+ uv python install 3.12
90
+ uv sync --locked --all-groups
91
+ make release-check
92
+ uv run --locked --no-sync python tools/codex_plugin.py install
93
+ ```
94
+
95
+ `make release-check`는 네이티브 런타임과 설치 가능한 패키지를 로컬에서
96
+ 빌드합니다. 생성 파일은 Git에서 제외된 `build/`와 `dist/`에 기록됩니다.
97
+
98
+ ## 사용법
99
+
100
+ Codex를 평소처럼 사용하세요. OpenSocrates는 제출된 각 프롬프트 전에 실행되며,
101
+ 요청에 판단, 해석, 진단, 설명, 계획, 근거 조정 또는 다른 구조적 추론 과정이
102
+ 필요할 때 개입할 수 있습니다. 개입 시 다음 순서로 동작합니다.
103
+
104
+ 1. 매번 새로운 임시 셀렉터 스레드를 사용합니다.
105
+ 2. 저작된 48개 시스템 카탈로그에서 적합한 시스템을 선택합니다.
106
+ 3. 선택된 전체 콘텐츠를 소유자 전용 임시 Markdown 파일에 기록합니다.
107
+ 4. 활성 작업에 해당 파일을 읽으라는 하나의 숨겨진 컨텍스트 메시지를
108
+ 추가합니다.
109
+
110
+ 선택 수에는 고정 제한이 없습니다. 훅 메시지는 작게 유지하면서 선택된 이론과
111
+ 예제 전체는 압축하거나 자르지 않고 참조 파일에 보존합니다.
112
+
113
+ 셀렉터의 내부 제한 시간은 30초이며 재시도하지 않고 실패 시 통과합니다.
114
+ 타임아웃, SDK 오류, 잘못된 출력, 미개입 결정, 안전하지 않은 컨텍스트 또는
115
+ 사용할 수 없는 훅은 모두 주입 없이 끝나며 사용자의 작업을 차단하지 않습니다.
116
+
117
+ 제한된 트랜스크립트 컨텍스트 요청도 막으려면:
118
+
119
+ ```bash
120
+ export OPENSOCRATES_SELECTOR_TRANSCRIPT_ACCESS=0
121
+ ```
122
+
123
+ ## 개인정보 보호와 보안
124
+
125
+ OpenSocrates는 Codex 앱 서버와 사용자의 기존 Codex OAuth 세션을 통해 로컬에서
126
+ 실행됩니다. API 키가 필요 없고, 백엔드를 호스팅하지 않으며, 사용자가 요청한
127
+ 작업을 대신 실행하지 않고, 텔레메트리를 추가하지 않습니다.
128
+
129
+ 임시 instruction 파일에는 OpenSocrates가 저작한 콘텐츠만 들어갑니다. 원시
130
+ 프롬프트, 대화 기록, 워크스페이스 파일, 도구 데이터, OAuth 자격 증명과 셀렉터
131
+ 추론은 기록, 로그, 메트릭, 진단 또는 임시 instruction 파일에 저장되지
132
+ 않습니다. `Stop`에서 현재 turn 파일을, `SessionEnd`에서 남은 세션 파일을,
133
+ `SessionStart`에서 24시간이 지난 비정상 종료 잔여 파일을 삭제합니다.
134
+
135
+ 취약점 신고 방법과 현재 지원 경계는 [SECURITY.md](SECURITY.md)를 확인하세요.
136
+
137
+ ## 개발
138
+
139
+ 소스 저장소에는 네이티브 바이너리와 중간 빌드 결과를 넣지 않습니다. 잠긴 개발
140
+ 환경을 설치하고 다음 검사를 실행합니다.
141
+
142
+ ```bash
143
+ make bootstrap
144
+ make format-check
145
+ make lint
146
+ make generated-check
147
+ make content-check
148
+ make docs-check
149
+ make security-scan
150
+ make smoke
151
+ npm test
152
+ npm pack --dry-run
153
+ make release-check
154
+ ```
155
+
156
+ 저장소 구조:
157
+
158
+ | 경로 | 용도 |
159
+ | --- | --- |
160
+ | `src/opensocrates/` | Python 3.12 런타임과 셀렉터 소스 |
161
+ | `content/` | 정책, 다국어 메시지, 추론 방법, 이론과 예제 |
162
+ | `plugin-src/codex/` | Codex 플러그인 템플릿 |
163
+ | `schemas/source/` | 정규 스키마 정의 |
164
+ | `schemas/v1/` | 생성된 버전 고정 공개 스키마 |
165
+ | `installer/` | 외부 의존성이 없는 Node.js GitHub/npx 설치 프로그램 |
166
+ | `tools/` | 빌드, 검증, 라이프사이클, 보안, 릴리스 도구 |
167
+ | `packaging/` | 네이티브 런처와 패키징 설정 |
168
+ | `build/`, `dist/` | 로컬에서 생성되어 릴리스 자산으로 게시되는 파일 |
169
+
170
+ 생성된 스키마와 컴파일된 콘텐츠는 정규 소스와 생성기를 통해 변경해야 합니다.
171
+ 릴리스 바이너리는 Git 이력이 아니라 GitHub Releases에 둡니다.
172
+
173
+ ## 기여
174
+
175
+ 기여를 환영합니다. Pull Request를 열기 전에 [CONTRIBUTING.md](CONTRIBUTING.md)를
176
+ 읽고, 버그와 기능 요청에는 이슈 템플릿을 사용하세요. 이 프로젝트는
177
+ [행동 강령](CODE_OF_CONDUCT.md)을 따릅니다.
178
+
179
+ ## 라이선스
180
+
181
+ OpenSocrates는 [MIT 라이선스](LICENSE)로 배포됩니다. 저작권 및 라이선스 고지를
182
+ 유지하면 사용, 수정, 배포, 재라이선스와 판매가 가능합니다.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # OpenSocrates
2
+
3
+ **English** | [한국어](README.ko.md)
4
+
5
+ [![CI](https://github.com/ParkerHwang/OpenSocrates/actions/workflows/ci.yml/badge.svg)](https://github.com/ParkerHwang/OpenSocrates/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/opensocrates)](https://www.npmjs.com/package/opensocrates)
7
+ [![Release](https://img.shields.io/github/v/release/ParkerHwang/OpenSocrates)](https://github.com/ParkerHwang/OpenSocrates/releases)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ OpenSocrates is a local reasoning framework for AI-agent hosts. It detects
11
+ requests that benefit from deliberate reasoning, selects relevant reasoning
12
+ systems in a separate Codex context, and adds their authored theory and
13
+ examples to the active task. Straightforward factual and mechanical work can
14
+ pass through unchanged.
15
+
16
+ Version `1.0.0` includes 48 reasoning systems and supports Codex Desktop and
17
+ Codex CLI on Apple-silicon macOS (`darwin-arm64`). Other platforms, binary
18
+ signing, notarization, and clean-machine installation are not yet claimed as
19
+ validated.
20
+
21
+ ## Install
22
+
23
+ Before installing, sign in to Codex with OAuth and make sure the `codex`
24
+ command is available.
25
+
26
+ ### Option 1: npx from npm
27
+
28
+ Requires Node.js 20 or later. The npm package is a small, dependency-free
29
+ installer; it downloads and verifies the matching package from GitHub
30
+ Releases.
31
+
32
+ ```bash
33
+ npx --yes opensocrates@1.0.0 install
34
+ ```
35
+
36
+ Lifecycle commands:
37
+
38
+ ```bash
39
+ npx --yes opensocrates@1.0.0 status
40
+ npx --yes opensocrates@1.0.0 update
41
+ npx --yes opensocrates@1.0.0 remove
42
+ ```
43
+
44
+ ### Option 2: npx from GitHub
45
+
46
+ Requires Node.js 20 or later. This command installs directly from the tagged
47
+ GitHub repository and remains available without the npm registry.
48
+
49
+ ```bash
50
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 install
51
+ ```
52
+
53
+ Lifecycle commands:
54
+
55
+ ```bash
56
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 status
57
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 update
58
+ npx --yes github:ParkerHwang/OpenSocrates#v1.0.0 remove
59
+ ```
60
+
61
+ The installer downloads the matching GitHub Release asset, verifies both the
62
+ release checksum and every checksum inside the package, and then registers a
63
+ private managed marketplace under the current Codex home.
64
+
65
+ ### Option 3: download from GitHub Releases
66
+
67
+ Download `opensocrates.mjs` from the
68
+ [v1.0.0 release](https://github.com/ParkerHwang/OpenSocrates/releases/tag/v1.0.0),
69
+ then run:
70
+
71
+ ```bash
72
+ node opensocrates.mjs install
73
+ ```
74
+
75
+ For a fully manual download and checksum check:
76
+
77
+ ```bash
78
+ curl -fLO https://github.com/ParkerHwang/OpenSocrates/releases/download/v1.0.0/opensocrates-1.0.0-codex-plugin.zip
79
+ curl -fLO https://github.com/ParkerHwang/OpenSocrates/releases/download/v1.0.0/opensocrates-1.0.0-codex-plugin.zip.sha256
80
+ shasum -a 256 -c opensocrates-1.0.0-codex-plugin.zip.sha256
81
+ node opensocrates.mjs install \
82
+ --asset opensocrates-1.0.0-codex-plugin.zip \
83
+ --checksum opensocrates-1.0.0-codex-plugin.zip.sha256
84
+ ```
85
+
86
+ ### Option 4: build and install from source
87
+
88
+ Requires [uv](https://docs.astral.sh/uv/) and Python 3.12.
89
+
90
+ ```bash
91
+ git clone https://github.com/ParkerHwang/OpenSocrates.git
92
+ cd OpenSocrates
93
+ uv python install 3.12
94
+ uv sync --locked --all-groups
95
+ make release-check
96
+ uv run --locked --no-sync python tools/codex_plugin.py install
97
+ ```
98
+
99
+ `make release-check` builds the native runtime and installable package locally.
100
+ Generated files are written to ignored `build/` and `dist/` directories.
101
+
102
+ ## Use
103
+
104
+ Use Codex normally. Before each submitted prompt, OpenSocrates may intervene
105
+ when the request needs judgment, interpretation, diagnosis, explanation,
106
+ planning, evidence reconciliation, or another structured reasoning process.
107
+ When it intervenes, it:
108
+
109
+ 1. uses a fresh, ephemeral selector thread;
110
+ 2. chooses from the authored 48-system catalog;
111
+ 3. writes the complete selected content to an owner-only temporary Markdown
112
+ file; and
113
+ 4. adds one hidden context message telling the active task to read that file.
114
+
115
+ There is no fixed selection-count limit. The hook message stays small while
116
+ the full selected theory and examples remain uncompressed in the referenced
117
+ file.
118
+
119
+ The selector has a 30-second internal deadline, does not retry, and fails open.
120
+ A timeout, SDK error, invalid output, non-intervention decision, unsafe
121
+ context, or unavailable hook produces no injection and does not block the
122
+ user's task.
123
+
124
+ To prevent the selector from requesting bounded transcript context:
125
+
126
+ ```bash
127
+ export OPENSOCRATES_SELECTOR_TRANSCRIPT_ACCESS=0
128
+ ```
129
+
130
+ ## Privacy and security
131
+
132
+ OpenSocrates runs locally through the Codex app-server and the user's existing
133
+ Codex OAuth session. It does not require an API key, host a backend, execute
134
+ the user's requested task, or add telemetry.
135
+
136
+ Temporary instruction files contain authored OpenSocrates content only. Raw
137
+ prompts, transcripts, workspace files, tool data, OAuth credentials, and
138
+ selector reasoning are not written to records, logs, metrics, diagnostics, or
139
+ temporary instruction files. Turn files are removed on `Stop`, remaining
140
+ session files on `SessionEnd`, and crash leftovers older than 24 hours on
141
+ `SessionStart`.
142
+
143
+ See [SECURITY.md](SECURITY.md) for vulnerability reporting and the current
144
+ support boundary.
145
+
146
+ ## Develop
147
+
148
+ The source repository intentionally excludes native binaries and intermediate
149
+ build output. Install the locked development environment and run:
150
+
151
+ ```bash
152
+ make bootstrap
153
+ make format-check
154
+ make lint
155
+ make generated-check
156
+ make content-check
157
+ make docs-check
158
+ make security-scan
159
+ make smoke
160
+ npm test
161
+ npm pack --dry-run
162
+ make release-check
163
+ ```
164
+
165
+ Repository layout:
166
+
167
+ | Path | Purpose |
168
+ | --- | --- |
169
+ | `src/opensocrates/` | Python 3.12 runtime and selector source |
170
+ | `content/` | Policies, localized messages, methods, theory, and examples |
171
+ | `plugin-src/codex/` | Codex plugin templates |
172
+ | `schemas/source/` | Canonical schema definitions |
173
+ | `schemas/v1/` | Generated, versioned public schemas |
174
+ | `installer/` | Dependency-free Node.js GitHub/npx installer |
175
+ | `tools/` | Build, validation, lifecycle, security, and release tools |
176
+ | `packaging/` | Native launcher and packaging configuration |
177
+ | `build/`, `dist/` | Generated locally and published as release assets |
178
+
179
+ Generated schemas and compiled content must be changed through their canonical
180
+ sources and generators. Release binaries belong in GitHub Releases, not in the
181
+ Git history.
182
+
183
+ ## Contributing
184
+
185
+ Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) before
186
+ opening a pull request, and use the issue templates for bugs and feature
187
+ requests. The project follows the [Code of Conduct](CODE_OF_CONDUCT.md).
188
+
189
+ ## License
190
+
191
+ OpenSocrates is available under the [MIT License](LICENSE). You may use,
192
+ modify, distribute, sublicense, and sell copies, provided the copyright and
193
+ license notice are preserved.
package/SECURITY.md ADDED
@@ -0,0 +1,47 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are provided for the latest `1.x` release. Pre-1.0 builds are
6
+ not supported.
7
+
8
+ | Version | Supported |
9
+ | --- | --- |
10
+ | 1.x | Yes |
11
+ | < 1.0 | No |
12
+
13
+ ## Report a vulnerability
14
+
15
+ Do not publish credentials, private prompts, transcripts, workspace data, or
16
+ exploit details in a public issue.
17
+
18
+ Use GitHub's
19
+ [private vulnerability reporting](https://github.com/ParkerHwang/OpenSocrates/security/advisories/new)
20
+ to send a confidential report. Include:
21
+
22
+ - the affected version and platform;
23
+ - the smallest reproducible example;
24
+ - expected and observed impact;
25
+ - whether credentials or private user data may be exposed;
26
+ - any suggested mitigation.
27
+
28
+ If private reporting is unavailable, open a public issue requesting a private
29
+ contact channel without including vulnerability details.
30
+
31
+ Reports are reviewed as maintainer capacity allows. Acknowledgement,
32
+ assessment, remediation, and disclosure timing depend on reproducibility,
33
+ severity, and the availability of a safe fix.
34
+
35
+ ## Security boundary
36
+
37
+ OpenSocrates 1.0:
38
+
39
+ - runs locally through Codex and uses the user's existing OAuth session;
40
+ - does not require or store an OpenAI API key;
41
+ - does not host an OpenSocrates backend or add telemetry;
42
+ - verifies downloadable packages with SHA-256 release and package manifests;
43
+ - fails open when the selector cannot safely complete.
44
+
45
+ Signing, notarization, clean-machine installation, platforms other than
46
+ `darwin-arm64`, and actual host hook delivery are not claimed as validated.
47
+ See the release limitations file for the complete measured boundary.
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,736 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { createReadStream, createWriteStream, realpathSync } from "node:fs";
6
+ import {
7
+ access,
8
+ cp,
9
+ lstat,
10
+ mkdir,
11
+ mkdtemp,
12
+ readFile,
13
+ readdir,
14
+ rename,
15
+ rm,
16
+ stat,
17
+ writeFile,
18
+ } from "node:fs/promises";
19
+ import { homedir, tmpdir } from "node:os";
20
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
21
+ import { Readable } from "node:stream";
22
+ import { pipeline } from "node:stream/promises";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ export const PRODUCT_VERSION = "1.0.0";
26
+ export const REPOSITORY = "ParkerHwang/OpenSocrates";
27
+ export const MARKETPLACE_NAME = "opensocrates";
28
+ export const PLUGIN_NAME = "opensocrates";
29
+ export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
30
+ export const ASSET_NAME = `opensocrates-${PRODUCT_VERSION}-codex-plugin.zip`;
31
+ export const CHECKSUM_NAME = `${ASSET_NAME}.sha256`;
32
+
33
+ const MARKER_NAME = ".opensocrates-managed.json";
34
+ const MARKER = Object.freeze({
35
+ schemaVersion: 1,
36
+ marketplaceName: MARKETPLACE_NAME,
37
+ pluginName: PLUGIN_NAME,
38
+ });
39
+ const MARKETPLACE_RELATIVE = join(".agents", "plugins", "marketplace.json");
40
+ const PLUGIN_RELATIVE = join("build", "generated", "plugins", "codex");
41
+ const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
42
+ const MAX_ARCHIVE_ENTRIES = 10_000;
43
+
44
+ export class InstallerError extends Error {}
45
+
46
+ function fail(message) {
47
+ throw new InstallerError(message);
48
+ }
49
+
50
+ async function exists(target) {
51
+ try {
52
+ await access(target);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function jsonEqual(left, right) {
60
+ const leftKeys = Object.keys(left).sort();
61
+ const rightKeys = Object.keys(right).sort();
62
+ return (
63
+ leftKeys.length === rightKeys.length &&
64
+ leftKeys.every((key, index) => key === rightKeys[index] && left[key] === right[key])
65
+ );
66
+ }
67
+
68
+ export function markerMatches(value) {
69
+ return (
70
+ value !== null &&
71
+ typeof value === "object" &&
72
+ !Array.isArray(value) &&
73
+ jsonEqual(value, MARKER)
74
+ );
75
+ }
76
+
77
+ export function isSafeArchivePath(value) {
78
+ if (
79
+ typeof value !== "string" ||
80
+ value.length === 0 ||
81
+ value.includes("\0") ||
82
+ value.includes("\\") ||
83
+ value.startsWith("/")
84
+ ) {
85
+ return false;
86
+ }
87
+ const candidate = value.endsWith("/") ? value.slice(0, -1) : value;
88
+ if (candidate.length === 0) {
89
+ return false;
90
+ }
91
+ return candidate.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
92
+ }
93
+
94
+ export function parseChecksumText(text, expectedName = ASSET_NAME) {
95
+ const candidates = String(text)
96
+ .split(/\r?\n/u)
97
+ .map((line) => line.trim())
98
+ .filter(Boolean);
99
+ for (const line of candidates) {
100
+ const match = line.match(/^([a-fA-F0-9]{64})(?:\s+[*]?(.+))?$/u);
101
+ if (!match) {
102
+ continue;
103
+ }
104
+ const namedFile = match[2]?.trim();
105
+ if (namedFile && basename(namedFile) !== expectedName) {
106
+ continue;
107
+ }
108
+ return match[1].toLowerCase();
109
+ }
110
+ fail(`checksum file does not contain a SHA-256 entry for ${expectedName}`);
111
+ }
112
+
113
+ export function parseCli(argv) {
114
+ const actions = new Set(["install", "status", "update", "remove", "verify", "help"]);
115
+ const args = [...argv];
116
+ let action = "install";
117
+ if (args[0] && !args[0].startsWith("-")) {
118
+ action = args.shift();
119
+ }
120
+ if (!actions.has(action)) {
121
+ fail(`unknown action ${JSON.stringify(action)}`);
122
+ }
123
+ const options = { action, asset: null, checksum: null };
124
+ while (args.length > 0) {
125
+ const flag = args.shift();
126
+ if (flag === "--help" || flag === "-h") {
127
+ options.action = "help";
128
+ continue;
129
+ }
130
+ if (flag !== "--asset" && flag !== "--checksum") {
131
+ fail(`unknown option ${JSON.stringify(flag)}`);
132
+ }
133
+ const value = args.shift();
134
+ if (!value || value.startsWith("-")) {
135
+ fail(`${flag} requires a file path`);
136
+ }
137
+ options[flag === "--asset" ? "asset" : "checksum"] = resolve(value);
138
+ }
139
+ if ((options.asset === null) !== (options.checksum === null)) {
140
+ fail("--asset and --checksum must be supplied together");
141
+ }
142
+ if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
143
+ fail(`${options.action} does not accept --asset or --checksum`);
144
+ }
145
+ return options;
146
+ }
147
+
148
+ function showHelp() {
149
+ console.log(`OpenSocrates ${PRODUCT_VERSION}
150
+
151
+ Usage:
152
+ opensocrates install [--asset ZIP --checksum SHA256]
153
+ opensocrates status
154
+ opensocrates update [--asset ZIP --checksum SHA256]
155
+ opensocrates remove
156
+ opensocrates verify [--asset ZIP --checksum SHA256]
157
+
158
+ Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
159
+ package and checksum from GitHub Releases.
160
+ `);
161
+ }
162
+
163
+ function managedPaths() {
164
+ const configured = process.env.CODEX_HOME;
165
+ const codexHome = resolve(configured ? configured : join(homedir(), ".codex"));
166
+ const root = join(codexHome, "managed-marketplaces", MARKETPLACE_NAME);
167
+ return {
168
+ codexHome,
169
+ root,
170
+ parent: dirname(root),
171
+ marker: join(root, MARKER_NAME),
172
+ marketplace: join(root, MARKETPLACE_RELATIVE),
173
+ plugin: join(root, PLUGIN_RELATIVE),
174
+ };
175
+ }
176
+
177
+ function codexBinary() {
178
+ return process.env.CODEX_BIN || "codex";
179
+ }
180
+
181
+ function run(command, args, { allowFailure = false } = {}) {
182
+ const result = spawnSync(command, args, {
183
+ encoding: "utf8",
184
+ env: process.env,
185
+ maxBuffer: 16 * 1024 * 1024,
186
+ });
187
+ if (result.error) {
188
+ if (allowFailure) {
189
+ return result;
190
+ }
191
+ fail(`could not run ${command}: ${result.error.message}`);
192
+ }
193
+ if (result.status !== 0 && !allowFailure) {
194
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status}`;
195
+ fail(`${command} ${args.join(" ")} failed: ${detail}`);
196
+ }
197
+ return result;
198
+ }
199
+
200
+ function runCodexJson(args) {
201
+ const result = run(codexBinary(), args);
202
+ try {
203
+ const payload = JSON.parse(result.stdout);
204
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
205
+ fail("Codex returned a non-object JSON value");
206
+ }
207
+ return payload;
208
+ } catch (error) {
209
+ if (error instanceof InstallerError) {
210
+ throw error;
211
+ }
212
+ fail(`Codex returned invalid JSON for ${args.join(" ")}: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ function requireCodex() {
217
+ run(codexBinary(), ["--version"]);
218
+ }
219
+
220
+ function marketplaceEntry() {
221
+ const payload = runCodexJson(["plugin", "marketplace", "list", "--json"]);
222
+ const entries = payload.marketplaces;
223
+ if (!Array.isArray(entries)) {
224
+ fail("Codex marketplace list returned an unexpected schema");
225
+ }
226
+ const matches = entries.filter((entry) => entry?.name === MARKETPLACE_NAME);
227
+ if (matches.length > 1) {
228
+ fail(`Codex reported duplicate marketplaces named ${MARKETPLACE_NAME}`);
229
+ }
230
+ return matches[0] ?? null;
231
+ }
232
+
233
+ function pluginState() {
234
+ const payload = runCodexJson([
235
+ "plugin",
236
+ "list",
237
+ "--marketplace",
238
+ MARKETPLACE_NAME,
239
+ "--available",
240
+ "--json",
241
+ ]);
242
+ const installed = Array.isArray(payload.installed) ? payload.installed : null;
243
+ const available = Array.isArray(payload.available) ? payload.available : null;
244
+ if (installed === null || available === null) {
245
+ fail("Codex plugin list returned an unexpected schema");
246
+ }
247
+ const installedMatches = installed.filter((entry) => entry?.pluginId === PLUGIN_ID);
248
+ const availableMatches = available.filter((entry) => entry?.pluginId === PLUGIN_ID);
249
+ if (installedMatches.length > 1 || availableMatches.length > 1) {
250
+ fail(`Codex reported duplicate entries for ${PLUGIN_ID}`);
251
+ }
252
+ if (installedMatches.length === 1) {
253
+ return { kind: "installed", version: installedMatches[0].version ?? null };
254
+ }
255
+ if (availableMatches.length === 1) {
256
+ return { kind: "available", version: availableMatches[0].version ?? null };
257
+ }
258
+ return { kind: "missing", version: null };
259
+ }
260
+
261
+ async function readJsonObject(target) {
262
+ let payload;
263
+ try {
264
+ payload = JSON.parse(await readFile(target, "utf8"));
265
+ } catch (error) {
266
+ fail(`cannot read valid JSON from ${target}: ${error.message}`);
267
+ }
268
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
269
+ fail(`expected a JSON object in ${target}`);
270
+ }
271
+ return payload;
272
+ }
273
+
274
+ async function requireOwnedRoot(root) {
275
+ const info = await lstat(root);
276
+ if (!info.isDirectory() || info.isSymbolicLink()) {
277
+ fail(`managed marketplace path is not an owned directory: ${root}`);
278
+ }
279
+ const marker = await readJsonObject(join(root, MARKER_NAME));
280
+ if (!markerMatches(marker)) {
281
+ fail(`managed marketplace path has no valid ownership marker: ${root}`);
282
+ }
283
+ }
284
+
285
+ async function sha256File(target) {
286
+ const hash = createHash("sha256");
287
+ for await (const chunk of createReadStream(target)) {
288
+ hash.update(chunk);
289
+ }
290
+ return hash.digest("hex");
291
+ }
292
+
293
+ async function downloadFile(url, destination) {
294
+ const response = await fetch(url, {
295
+ headers: {
296
+ accept: "application/octet-stream",
297
+ "user-agent": `OpenSocrates-installer/${PRODUCT_VERSION}`,
298
+ },
299
+ redirect: "follow",
300
+ signal: AbortSignal.timeout(15 * 60 * 1000),
301
+ });
302
+ if (!response.ok || response.body === null) {
303
+ fail(`download failed (${response.status}) for ${url}`);
304
+ }
305
+ const contentLength = Number(response.headers.get("content-length") ?? "0");
306
+ if (Number.isFinite(contentLength) && contentLength > MAX_DOWNLOAD_BYTES) {
307
+ fail(`download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
308
+ }
309
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(destination, { mode: 0o600 }));
310
+ const downloaded = (await stat(destination)).size;
311
+ if (downloaded === 0 || downloaded > MAX_DOWNLOAD_BYTES) {
312
+ fail(`downloaded file has an invalid size: ${downloaded} bytes`);
313
+ }
314
+ }
315
+
316
+ async function resolveAssetInputs(options, scratch) {
317
+ if (options.asset !== null) {
318
+ if (!(await exists(options.asset)) || !(await exists(options.checksum))) {
319
+ fail("the supplied asset or checksum file does not exist");
320
+ }
321
+ return { asset: options.asset, checksum: options.checksum };
322
+ }
323
+ const base = `https://github.com/${REPOSITORY}/releases/download/v${PRODUCT_VERSION}`;
324
+ const asset = join(scratch, ASSET_NAME);
325
+ const checksum = join(scratch, CHECKSUM_NAME);
326
+ console.log(`Downloading OpenSocrates ${PRODUCT_VERSION} from GitHub Releases...`);
327
+ await downloadFile(`${base}/${ASSET_NAME}`, asset);
328
+ await downloadFile(`${base}/${CHECKSUM_NAME}`, checksum);
329
+ return { asset, checksum };
330
+ }
331
+
332
+ async function verifyOuterChecksum(asset, checksum) {
333
+ const expected = parseChecksumText(await readFile(checksum, "utf8"));
334
+ const actual = await sha256File(asset);
335
+ if (actual !== expected) {
336
+ fail(`release checksum mismatch: expected ${expected}, got ${actual}`);
337
+ }
338
+ }
339
+
340
+ function archiveEntries(asset) {
341
+ const unzip = process.platform === "darwin" ? "/usr/bin/unzip" : "unzip";
342
+ const result = run(unzip, ["-Z1", asset]);
343
+ const entries = result.stdout.split(/\r?\n/u).filter(Boolean);
344
+ if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) {
345
+ fail(`archive contains an invalid number of entries: ${entries.length}`);
346
+ }
347
+ const unique = new Set();
348
+ for (const entry of entries) {
349
+ if (!isSafeArchivePath(entry)) {
350
+ fail(`archive contains an unsafe path: ${JSON.stringify(entry)}`);
351
+ }
352
+ if (unique.has(entry)) {
353
+ fail(`archive contains a duplicate path: ${entry}`);
354
+ }
355
+ unique.add(entry);
356
+ }
357
+ return entries;
358
+ }
359
+
360
+ async function extractArchive(asset, destination) {
361
+ archiveEntries(asset);
362
+ await mkdir(destination, { recursive: true, mode: 0o700 });
363
+ const unzip = process.platform === "darwin" ? "/usr/bin/unzip" : "unzip";
364
+ run(unzip, ["-q", asset, "-d", destination]);
365
+ }
366
+
367
+ async function walkFiles(root, current = root, output = []) {
368
+ const entries = await readdir(current, { withFileTypes: true });
369
+ for (const entry of entries) {
370
+ const absolute = join(current, entry.name);
371
+ const info = await lstat(absolute);
372
+ if (info.isSymbolicLink()) {
373
+ fail(`package contains a symbolic link: ${relative(root, absolute)}`);
374
+ }
375
+ if (info.isDirectory()) {
376
+ await walkFiles(root, absolute, output);
377
+ } else if (info.isFile()) {
378
+ output.push(relative(root, absolute).split(sep).join("/"));
379
+ } else {
380
+ fail(`package contains an unsupported filesystem entry: ${relative(root, absolute)}`);
381
+ }
382
+ }
383
+ return output;
384
+ }
385
+
386
+ async function verifyPackageChecksums(pluginRoot) {
387
+ const checksumPath = join(pluginRoot, "checksums.sha256");
388
+ const lines = (await readFile(checksumPath, "utf8"))
389
+ .split(/\r?\n/u)
390
+ .map((line) => line.trim())
391
+ .filter(Boolean);
392
+ const declared = new Set();
393
+ for (const line of lines) {
394
+ const match = line.match(/^([a-fA-F0-9]{64})\s+[*]?(.+)$/u);
395
+ if (!match) {
396
+ fail(`invalid package checksum line: ${line}`);
397
+ }
398
+ const expected = match[1].toLowerCase();
399
+ const item = match[2].trim();
400
+ if (!isSafeArchivePath(item) || item === "checksums.sha256" || declared.has(item)) {
401
+ fail(`invalid or duplicate package checksum path: ${item}`);
402
+ }
403
+ declared.add(item);
404
+ const target = join(pluginRoot, ...item.split("/"));
405
+ if (!(await exists(target))) {
406
+ fail(`package checksum references a missing file: ${item}`);
407
+ }
408
+ const actual = await sha256File(target);
409
+ if (actual !== expected) {
410
+ fail(`package checksum mismatch for ${item}`);
411
+ }
412
+ }
413
+ const actualFiles = new Set((await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"));
414
+ if (
415
+ actualFiles.size !== declared.size ||
416
+ [...actualFiles].some((item) => !declared.has(item))
417
+ ) {
418
+ fail("package checksum manifest does not cover the complete archive");
419
+ }
420
+ return declared.size;
421
+ }
422
+
423
+ async function verifyExtractedPackage(pluginRoot) {
424
+ const manifest = await readJsonObject(join(pluginRoot, ".codex-plugin", "plugin.json"));
425
+ if (manifest.name !== PLUGIN_NAME || manifest.version !== PRODUCT_VERSION) {
426
+ fail(
427
+ `plugin manifest mismatch: expected ${PLUGIN_NAME} ${PRODUCT_VERSION}, ` +
428
+ `got ${String(manifest.name)} ${String(manifest.version)}`,
429
+ );
430
+ }
431
+ const release = await readJsonObject(join(pluginRoot, "release-manifest.json"));
432
+ if (
433
+ release.product_version !== PRODUCT_VERSION ||
434
+ release.host !== "codex" ||
435
+ release.schema !== "opensocrates.plugin-release-manifest/1.0.0"
436
+ ) {
437
+ fail("package release manifest does not match this installer");
438
+ }
439
+ const runtime = join(
440
+ pluginRoot,
441
+ "runtime",
442
+ "darwin-arm64",
443
+ "opensocrates-runtime",
444
+ "opensocrates-runtime",
445
+ );
446
+ const runtimeInfo = await stat(runtime);
447
+ if (!runtimeInfo.isFile() || (runtimeInfo.mode & 0o111) === 0) {
448
+ fail("package is missing the executable darwin-arm64 runtime");
449
+ }
450
+ return verifyPackageChecksums(pluginRoot);
451
+ }
452
+
453
+ async function prepareVerifiedPackage(options) {
454
+ const scratch = await mkdtemp(join(tmpdir(), "opensocrates-install-"));
455
+ try {
456
+ const { asset, checksum } = await resolveAssetInputs(options, scratch);
457
+ await verifyOuterChecksum(asset, checksum);
458
+ const pluginRoot = join(scratch, "plugin");
459
+ await extractArchive(asset, pluginRoot);
460
+ const checkedFiles = await verifyExtractedPackage(pluginRoot);
461
+ return { scratch, pluginRoot, checkedFiles };
462
+ } catch (error) {
463
+ await rm(scratch, { recursive: true, force: true });
464
+ throw error;
465
+ }
466
+ }
467
+
468
+ function expectedMarketplace() {
469
+ return {
470
+ name: MARKETPLACE_NAME,
471
+ interface: { displayName: "OpenSocrates" },
472
+ plugins: [
473
+ {
474
+ name: PLUGIN_NAME,
475
+ source: {
476
+ source: "local",
477
+ path: "./build/generated/plugins/codex",
478
+ },
479
+ policy: {
480
+ installation: "AVAILABLE",
481
+ authentication: "ON_INSTALL",
482
+ },
483
+ category: "Productivity",
484
+ },
485
+ ],
486
+ };
487
+ }
488
+
489
+ async function buildStagingTree(parent, pluginSource) {
490
+ const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
491
+ try {
492
+ const marketplace = join(staging, MARKETPLACE_RELATIVE);
493
+ const plugin = join(staging, PLUGIN_RELATIVE);
494
+ await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
495
+ await mkdir(dirname(plugin), { recursive: true, mode: 0o700 });
496
+ await cp(pluginSource, plugin, { recursive: true, preserveTimestamps: true });
497
+ await writeFile(marketplace, `${JSON.stringify(expectedMarketplace(), null, 2)}\n`, {
498
+ encoding: "utf8",
499
+ mode: 0o600,
500
+ });
501
+ await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(MARKER, null, 2)}\n`, {
502
+ encoding: "utf8",
503
+ mode: 0o600,
504
+ });
505
+ await verifyExtractedPackage(plugin);
506
+ return staging;
507
+ } catch (error) {
508
+ await rm(staging, { recursive: true, force: true });
509
+ throw error;
510
+ }
511
+ }
512
+
513
+ function entryRoot(entry) {
514
+ if (entry === null) {
515
+ return null;
516
+ }
517
+ if (typeof entry.root !== "string" || entry.root.trim().length === 0) {
518
+ fail(`Codex marketplace ${MARKETPLACE_NAME} has no usable root`);
519
+ }
520
+ return resolve(entry.root);
521
+ }
522
+
523
+ function removeRegistration(entry, state) {
524
+ if (state.kind === "installed") {
525
+ runCodexJson(["plugin", "remove", PLUGIN_ID, "--json"]);
526
+ }
527
+ if (entry !== null) {
528
+ runCodexJson(["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"]);
529
+ }
530
+ }
531
+
532
+ function addRegistration(root, installPlugin) {
533
+ runCodexJson(["plugin", "marketplace", "add", root, "--json"]);
534
+ if (installPlugin) {
535
+ return runCodexJson(["plugin", "add", PLUGIN_ID, "--json"]);
536
+ }
537
+ return null;
538
+ }
539
+
540
+ function removeRegistrationBestEffort() {
541
+ const entry = (() => {
542
+ try {
543
+ return marketplaceEntry();
544
+ } catch {
545
+ return null;
546
+ }
547
+ })();
548
+ if (entry === null) {
549
+ return;
550
+ }
551
+ try {
552
+ const state = pluginState();
553
+ if (state.kind === "installed") {
554
+ run(codexBinary(), ["plugin", "remove", PLUGIN_ID, "--json"], { allowFailure: true });
555
+ }
556
+ } catch {
557
+ // Rollback continues with marketplace cleanup.
558
+ }
559
+ run(codexBinary(), ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"], {
560
+ allowFailure: true,
561
+ });
562
+ }
563
+
564
+ async function installVerifiedPackage(pluginSource, action) {
565
+ requireCodex();
566
+ const paths = managedPaths();
567
+ await mkdir(paths.parent, { recursive: true, mode: 0o700 });
568
+
569
+ const previousEntry = marketplaceEntry();
570
+ if (previousEntry !== null && entryRoot(previousEntry) !== paths.root) {
571
+ fail(
572
+ `marketplace ${MARKETPLACE_NAME} is already registered at ${entryRoot(previousEntry)}; ` +
573
+ "refusing to overwrite an unmanaged location",
574
+ );
575
+ }
576
+ const previousState = previousEntry === null ? { kind: "missing", version: null } : pluginState();
577
+ if (await exists(paths.root)) {
578
+ await requireOwnedRoot(paths.root);
579
+ }
580
+
581
+ const staging = await buildStagingTree(paths.parent, pluginSource);
582
+ const backup = join(paths.parent, `.opensocrates.backup-${randomUUID()}`);
583
+ let registrationRemoved = false;
584
+ let backupCreated = false;
585
+ let newRootActive = false;
586
+ try {
587
+ if (previousEntry !== null) {
588
+ registrationRemoved = true;
589
+ removeRegistration(previousEntry, previousState);
590
+ }
591
+ if (await exists(paths.root)) {
592
+ await rename(paths.root, backup);
593
+ backupCreated = true;
594
+ }
595
+ await rename(staging, paths.root);
596
+ newRootActive = true;
597
+ const result = addRegistration(paths.root, true);
598
+ const state = pluginState();
599
+ if (
600
+ result?.pluginId !== PLUGIN_ID ||
601
+ result?.version !== PRODUCT_VERSION ||
602
+ state.kind !== "installed" ||
603
+ state.version !== PRODUCT_VERSION
604
+ ) {
605
+ fail("Codex did not confirm the expected installed plugin and version");
606
+ }
607
+ if (backupCreated) {
608
+ await requireOwnedRoot(backup);
609
+ await rm(backup, { recursive: true });
610
+ }
611
+ const verb = action === "update" ? "updated" : "installed";
612
+ console.log(`OpenSocrates ${PRODUCT_VERSION} ${verb} successfully.`);
613
+ console.log(`Managed marketplace: ${paths.root}`);
614
+ console.log("Start a new Codex task to load the updated skills and hooks.");
615
+ } catch (error) {
616
+ removeRegistrationBestEffort();
617
+ if (newRootActive && (await exists(paths.root))) {
618
+ await requireOwnedRoot(paths.root);
619
+ await rm(paths.root, { recursive: true });
620
+ }
621
+ if (backupCreated && (await exists(backup))) {
622
+ await requireOwnedRoot(backup);
623
+ await rename(backup, paths.root);
624
+ }
625
+ if (registrationRemoved && previousEntry !== null) {
626
+ try {
627
+ addRegistration(paths.root, previousState.kind === "installed");
628
+ } catch (rollbackError) {
629
+ fail(`${error.message}; rollback also failed: ${rollbackError.message}`);
630
+ }
631
+ }
632
+ throw error;
633
+ } finally {
634
+ if (await exists(staging)) {
635
+ await rm(staging, { recursive: true, force: true });
636
+ }
637
+ }
638
+ }
639
+
640
+ async function showStatus() {
641
+ requireCodex();
642
+ const paths = managedPaths();
643
+ const entry = marketplaceEntry();
644
+ if (entry === null) {
645
+ if (await exists(paths.root)) {
646
+ await requireOwnedRoot(paths.root);
647
+ console.log(`OpenSocrates files are present but not registered: ${paths.root}`);
648
+ } else {
649
+ console.log("OpenSocrates is not installed.");
650
+ }
651
+ return;
652
+ }
653
+ if (entryRoot(entry) !== paths.root) {
654
+ fail(`OpenSocrates is registered at an unmanaged location: ${entryRoot(entry)}`);
655
+ }
656
+ await requireOwnedRoot(paths.root);
657
+ const state = pluginState();
658
+ if (state.kind === "installed") {
659
+ console.log(`OpenSocrates ${state.version ?? "unknown"} is installed.`);
660
+ } else if (state.kind === "available") {
661
+ console.log(`OpenSocrates ${state.version ?? "unknown"} is available but not installed.`);
662
+ } else {
663
+ console.log("The OpenSocrates marketplace is registered, but the plugin is missing.");
664
+ }
665
+ }
666
+
667
+ async function removeInstalled() {
668
+ requireCodex();
669
+ const paths = managedPaths();
670
+ const entry = marketplaceEntry();
671
+ if (entry !== null && entryRoot(entry) !== paths.root) {
672
+ fail(`OpenSocrates is registered at an unmanaged location: ${entryRoot(entry)}`);
673
+ }
674
+ const state = entry === null ? { kind: "missing", version: null } : pluginState();
675
+ removeRegistration(entry, state);
676
+ if (await exists(paths.root)) {
677
+ await requireOwnedRoot(paths.root);
678
+ await rm(paths.root, { recursive: true });
679
+ }
680
+ console.log("OpenSocrates was removed from Codex.");
681
+ }
682
+
683
+ export async function main(argv = process.argv.slice(2)) {
684
+ const options = parseCli(argv);
685
+ if (options.action === "help") {
686
+ showHelp();
687
+ return 0;
688
+ }
689
+ if (options.action === "status") {
690
+ await showStatus();
691
+ return 0;
692
+ }
693
+ if (options.action === "remove") {
694
+ await removeInstalled();
695
+ return 0;
696
+ }
697
+ if (
698
+ ["install", "update"].includes(options.action) &&
699
+ (process.platform !== "darwin" || process.arch !== "arm64")
700
+ ) {
701
+ fail(
702
+ `OpenSocrates ${PRODUCT_VERSION} prebuilt installation supports darwin-arm64 only; ` +
703
+ `detected ${process.platform}-${process.arch}`,
704
+ );
705
+ }
706
+
707
+ const prepared = await prepareVerifiedPackage(options);
708
+ try {
709
+ console.log(
710
+ `Verified OpenSocrates ${PRODUCT_VERSION} release and ${prepared.checkedFiles} package files.`,
711
+ );
712
+ if (options.action === "verify") {
713
+ return 0;
714
+ }
715
+ await installVerifiedPackage(prepared.pluginRoot, options.action);
716
+ return 0;
717
+ } finally {
718
+ await rm(prepared.scratch, { recursive: true, force: true });
719
+ }
720
+ }
721
+
722
+ let invokedDirectly = false;
723
+ if (process.argv[1] !== undefined) {
724
+ try {
725
+ invokedDirectly = realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
726
+ } catch {
727
+ invokedDirectly = false;
728
+ }
729
+ }
730
+ if (invokedDirectly) {
731
+ main().catch((error) => {
732
+ const message = error instanceof Error ? error.message : String(error);
733
+ console.error(`error: ${message}`);
734
+ process.exitCode = 1;
735
+ });
736
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "opensocrates",
3
+ "version": "1.0.0",
4
+ "description": "Install and manage the OpenSocrates reasoning framework for Codex.",
5
+ "type": "module",
6
+ "bin": {
7
+ "opensocrates": "installer/opensocrates.mjs"
8
+ },
9
+ "files": [
10
+ "installer/opensocrates.mjs",
11
+ "CHANGELOG.md",
12
+ "SECURITY.md",
13
+ "VERSION"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test installer/opensocrates.test.mjs && npm run test:npx",
17
+ "test:npx": "node installer/package-smoke.mjs",
18
+ "pack:check": "npm pack --dry-run",
19
+ "prepublishOnly": "npm test"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/ParkerHwang/OpenSocrates.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/ParkerHwang/OpenSocrates/issues"
30
+ },
31
+ "homepage": "https://github.com/ParkerHwang/OpenSocrates#readme",
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "keywords": [
38
+ "ai",
39
+ "codex",
40
+ "reasoning",
41
+ "agent",
42
+ "installer",
43
+ "critical-thinking",
44
+ "socratic"
45
+ ]
46
+ }