@codex-modules/teams 0.1.1
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/LICENSE +21 -0
- package/README.ko.md +158 -0
- package/README.md +158 -0
- package/dist/agents.d.ts +38 -0
- package/dist/agents.js +285 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +128 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/lock.d.ts +14 -0
- package/dist/lock.js +58 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +378 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +53 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +42 -0
- package/dist/runner.d.ts +32 -0
- package/dist/runner.js +156 -0
- package/dist/skill.d.ts +14 -0
- package/dist/skill.js +84 -0
- package/dist/state.d.ts +45 -0
- package/dist/state.js +261 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.js +175 -0
- package/dist/toml.d.ts +3 -0
- package/dist/toml.js +38 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ArcSolver
|
|
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,158 @@
|
|
|
1
|
+
<p align="right"><a href="README.md">English</a> | 한국어</p>
|
|
2
|
+
|
|
3
|
+
# @codex-modules/teams
|
|
4
|
+
|
|
5
|
+
`codex-teams`는 `team.json`을 Codex agent TOML, 리더 지시문, 프로젝트 로컬 팀 상태로 바꾸는 CLI입니다.
|
|
6
|
+
|
|
7
|
+
멀티 에이전트 실행 엔진은 Codex native stable `multi_agent_v1.spawn_agent`를 리더 세션 안에서 사용합니다. 이 패키지는 엔진을 다시 만들지 않고, 팀 정의, 안전한 install/uninstall, task lease, journal, dry-run 기본 headless runner를 관리합니다.
|
|
8
|
+
|
|
9
|
+
## 설치
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @codex-modules/teams
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
또는 source에서:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
이 패키지의 CLI를 바로 실행하려면:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node dist/cli.js doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
global 설치나 npm linking 후에는:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
codex-teams doctor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 사용법
|
|
35
|
+
|
|
36
|
+
starter team을 만듭니다:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
codex-teams init --preset review-panel --out team.json
|
|
40
|
+
codex-teams validate team.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
팀을 Codex home에 설치합니다:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
codex-teams install team.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
기본값은 `$CODEX_HOME/agents/<team>-<member>.toml`을 쓰고 `$CODEX_HOME/agents/.codex-teams-manifest.json`에 소유권을 기록합니다. sandbox나 다른 Codex home을 쓰려면 `--codex-home <dir>`를 넘기세요.
|
|
50
|
+
|
|
51
|
+
Project scope 설치는 명시적으로 선택합니다:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
codex-teams install team.json --scope project
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Project scope는 `<cwd>/.codex/agents/`에 쓰고 trust 경고를 출력합니다. `config.toml`을 편집하거나 프로젝트를 trusted로 등록하지 않습니다.
|
|
58
|
+
|
|
59
|
+
리더 프롬프트를 생성합니다:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
codex-teams leader-prompt team.json --goal "Review this change for security and correctness"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
프로젝트 내구 상태를 시작합니다:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
codex-teams state init review-panel --goal "Review this change"
|
|
69
|
+
codex-teams task add review-panel --title "Security review"
|
|
70
|
+
codex-teams task claim review-panel task-001 --actor security
|
|
71
|
+
codex-teams note add review-panel --actor leader --text "Security and correctness can run in parallel"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`run`은 기본적으로 dry-run입니다:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codex-teams run team.json --goal "Review this change"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
실제 `codex exec` 실행은 두 opt-in이 모두 필요합니다:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
codex-teams run team.json --goal "Review this change" --execute --allow-codex
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
실행 runner는 기본적으로 `codex exec -s workspace-write --skip-git-repo-check --json --ephemeral`을 사용하고 run artifact를 `.codex-teams/<team>/runs/` 아래에 남깁니다. runner는 `read-only` 또는 `workspace-write` sandbox만 허용하고, danger-access flag를 전달하지 않으며, 조립된 prompt를 `codex exec`의 단일 positional 인자로만 넘깁니다.
|
|
87
|
+
|
|
88
|
+
선택적 Codex skill을 설치합니다:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
codex-teams skill install
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 동작 방식
|
|
95
|
+
|
|
96
|
+
`team.json`은 team name, defaults, 2명에서 8명 사이의 members를 정의합니다. Member name은 `<team>-<member>` 형식의 Codex agent type이 됩니다.
|
|
97
|
+
|
|
98
|
+
Install은 각 member를 다음 필드가 있는 TOML로 렌더합니다:
|
|
99
|
+
|
|
100
|
+
- `name`
|
|
101
|
+
- `description`
|
|
102
|
+
- `model`
|
|
103
|
+
- `sandbox_mode`
|
|
104
|
+
- `nickname_candidates`
|
|
105
|
+
- `developer_instructions`
|
|
106
|
+
|
|
107
|
+
모든 TOML string은 basic string으로 렌더되어 따옴표, 백슬래시, 개행, TOML 제어문자가 escape됩니다. `nickname_candidates`는 string array로 렌더됩니다.
|
|
108
|
+
|
|
109
|
+
기존 unmanaged file은 `--force` 없이는 절대 덮어쓰지 않습니다. `--force`를 사용한 overwrite는 먼저 backup을 만듭니다. Uninstall은 선택한 target root의 manifest에 기록된 파일만 건드립니다.
|
|
110
|
+
|
|
111
|
+
프로젝트 상태는 다음 위치에 있습니다:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
.codex-teams/<team>/
|
|
115
|
+
state.json
|
|
116
|
+
tasks.json
|
|
117
|
+
journal.jsonl
|
|
118
|
+
artifacts/<member>/
|
|
119
|
+
runs/
|
|
120
|
+
locks/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`state.json`과 `tasks.json`은 mkdir lock 안에서 atomic write로 저장됩니다. `journal.jsonl`은 같은 lock 안에서 append-only로 추가됩니다. Task claim은 lease를 사용하며, 만료된 claim은 `task claim`과 `task list --reclaim`에서 회수됩니다. 결정론적 테스트에는 `CODEX_TEAMS_NOW`로 시간을 override할 수 있습니다.
|
|
124
|
+
|
|
125
|
+
상태 CLI는 리더 또는 사람이 호출하는 표면입니다. 멤버는 마지막 `TEAM-RESULT: <one-line summary>` 줄로 보고합니다. Workspace-write 멤버는 선택적으로 artifact를 남길 수 있지만, canonical 결과 채널은 final message입니다.
|
|
126
|
+
|
|
127
|
+
`doctor`는 Codex binary, version, native feature state, model catalog availability, write access, installed teams를 user/project scope로 나눠 보고합니다. 건강한 native workflow에는 `multi_agent`가 stable and enabled여야 합니다. `enable_fanout`과 `multi_agent_v2`는 under-development surface로만 보고합니다.
|
|
128
|
+
|
|
129
|
+
이 package는 runtime dependency가 없습니다. package root는 team parsing, install/uninstall, doctor, prompt/run planning, durable state/task/note operation을 위한 지원 대상 high-level helper만 export합니다.
|
|
130
|
+
|
|
131
|
+
## Attribution
|
|
132
|
+
|
|
133
|
+
No third-party code is included; the state protocol is an original clean-room design.
|
|
134
|
+
|
|
135
|
+
## 제거와 롤백
|
|
136
|
+
|
|
137
|
+
설치된 team을 제거합니다:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
codex-teams uninstall review-panel
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
sandbox 또는 project scope를 썼다면 install 때와 같은 target root를 지정합니다:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
codex-teams uninstall review-panel --codex-home /tmp/codex-home
|
|
147
|
+
codex-teams uninstall review-panel --scope project
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Uninstall은 manifest-owned file만 삭제합니다. `--force` install이 unmanaged file을 backup했다면 uninstall 때 그 backup을 복원합니다. 설치된 파일이 install 이후 바뀌었다면, 수동 확인을 위해 uninstall이 삭제를 거부합니다.
|
|
151
|
+
|
|
152
|
+
선택적 skill 제거:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
codex-teams skill uninstall
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`.codex-teams/` 아래 상태는 project-local runtime data입니다. 프로젝트가 git worktree로 보이면 기본적으로 ignore됩니다. task board, journal, runs, artifacts가 더 필요 없으면 `.codex-teams/<team>/`을 삭제하세요.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<p align="right">English | <a href="README.ko.md">한국어</a></p>
|
|
2
|
+
|
|
3
|
+
# @codex-modules/teams
|
|
4
|
+
|
|
5
|
+
`codex-teams` turns a `team.json` file into Codex agent TOML files, leader instructions, and durable project-local team state.
|
|
6
|
+
|
|
7
|
+
It uses Codex native stable `multi_agent_v1.spawn_agent` through a leader session. This package does not reimplement the multi-agent engine. It manages team definitions, safe install/uninstall, task leases, a journal, and a dry-run-first headless runner.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @codex-modules/teams
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or from source:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use the CLI from this package:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node dist/cli.js doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
When installed globally or through npm linking, the command is:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
codex-teams doctor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Create a starter team:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
codex-teams init --preset review-panel --out team.json
|
|
40
|
+
codex-teams validate team.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install the team into a Codex home:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
codex-teams install team.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
By default this writes `$CODEX_HOME/agents/<team>-<member>.toml` and records ownership in `$CODEX_HOME/agents/.codex-teams-manifest.json`. Use `--codex-home <dir>` for a sandbox or alternate Codex home.
|
|
50
|
+
|
|
51
|
+
Project-scope install is explicit:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
codex-teams install team.json --scope project
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Project scope writes `<cwd>/.codex/agents/` and prints a trust warning. It does not edit `config.toml` or mark the project trusted.
|
|
58
|
+
|
|
59
|
+
Generate the leader prompt:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
codex-teams leader-prompt team.json --goal "Review this change for security and correctness"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Start durable state in the project:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
codex-teams state init review-panel --goal "Review this change"
|
|
69
|
+
codex-teams task add review-panel --title "Security review"
|
|
70
|
+
codex-teams task claim review-panel task-001 --actor security
|
|
71
|
+
codex-teams note add review-panel --actor leader --text "Security and correctness can run in parallel"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run is dry-run by default:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codex-teams run team.json --goal "Review this change"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Actual `codex exec` launch requires both opt-ins:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
codex-teams run team.json --goal "Review this change" --execute --allow-codex
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The executed runner uses `codex exec -s workspace-write --skip-git-repo-check --json --ephemeral` by default and writes run artifacts under `.codex-teams/<team>/runs/`. The runner only accepts `read-only` or `workspace-write` sandbox modes, never forwards danger-access flags, and passes the assembled prompt as one positional `codex exec` argument.
|
|
87
|
+
|
|
88
|
+
Install the optional Codex skill:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
codex-teams skill install
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## How It Works
|
|
95
|
+
|
|
96
|
+
`team.json` defines a team name, defaults, and 2 to 8 members. Member names become Codex agent types named `<team>-<member>`.
|
|
97
|
+
|
|
98
|
+
Install renders each member to TOML with:
|
|
99
|
+
|
|
100
|
+
- `name`
|
|
101
|
+
- `description`
|
|
102
|
+
- `model`
|
|
103
|
+
- `sandbox_mode`
|
|
104
|
+
- `nickname_candidates`
|
|
105
|
+
- `developer_instructions`
|
|
106
|
+
|
|
107
|
+
All TOML strings are basic strings with quotes, backslashes, newlines, and TOML control characters escaped. `nickname_candidates` is rendered as a string array.
|
|
108
|
+
|
|
109
|
+
Existing unmanaged files are never overwritten unless `--force` is passed. Forced overwrites are backed up first. Uninstall only touches files recorded in the manifest for the selected target root.
|
|
110
|
+
|
|
111
|
+
Project state lives in:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
.codex-teams/<team>/
|
|
115
|
+
state.json
|
|
116
|
+
tasks.json
|
|
117
|
+
journal.jsonl
|
|
118
|
+
artifacts/<member>/
|
|
119
|
+
runs/
|
|
120
|
+
locks/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`state.json` and `tasks.json` are written atomically under a mkdir lock. `journal.jsonl` is append-only under the same lock. Task claims use leases; expired claims are reclaimed by `task claim` and `task list --reclaim`. `CODEX_TEAMS_NOW` can override time for deterministic tests.
|
|
124
|
+
|
|
125
|
+
Leader state commands are for the leader or a human operator. Members report through their final `TEAM-RESULT: <one-line summary>` line. Workspace-write members may also leave optional artifacts, but the final message is the canonical result channel.
|
|
126
|
+
|
|
127
|
+
`doctor` reports the Codex binary, version, native feature state, model catalog availability, write access, and installed teams split into user and project scopes. `multi_agent` must be stable and enabled for a healthy native workflow. `enable_fanout` and `multi_agent_v2` are reported as under-development surfaces only.
|
|
128
|
+
|
|
129
|
+
The package has zero runtime dependencies. Its package root exports only the supported high-level helpers for team parsing, install/uninstall, doctor, prompt/run planning, and durable state/task/note operations.
|
|
130
|
+
|
|
131
|
+
## Attribution
|
|
132
|
+
|
|
133
|
+
No third-party code is included; the state protocol is an original clean-room design.
|
|
134
|
+
|
|
135
|
+
## Uninstall Rollback
|
|
136
|
+
|
|
137
|
+
Remove an installed team:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
codex-teams uninstall review-panel
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For a sandbox or project scope, use the same target root used during install:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
codex-teams uninstall review-panel --codex-home /tmp/codex-home
|
|
147
|
+
codex-teams uninstall review-panel --scope project
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Uninstall deletes only manifest-owned files. If install backed up an unmanaged file with `--force`, uninstall restores that backup. If an installed file changed after install, uninstall refuses to remove it so you can inspect the file manually.
|
|
151
|
+
|
|
152
|
+
Remove the optional skill:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
codex-teams skill uninstall
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
State under `.codex-teams/` is project-local runtime data. It is ignored by default when the project appears to be a git worktree. Delete `.codex-teams/<team>/` when you no longer need the task board, journal, runs, or artifacts.
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { InstallScope, TeamDef, TeamsManifest } from "./types.js";
|
|
2
|
+
export type InstallOptions = {
|
|
3
|
+
codexHome?: string;
|
|
4
|
+
scope?: InstallScope;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
skipModelCheck?: boolean;
|
|
8
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
9
|
+
};
|
|
10
|
+
export type InstallResult = {
|
|
11
|
+
team: string;
|
|
12
|
+
scope: InstallScope;
|
|
13
|
+
targetRoot: string;
|
|
14
|
+
files: string[];
|
|
15
|
+
removed: string[];
|
|
16
|
+
warnings: string[];
|
|
17
|
+
};
|
|
18
|
+
export type UninstallResult = {
|
|
19
|
+
team: string;
|
|
20
|
+
scope: InstallScope;
|
|
21
|
+
targetRoot: string;
|
|
22
|
+
removed: string[];
|
|
23
|
+
restored: string[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
};
|
|
26
|
+
export declare function installTeam(teamPath: string, opts?: InstallOptions): InstallResult;
|
|
27
|
+
export declare function installTeamDef(team: TeamDef, opts?: InstallOptions): InstallResult;
|
|
28
|
+
export declare function uninstallTeam(team: string, opts?: InstallOptions): UninstallResult;
|
|
29
|
+
export declare function renderAgentToml(team: TeamDef, memberName: string): string;
|
|
30
|
+
export declare function resolveCodexHome(env?: NodeJS.ProcessEnv | Record<string, string | undefined>, explicit?: string): string;
|
|
31
|
+
export declare function resolveAgentsRoot(scope: InstallScope, opts?: InstallOptions): string;
|
|
32
|
+
export declare function manifestPath(targetRoot: string): string;
|
|
33
|
+
export declare function readManifest(targetRoot: string, warnings?: string[]): TeamsManifest;
|
|
34
|
+
export declare function writeManifest(targetRoot: string, manifest: TeamsManifest): void;
|
|
35
|
+
export declare function findCodexBinary(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string | null;
|
|
36
|
+
export declare function listInstalledTeams(targetRoot: string): string[];
|
|
37
|
+
export declare function parseModelCatalog(stdout: string): string[];
|
|
38
|
+
export declare function backupSkillFile(file: string, backupDir: string): string;
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, chmodSync, constants as fsConstants, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { assembleDeveloperInstructions } from "./prompt.js";
|
|
7
|
+
import { memberAgentName, parseTeamJson, resolveMemberModel, resolveMemberSandbox } from "./team.js";
|
|
8
|
+
import { renderToml } from "./toml.js";
|
|
9
|
+
import { nowIso, readJson, writeFileAtomic, writeJsonAtomic } from "./state.js";
|
|
10
|
+
import { assertConfinedRoot, isConfinedPath } from "./paths.js";
|
|
11
|
+
export function installTeam(teamPath, opts = {}) {
|
|
12
|
+
const team = parseTeamJson(teamPath);
|
|
13
|
+
return installTeamDef(team, opts);
|
|
14
|
+
}
|
|
15
|
+
export function installTeamDef(team, opts = {}) {
|
|
16
|
+
const scope = opts.scope ?? "user";
|
|
17
|
+
const targetRoot = resolveAgentsRoot(scope, opts);
|
|
18
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
19
|
+
if (scope === "project")
|
|
20
|
+
assertProjectRoot(targetRoot, opts);
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const removed = [];
|
|
23
|
+
if (scope === "project") {
|
|
24
|
+
warnings.push("project scope requires a trusted Codex project; config.toml is not modified automatically");
|
|
25
|
+
}
|
|
26
|
+
if (!opts.skipModelCheck)
|
|
27
|
+
warnings.push(...checkModels(team, opts));
|
|
28
|
+
const manifest = readManifest(targetRoot, warnings);
|
|
29
|
+
const installedAt = nowIso(opts.env);
|
|
30
|
+
const newMembers = new Set(team.members.map(member => member.name));
|
|
31
|
+
const oldTeamEntries = manifest.entries.filter(entry => entry.team === team.name && entry.kind === "agent");
|
|
32
|
+
const nextEntries = manifest.entries.filter(entry => !(entry.team === team.name && entry.kind === "agent"));
|
|
33
|
+
const files = [];
|
|
34
|
+
for (const entry of oldTeamEntries) {
|
|
35
|
+
if (entry.member && newMembers.has(entry.member))
|
|
36
|
+
continue;
|
|
37
|
+
const unsafeReason = validateManagedEntry(entry, targetRoot, scope);
|
|
38
|
+
if (unsafeReason) {
|
|
39
|
+
warnings.push(`skipping stale cleanup for ${entry.file}: ${unsafeReason}`);
|
|
40
|
+
nextEntries.push(entry);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!existsSync(entry.file))
|
|
44
|
+
continue;
|
|
45
|
+
const currentHash = sha256(readFileSync(entry.file, "utf8"));
|
|
46
|
+
if (currentHash !== entry.hash) {
|
|
47
|
+
warnings.push(`stale managed agent changed since install; leaving in place: ${entry.file}`);
|
|
48
|
+
nextEntries.push(entry);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
rmSync(entry.file, { force: true });
|
|
52
|
+
removed.push(entry.file);
|
|
53
|
+
warnings.push(`removed stale managed agent file: ${entry.file}`);
|
|
54
|
+
}
|
|
55
|
+
for (const member of team.members) {
|
|
56
|
+
const file = join(targetRoot, `${memberAgentName(team, member)}.toml`);
|
|
57
|
+
const content = renderAgentToml(team, member.name);
|
|
58
|
+
const existing = manifest.entries.find(entry => entry.file === file && entry.kind === "agent" && entry.scope === scope);
|
|
59
|
+
let backup = existing?.backup ?? null;
|
|
60
|
+
if (existsSync(file) && !existing) {
|
|
61
|
+
if (!opts.force)
|
|
62
|
+
throw new Error(`refusing to overwrite unmanaged agent file: ${file} (use --force to back it up first)`);
|
|
63
|
+
backup = backupFile(file, join(targetRoot, ".codex-teams-backups"));
|
|
64
|
+
}
|
|
65
|
+
writeFileAtomic(file, content);
|
|
66
|
+
files.push(file);
|
|
67
|
+
nextEntries.push({
|
|
68
|
+
team: team.name,
|
|
69
|
+
scope,
|
|
70
|
+
file,
|
|
71
|
+
backup,
|
|
72
|
+
hash: sha256(content),
|
|
73
|
+
kind: "agent",
|
|
74
|
+
member: member.name,
|
|
75
|
+
installed_at: installedAt,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
writeManifest(targetRoot, { ...manifest, entries: nextEntries });
|
|
79
|
+
return { team: team.name, scope, targetRoot, files, removed, warnings };
|
|
80
|
+
}
|
|
81
|
+
export function uninstallTeam(team, opts = {}) {
|
|
82
|
+
const scope = opts.scope ?? "user";
|
|
83
|
+
const targetRoot = resolveAgentsRoot(scope, opts);
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const manifest = readManifest(targetRoot, warnings);
|
|
86
|
+
const removed = [];
|
|
87
|
+
const restored = [];
|
|
88
|
+
const keep = [];
|
|
89
|
+
for (const entry of manifest.entries) {
|
|
90
|
+
if (entry.team !== team || entry.kind !== "agent") {
|
|
91
|
+
keep.push(entry);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const unsafeReason = validateManagedEntry(entry, targetRoot, scope);
|
|
95
|
+
if (unsafeReason) {
|
|
96
|
+
warnings.push(`skipping manifest entry for ${entry.file}: ${unsafeReason}`);
|
|
97
|
+
keep.push(entry);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (existsSync(entry.file)) {
|
|
101
|
+
const currentHash = sha256(readFileSync(entry.file, "utf8"));
|
|
102
|
+
if (currentHash !== entry.hash)
|
|
103
|
+
throw new Error(`installed file changed since install; refusing uninstall without manual review: ${entry.file}`);
|
|
104
|
+
rmSync(entry.file, { force: true });
|
|
105
|
+
removed.push(entry.file);
|
|
106
|
+
}
|
|
107
|
+
if (entry.backup && existsSync(entry.backup)) {
|
|
108
|
+
copyFileSync(entry.backup, entry.file);
|
|
109
|
+
restored.push(entry.file);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
writeManifest(targetRoot, { ...manifest, entries: keep });
|
|
113
|
+
return { team, scope, targetRoot, removed, restored, warnings };
|
|
114
|
+
}
|
|
115
|
+
export function renderAgentToml(team, memberName) {
|
|
116
|
+
const member = team.members.find(item => item.name === memberName);
|
|
117
|
+
if (!member)
|
|
118
|
+
throw new Error(`unknown member: ${memberName}`);
|
|
119
|
+
const agentName = memberAgentName(team, member);
|
|
120
|
+
return renderToml({
|
|
121
|
+
name: agentName,
|
|
122
|
+
description: member.focus,
|
|
123
|
+
model: resolveMemberModel(team, member),
|
|
124
|
+
sandbox_mode: resolveMemberSandbox(team, member),
|
|
125
|
+
nickname_candidates: [member.name],
|
|
126
|
+
developer_instructions: assembleDeveloperInstructions(team, member),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function resolveCodexHome(env = process.env, explicit) {
|
|
130
|
+
return resolve(explicit ?? env.CODEX_HOME ?? join(homedir(), ".codex"));
|
|
131
|
+
}
|
|
132
|
+
export function resolveAgentsRoot(scope, opts = {}) {
|
|
133
|
+
if (scope === "project") {
|
|
134
|
+
const projectRoot = realProjectRoot(opts.cwd ?? process.cwd());
|
|
135
|
+
return assertConfinedRoot(join(projectRoot, ".codex", "agents"), projectRoot);
|
|
136
|
+
}
|
|
137
|
+
return join(resolveCodexHome(opts.env, opts.codexHome), "agents");
|
|
138
|
+
}
|
|
139
|
+
export function manifestPath(targetRoot) {
|
|
140
|
+
return join(targetRoot, ".codex-teams-manifest.json");
|
|
141
|
+
}
|
|
142
|
+
export function readManifest(targetRoot, warnings = []) {
|
|
143
|
+
const path = manifestPath(targetRoot);
|
|
144
|
+
if (!existsSync(path))
|
|
145
|
+
return { version: 1, owner: "@codex-modules/teams", entries: [] };
|
|
146
|
+
const manifest = readJson(path);
|
|
147
|
+
if (manifest.version !== 1 || manifest.owner !== "@codex-modules/teams" || !Array.isArray(manifest.entries)) {
|
|
148
|
+
throw new Error(`unsupported teams manifest format: ${path}`);
|
|
149
|
+
}
|
|
150
|
+
return { ...manifest, entries: manifest.entries.flatMap((entry, index) => validateManifestEntry(entry, `${path} entries[${index}]`, warnings)) };
|
|
151
|
+
}
|
|
152
|
+
export function writeManifest(targetRoot, manifest) {
|
|
153
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
154
|
+
writeJsonAtomic(manifestPath(targetRoot), manifest);
|
|
155
|
+
}
|
|
156
|
+
export function findCodexBinary(env = process.env) {
|
|
157
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
158
|
+
if (!pathValue)
|
|
159
|
+
return null;
|
|
160
|
+
const names = process.platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
|
|
161
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
162
|
+
if (!dir)
|
|
163
|
+
continue;
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
const candidate = join(dir, name);
|
|
166
|
+
try {
|
|
167
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
168
|
+
return candidate;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// keep searching
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
export function listInstalledTeams(targetRoot) {
|
|
178
|
+
return [...new Set(readManifest(targetRoot).entries.filter(entry => entry.kind === "agent").map(entry => entry.team))].sort();
|
|
179
|
+
}
|
|
180
|
+
function checkModels(team, opts) {
|
|
181
|
+
const warnings = [];
|
|
182
|
+
const bin = findCodexBinary(opts.env);
|
|
183
|
+
if (!bin)
|
|
184
|
+
return ["codex binary not found; skipping model catalog preflight"];
|
|
185
|
+
const result = spawnSync(bin, ["debug", "models"], {
|
|
186
|
+
env: { ...process.env, ...(opts.env ?? {}), CODEX_HOME: resolveCodexHome(opts.env, opts.codexHome) },
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
189
|
+
timeout: 10_000,
|
|
190
|
+
});
|
|
191
|
+
if (result.status !== 0 || result.error) {
|
|
192
|
+
const reason = result.error ? result.error.message : result.stderr.trim();
|
|
193
|
+
return [`codex debug models unavailable; continuing without strict model preflight${reason ? ` (${reason})` : ""}`];
|
|
194
|
+
}
|
|
195
|
+
let catalog;
|
|
196
|
+
try {
|
|
197
|
+
catalog = parseModelCatalog(result.stdout);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
201
|
+
return [`codex debug models returned non-JSON catalog; continuing without strict model preflight (${reason})`];
|
|
202
|
+
}
|
|
203
|
+
if (catalog.length === 0)
|
|
204
|
+
return ["codex debug models returned no parseable catalog entries; continuing"];
|
|
205
|
+
const available = new Set(catalog);
|
|
206
|
+
for (const member of team.members) {
|
|
207
|
+
const model = resolveMemberModel(team, member);
|
|
208
|
+
if (!available.has(model))
|
|
209
|
+
throw new Error(`model ${model} for member ${member.name} is not in codex debug models catalog`);
|
|
210
|
+
}
|
|
211
|
+
return warnings;
|
|
212
|
+
}
|
|
213
|
+
export function parseModelCatalog(stdout) {
|
|
214
|
+
const parsed = JSON.parse(stdout);
|
|
215
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.models))
|
|
216
|
+
return [];
|
|
217
|
+
const values = new Set();
|
|
218
|
+
for (const item of parsed.models) {
|
|
219
|
+
if (!isRecord(item))
|
|
220
|
+
continue;
|
|
221
|
+
const value = typeof item.slug === "string" ? item.slug : typeof item.id === "string" ? item.id : null;
|
|
222
|
+
if (value)
|
|
223
|
+
values.add(value);
|
|
224
|
+
}
|
|
225
|
+
return [...values].sort();
|
|
226
|
+
}
|
|
227
|
+
function backupFile(file, backupDir) {
|
|
228
|
+
mkdirSync(backupDir, { recursive: true });
|
|
229
|
+
const backup = join(backupDir, `${basename(file)}.${new Date().toISOString().replace(/[:.]/g, "-")}.${process.pid}.bak`);
|
|
230
|
+
copyFileSync(file, backup);
|
|
231
|
+
try {
|
|
232
|
+
chmodSync(backup, statSync(file).mode & 0o777);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// best effort
|
|
236
|
+
}
|
|
237
|
+
return backup;
|
|
238
|
+
}
|
|
239
|
+
export function backupSkillFile(file, backupDir) {
|
|
240
|
+
return backupFile(file, backupDir);
|
|
241
|
+
}
|
|
242
|
+
function sha256(content) {
|
|
243
|
+
return createHash("sha256").update(content).digest("hex");
|
|
244
|
+
}
|
|
245
|
+
function assertProjectRoot(targetRoot, opts) {
|
|
246
|
+
assertConfinedRoot(targetRoot, realProjectRoot(opts.cwd ?? process.cwd()));
|
|
247
|
+
}
|
|
248
|
+
function realProjectRoot(cwd) {
|
|
249
|
+
return statSync(resolve(cwd)).isDirectory() ? resolve(cwd) : dirname(resolve(cwd));
|
|
250
|
+
}
|
|
251
|
+
function validateManagedEntry(entry, targetRoot, scope) {
|
|
252
|
+
if (entry.scope !== scope)
|
|
253
|
+
return `scope ${entry.scope} does not match ${scope}`;
|
|
254
|
+
if (!entry.member)
|
|
255
|
+
return "agent entry is missing member";
|
|
256
|
+
if (!isConfinedPath(entry.file, targetRoot))
|
|
257
|
+
return "file path is outside target root";
|
|
258
|
+
if (entry.backup && !isConfinedPath(entry.backup, targetRoot))
|
|
259
|
+
return "backup path is outside target root";
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function validateManifestEntry(value, label, warnings) {
|
|
263
|
+
if (!isRecord(value)) {
|
|
264
|
+
warnings.push(`skipping invalid manifest entry at ${label}: expected object`);
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const backup = value.backup;
|
|
268
|
+
const kind = value.kind;
|
|
269
|
+
const scope = value.scope;
|
|
270
|
+
if (typeof value.team !== "string" ||
|
|
271
|
+
(scope !== "user" && scope !== "project" && scope !== "skill") ||
|
|
272
|
+
typeof value.file !== "string" ||
|
|
273
|
+
!(backup === undefined || backup === null || typeof backup === "string") ||
|
|
274
|
+
typeof value.hash !== "string" ||
|
|
275
|
+
(kind !== "agent" && kind !== "skill") ||
|
|
276
|
+
typeof value.installed_at !== "string" ||
|
|
277
|
+
(kind === "agent" && typeof value.member !== "string")) {
|
|
278
|
+
warnings.push(`skipping invalid manifest entry at ${label}: unsupported entry schema`);
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
return [value];
|
|
282
|
+
}
|
|
283
|
+
function isRecord(value) {
|
|
284
|
+
return typeof value === "object" && value !== null;
|
|
285
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DoctorReport } from "./types.js";
|
|
2
|
+
export type DoctorOptions = {
|
|
3
|
+
codexHome?: string;
|
|
4
|
+
stateDir?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
7
|
+
};
|
|
8
|
+
export declare function doctor(opts?: DoctorOptions): DoctorReport;
|
|
9
|
+
export declare function doctorIsHealthy(report: DoctorReport): boolean;
|
|
10
|
+
export declare function formatDoctor(report: DoctorReport): string;
|