cairn-ai 0.1.2
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/.agents/workflows/cairn-doctor.md +22 -0
- package/.agents/workflows/cairn-install.md +17 -0
- package/.agents/workflows/cairn-memory.md +15 -0
- package/.agents/workflows/cairn-plan.md +21 -0
- package/.agents/workflows/cairn-review.md +14 -0
- package/.agents/workflows/cairn-toolcheck.md +17 -0
- package/.agents/workflows/cairn-uninstall.md +17 -0
- package/.agents/workflows/cairn-upgrade.md +17 -0
- package/.agents/workflows/cairn-work.md +20 -0
- package/.claude/agents/explorer.md +5 -0
- package/.claude/agents/worker.md +5 -0
- package/.claude/commands/cairn-doctor.md +13 -0
- package/.claude/commands/cairn-install.md +13 -0
- package/.claude/commands/cairn-memory.md +5 -0
- package/.claude/commands/cairn-plan.md +5 -0
- package/.claude/commands/cairn-review.md +5 -0
- package/.claude/commands/cairn-toolcheck.md +10 -0
- package/.claude/commands/cairn-uninstall.md +13 -0
- package/.claude/commands/cairn-upgrade.md +13 -0
- package/.claude/commands/cairn-work.md +5 -0
- package/.codex-plugin/plugin.json +30 -0
- package/LICENSE +21 -0
- package/README.de.md +130 -0
- package/README.es.md +130 -0
- package/README.fr.md +130 -0
- package/README.ja.md +130 -0
- package/README.ko.md +130 -0
- package/README.md +138 -0
- package/README.pt.md +130 -0
- package/README.zh.md +130 -0
- package/agents/explorer.md +31 -0
- package/agents/worker.md +32 -0
- package/commands/cairn-doctor.md +27 -0
- package/commands/cairn-install.md +24 -0
- package/commands/cairn-memory.md +17 -0
- package/commands/cairn-plan.md +23 -0
- package/commands/cairn-review.md +16 -0
- package/commands/cairn-toolcheck.md +19 -0
- package/commands/cairn-uninstall.md +24 -0
- package/commands/cairn-upgrade.md +23 -0
- package/commands/cairn-work.md +22 -0
- package/docs/model-guidance/README.md +31 -0
- package/docs/model-guidance/claude.md +46 -0
- package/docs/model-guidance/codex.md +48 -0
- package/hooks/hooks.json +65 -0
- package/package.json +49 -0
- package/scripts/cairn +115 -0
- package/scripts/cairn-lifecycle.mjs +343 -0
- package/scripts/cairn-state.sh +104 -0
- package/scripts/cairn-toolcheck.mjs +205 -0
- package/skills/cairn-memory/SKILL.md +40 -0
- package/skills/cairn-plan/SKILL.md +137 -0
- package/skills/cairn-review/SKILL.md +44 -0
- package/skills/cairn-work/SKILL.md +81 -0
- package/templates/MEMORY.md +14 -0
- package/templates/PLAN.md +17 -0
- package/templates/domain-memory.md +20 -0
- package/templates/work-plan.md +86 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Codex Model Guidance
|
|
2
|
+
|
|
3
|
+
Codex-family models are strong at small implementation slices, explicit tool use, and repeatable verification. In Cairn, prefer them for bounded `worker` tasks and structurally clear planning.
|
|
4
|
+
|
|
5
|
+
## Use Strengths
|
|
6
|
+
|
|
7
|
+
- Quickly handle implementation slices with clear file scope.
|
|
8
|
+
- Read existing patterns and make minimal changes in the same style.
|
|
9
|
+
- Run repository tool readiness checks and bootstrap missing command-line tools.
|
|
10
|
+
- Capture module acceptance verification and surface integration verification from command results.
|
|
11
|
+
- Run dry-run, check, plan, diff, validate, or equivalent commands before mutating external state.
|
|
12
|
+
- Explore independent work in parallel and sequence only dependent work.
|
|
13
|
+
|
|
14
|
+
## Adjustment Rules
|
|
15
|
+
|
|
16
|
+
- Do not start implementation without a plan.
|
|
17
|
+
- Keep work units small. Split large changes into smaller plan slices.
|
|
18
|
+
- If `apply_patch` is unstable in the environment, use an editing tool or a clear file update path.
|
|
19
|
+
- Do not duplicate the same exploration. Wait for already delegated searches.
|
|
20
|
+
- Do not trust success-looking output alone. Confirm real files, commands, and surface evidence.
|
|
21
|
+
- Do not skip LSP, typecheck, lint, dry-run, or verification because a tool is missing. Try repository-local installation or record the failed install attempt as a blocker.
|
|
22
|
+
- Do not continue an open-ended verification loop. Use at most two verification passes per slice unless the plan records a risk-based exception.
|
|
23
|
+
- Write user-visible output in the OS locale unless the user asks for another language.
|
|
24
|
+
|
|
25
|
+
## Path Guidance
|
|
26
|
+
|
|
27
|
+
### Planning
|
|
28
|
+
|
|
29
|
+
- On Light Path, write explicit slice contracts when a plan artifact exists.
|
|
30
|
+
- Record detected stack, required tools, install commands, and tool blockers.
|
|
31
|
+
- Specify file scope and verification commands so implementation does not need to rediscover them.
|
|
32
|
+
- Specify the dry-run or check command for slices that can mutate external state, or record why none exists.
|
|
33
|
+
- On failure, prefer slice re-splitting over a repeated loop.
|
|
34
|
+
|
|
35
|
+
### Implementation
|
|
36
|
+
|
|
37
|
+
- Edit only within the file scope listed in the plan.
|
|
38
|
+
- Confirm required tools are available. Install or bootstrap missing required tools before implementation.
|
|
39
|
+
- Run the recorded dry-run or check command before external-state mutation when applicable.
|
|
40
|
+
- After changes, run both module acceptance verification and surface integration verification.
|
|
41
|
+
- If a gate fails, diagnose once, shrink or split the slice, and rerun both gates. Stop after the second failed pass and record the blocker.
|
|
42
|
+
- Return evidence in a form that can be recorded in `docs/plan/<topic>.md`.
|
|
43
|
+
|
|
44
|
+
### Delegation
|
|
45
|
+
|
|
46
|
+
- Handle search, file listing, command checks, and QA artifact capture.
|
|
47
|
+
- Use `explorer` for independent read-only discovery or read-only verification when available.
|
|
48
|
+
- When using `worker`, state clear file ownership and tell the worker not to revert others' edits.
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "sh \"${PLUGIN_ROOT}/scripts/cairn-state.sh\" session-start",
|
|
9
|
+
"timeout": 10,
|
|
10
|
+
"statusMessage": "Cairn: preparing repository memory and plan indexes"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "sh \"${PLUGIN_ROOT}/scripts/cairn-state.sh\" user-prompt-submit",
|
|
21
|
+
"timeout": 10,
|
|
22
|
+
"statusMessage": "Cairn: loading compact work context"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"PostToolUse": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "^(apply_patch|write|Write|edit|Edit|multi_edit|multiedit|MultiEdit)$",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "sh \"${PLUGIN_ROOT}/scripts/cairn-state.sh\" post-tool-use",
|
|
34
|
+
"timeout": 10,
|
|
35
|
+
"statusMessage": "Cairn: checking memory and plan indexes"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"Stop": [
|
|
41
|
+
{
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "sh \"${PLUGIN_ROOT}/scripts/cairn-state.sh\" stop",
|
|
46
|
+
"timeout": 10,
|
|
47
|
+
"statusMessage": "Cairn: checking two-gate evidence requirements"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"SubagentStop": [
|
|
53
|
+
{
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "sh \"${PLUGIN_ROOT}/scripts/cairn-state.sh\" subagent-stop",
|
|
58
|
+
"timeout": 10,
|
|
59
|
+
"statusMessage": "Cairn: checking subagent handoff evidence"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cairn-ai",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Token-efficient agent harness with tool readiness for Codex, Claude Code, and Antigravity",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "WkNam"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/dspaudio/cairn-ai",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/dspaudio/cairn-ai.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"codex",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"antigravity",
|
|
18
|
+
"agent",
|
|
19
|
+
"harness",
|
|
20
|
+
"plugin",
|
|
21
|
+
"workflow"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"cairn": "scripts/cairn",
|
|
25
|
+
"cairn-ai": "scripts/cairn"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
".agents/",
|
|
29
|
+
".claude/",
|
|
30
|
+
".codex-plugin/",
|
|
31
|
+
"agents/",
|
|
32
|
+
"commands/",
|
|
33
|
+
"docs/model-guidance/",
|
|
34
|
+
"hooks/",
|
|
35
|
+
"scripts/",
|
|
36
|
+
"skills/",
|
|
37
|
+
"templates/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"README.*.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"check": "node --check scripts/cairn-lifecycle.mjs && node --check scripts/cairn-toolcheck.mjs && node -e \"JSON.parse(require('node:fs').readFileSync('package.json','utf8')); JSON.parse(require('node:fs').readFileSync('.codex-plugin/plugin.json','utf8')); JSON.parse(require('node:fs').readFileSync('hooks/hooks.json','utf8'))\" && npm test",
|
|
43
|
+
"test": "node --test",
|
|
44
|
+
"prepack": "npm run check"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/scripts/cairn
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
cmd="${1:-help}"
|
|
5
|
+
shift || true
|
|
6
|
+
locale="${LC_ALL:-${LC_MESSAGES:-${LANG:-en}}}"
|
|
7
|
+
|
|
8
|
+
script_path="$0"
|
|
9
|
+
link_target="$(readlink "$script_path" 2>/dev/null || true)"
|
|
10
|
+
if [ -n "$link_target" ]; then
|
|
11
|
+
case "$link_target" in
|
|
12
|
+
/*) script_path="$link_target" ;;
|
|
13
|
+
*) script_path="$(dirname "$script_path")/$link_target" ;;
|
|
14
|
+
esac
|
|
15
|
+
fi
|
|
16
|
+
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
|
|
17
|
+
|
|
18
|
+
locale_family() {
|
|
19
|
+
case "$locale" in
|
|
20
|
+
ko*|KO*) printf '%s\n' ko ;;
|
|
21
|
+
ja*|JA*) printf '%s\n' ja ;;
|
|
22
|
+
zh*|ZH*) printf '%s\n' zh ;;
|
|
23
|
+
es*|ES*) printf '%s\n' es ;;
|
|
24
|
+
fr*|FR*) printf '%s\n' fr ;;
|
|
25
|
+
de*|DE*) printf '%s\n' de ;;
|
|
26
|
+
pt*|PT*) printf '%s\n' pt ;;
|
|
27
|
+
*) printf '%s\n' en ;;
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
say() {
|
|
32
|
+
key="$1"
|
|
33
|
+
family="$(locale_family)"
|
|
34
|
+
case "$family:$key" in
|
|
35
|
+
ko:memory) printf '%s\n' "cairn-memory 스킬을 사용하세요. MEMORY.md는 짧은 색인으로, 상세 내용은 docs/memory/<domain>.md에 기록합니다." ;;
|
|
36
|
+
ko:plan) printf '%s\n' "cairn-plan 스킬을 사용하세요. PLAN.md는 색인으로, 상세 계획은 docs/plan/<topic>.md에 기록합니다." ;;
|
|
37
|
+
ko:work) printf '%s\n' "cairn-work 스킬을 사용하세요. 하나의 모듈 조각을 실행하고 모듈 증거와 표면 증거를 확보합니다." ;;
|
|
38
|
+
ko:review) printf '%s\n' "cairn-review 스킬을 사용하세요. PLAN.md와 MEMORY.md 기준으로 증거를 비교합니다." ;;
|
|
39
|
+
ko:toolcheck) printf '%s\n' "저장소 도구를 점검하려면 cairn toolcheck 를 실행하세요. 누락 도구 설치까지 시도하려면 cairn toolcheck --install 을 사용합니다." ;;
|
|
40
|
+
ko:usage) printf '%s\n' "사용법: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
41
|
+
ja:memory) printf '%s\n' "cairn-memory スキルを使用してください。MEMORY.md は短い索引にし、詳細は docs/memory/<domain>.md に記録します。" ;;
|
|
42
|
+
ja:plan) printf '%s\n' "cairn-plan スキルを使用してください。PLAN.md は索引にし、詳細計画は docs/plan/<topic>.md に記録します。" ;;
|
|
43
|
+
ja:work) printf '%s\n' "cairn-work スキルを使用してください。1つのモジュールスライスを実行し、モジュール証拠と表面証拠を取得します。" ;;
|
|
44
|
+
ja:review) printf '%s\n' "cairn-review スキルを使用してください。PLAN.md と MEMORY.md に照らして証拠を比較します。" ;;
|
|
45
|
+
ja:toolcheck) printf '%s\n' "リポジトリツールを確認するには cairn toolcheck を実行してください。不足ツールのインストールも試すには cairn toolcheck --install を使用します。" ;;
|
|
46
|
+
ja:usage) printf '%s\n' "使い方: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
47
|
+
zh:memory) printf '%s\n' "使用 cairn-memory skill。将 MEMORY.md 保持为简短索引,并将详细内容写入 docs/memory/<domain>.md。" ;;
|
|
48
|
+
zh:plan) printf '%s\n' "使用 cairn-plan skill。将 PLAN.md 保持为索引,并将详细计划写入 docs/plan/<topic>.md。" ;;
|
|
49
|
+
zh:work) printf '%s\n' "使用 cairn-work skill。执行一个模块切片,并收集模块证据和表面证据。" ;;
|
|
50
|
+
zh:review) printf '%s\n' "使用 cairn-review skill。根据 PLAN.md 和 MEMORY.md 对比证据。" ;;
|
|
51
|
+
zh:toolcheck) printf '%s\n' "运行 cairn toolcheck 检查仓库工具。使用 cairn toolcheck --install 尝试安装缺失工具。" ;;
|
|
52
|
+
zh:usage) printf '%s\n' "用法: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
53
|
+
es:memory) printf '%s\n' "Usa la skill cairn-memory. Mantén MEMORY.md como un índice breve y escribe los detalles en docs/memory/<domain>.md." ;;
|
|
54
|
+
es:plan) printf '%s\n' "Usa la skill cairn-plan. Mantén PLAN.md como índice y escribe los planes detallados en docs/plan/<topic>.md." ;;
|
|
55
|
+
es:work) printf '%s\n' "Usa la skill cairn-work. Ejecuta un solo segmento de módulo y captura evidencia de módulo y de superficie." ;;
|
|
56
|
+
es:review) printf '%s\n' "Usa la skill cairn-review. Compara la evidencia con PLAN.md y MEMORY.md." ;;
|
|
57
|
+
es:toolcheck) printf '%s\n' "Ejecuta cairn toolcheck para revisar herramientas del repositorio. Usa cairn toolcheck --install para intentar instalar herramientas faltantes." ;;
|
|
58
|
+
es:usage) printf '%s\n' "Uso: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
59
|
+
fr:memory) printf '%s\n' "Utilisez la skill cairn-memory. Gardez MEMORY.md comme index court et écrivez les détails dans docs/memory/<domain>.md." ;;
|
|
60
|
+
fr:plan) printf '%s\n' "Utilisez la skill cairn-plan. Gardez PLAN.md comme index et écrivez les plans détaillés dans docs/plan/<topic>.md." ;;
|
|
61
|
+
fr:work) printf '%s\n' "Utilisez la skill cairn-work. Exécutez une tranche de module et capturez les preuves de module et de surface." ;;
|
|
62
|
+
fr:review) printf '%s\n' "Utilisez la skill cairn-review. Comparez les preuves avec PLAN.md et MEMORY.md." ;;
|
|
63
|
+
fr:toolcheck) printf '%s\n' "Exécutez cairn toolcheck pour vérifier les outils du dépôt. Utilisez cairn toolcheck --install pour tenter d'installer les outils manquants." ;;
|
|
64
|
+
fr:usage) printf '%s\n' "Utilisation: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
65
|
+
de:memory) printf '%s\n' "Verwenden Sie die Skill cairn-memory. Halten Sie MEMORY.md als kurzen Index und schreiben Sie Details nach docs/memory/<domain>.md." ;;
|
|
66
|
+
de:plan) printf '%s\n' "Verwenden Sie die Skill cairn-plan. Halten Sie PLAN.md als Index und schreiben Sie Detailpläne nach docs/plan/<topic>.md." ;;
|
|
67
|
+
de:work) printf '%s\n' "Verwenden Sie die Skill cairn-work. Führen Sie einen Modulabschnitt aus und erfassen Sie Modul- und Oberflächenbelege." ;;
|
|
68
|
+
de:review) printf '%s\n' "Verwenden Sie die Skill cairn-review. Vergleichen Sie Belege mit PLAN.md und MEMORY.md." ;;
|
|
69
|
+
de:toolcheck) printf '%s\n' "Führen Sie cairn toolcheck aus, um Repository-Tools zu prüfen. Verwenden Sie cairn toolcheck --install, um fehlende Tools zu installieren." ;;
|
|
70
|
+
de:usage) printf '%s\n' "Verwendung: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
71
|
+
pt:memory) printf '%s\n' "Use a skill cairn-memory. Mantenha MEMORY.md como um índice curto e escreva detalhes em docs/memory/<domain>.md." ;;
|
|
72
|
+
pt:plan) printf '%s\n' "Use a skill cairn-plan. Mantenha PLAN.md como índice e escreva planos detalhados em docs/plan/<topic>.md." ;;
|
|
73
|
+
pt:work) printf '%s\n' "Use a skill cairn-work. Execute uma fatia de módulo e capture evidências de módulo e de superfície." ;;
|
|
74
|
+
pt:review) printf '%s\n' "Use a skill cairn-review. Compare evidências com PLAN.md e MEMORY.md." ;;
|
|
75
|
+
pt:toolcheck) printf '%s\n' "Execute cairn toolcheck para verificar ferramentas do repositório. Use cairn toolcheck --install para tentar instalar ferramentas ausentes." ;;
|
|
76
|
+
pt:usage) printf '%s\n' "Uso: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
77
|
+
*)
|
|
78
|
+
case "$key" in
|
|
79
|
+
memory) printf '%s\n' "Use the cairn-memory skill. Keep MEMORY.md as a short index and write details to docs/memory/<domain>.md." ;;
|
|
80
|
+
plan) printf '%s\n' "Use the cairn-plan skill. Keep PLAN.md as an index and write detailed plans to docs/plan/<topic>.md." ;;
|
|
81
|
+
work) printf '%s\n' "Use the cairn-work skill. Execute one module slice and capture module and surface evidence." ;;
|
|
82
|
+
review) printf '%s\n' "Use the cairn-review skill. Compare evidence against PLAN.md and MEMORY.md." ;;
|
|
83
|
+
toolcheck) printf '%s\n' "Run cairn toolcheck to inspect repository tools. Use cairn toolcheck --install to attempt missing tool installation." ;;
|
|
84
|
+
usage) printf '%s\n' "Usage: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck" ;;
|
|
85
|
+
esac
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "$cmd" in
|
|
91
|
+
install|upgrade|doctor|uninstall)
|
|
92
|
+
node "$script_dir/cairn-lifecycle.mjs" "$cmd" "$@"
|
|
93
|
+
;;
|
|
94
|
+
init)
|
|
95
|
+
sh "$script_dir/cairn-state.sh" manual
|
|
96
|
+
;;
|
|
97
|
+
memory)
|
|
98
|
+
say memory
|
|
99
|
+
;;
|
|
100
|
+
plan)
|
|
101
|
+
say plan
|
|
102
|
+
;;
|
|
103
|
+
work)
|
|
104
|
+
say work
|
|
105
|
+
;;
|
|
106
|
+
review)
|
|
107
|
+
say review
|
|
108
|
+
;;
|
|
109
|
+
toolcheck)
|
|
110
|
+
node "$script_dir/cairn-toolcheck.mjs" "$@"
|
|
111
|
+
;;
|
|
112
|
+
*)
|
|
113
|
+
say usage
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const codexHome = resolve(process.env.CODEX_HOME ?? join(process.env.HOME ?? ".", ".codex"));
|
|
10
|
+
const claudeHome = resolve(process.env.CLAUDE_HOME ?? join(process.env.HOME ?? ".", ".claude"));
|
|
11
|
+
const antigravityHome = resolve(process.env.ANTIGRAVITY_HOME ?? join(process.env.HOME ?? ".", ".agents"));
|
|
12
|
+
const antigravityCliHome = resolve(process.env.ANTIGRAVITY_CLI_HOME ?? join(process.env.HOME ?? ".", ".gemini", "antigravity-cli"));
|
|
13
|
+
const configPath = process.env.CODEX_CONFIG_PATH ?? join(codexHome, "config.toml");
|
|
14
|
+
const marketplaceName = "cairn";
|
|
15
|
+
const pluginName = "cairn";
|
|
16
|
+
const marketplaceRoot = join(codexHome, "plugins", "cache", marketplaceName);
|
|
17
|
+
const installedPluginRoot = join(marketplaceRoot, "plugins", pluginName);
|
|
18
|
+
const marketplaceJsonPath = join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
|
|
19
|
+
|
|
20
|
+
if (isCliEntry()) {
|
|
21
|
+
const command = process.argv[2] ?? "help";
|
|
22
|
+
if (command === "install" || command === "upgrade") await install(command);
|
|
23
|
+
else if (command === "doctor") await doctor();
|
|
24
|
+
else if (command === "uninstall") await uninstall();
|
|
25
|
+
else help();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function install(mode) {
|
|
29
|
+
await copyPlugin();
|
|
30
|
+
await writeMarketplace();
|
|
31
|
+
await installClaudeFiles();
|
|
32
|
+
await installAntigravityFiles();
|
|
33
|
+
const hookStates = await trustedHookStates();
|
|
34
|
+
const config = await readText(configPath);
|
|
35
|
+
await backupConfig();
|
|
36
|
+
await writeFile(configPath, `${updateConfig(config, hookStates).trimEnd()}\n`);
|
|
37
|
+
console.log(t(mode === "upgrade" ? "upgradeComplete" : "installComplete"));
|
|
38
|
+
console.log(`Codex plugin: ${installedPluginRoot}`);
|
|
39
|
+
console.log(`Marketplace: ${marketplaceJsonPath}`);
|
|
40
|
+
console.log(`Hook trust states: ${hookStates.length}`);
|
|
41
|
+
console.log(`Antigravity skills: ${join(antigravityHome, "skills")}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function doctor() {
|
|
45
|
+
const checks = [];
|
|
46
|
+
checks.push(["source", await exists(join(pluginRoot, ".codex-plugin", "plugin.json"))]);
|
|
47
|
+
checks.push(["installed plugin", await exists(join(installedPluginRoot, ".codex-plugin", "plugin.json"))]);
|
|
48
|
+
checks.push(["marketplace", await exists(marketplaceJsonPath)]);
|
|
49
|
+
const installedManifest = await readJson(join(installedPluginRoot, ".codex-plugin", "plugin.json"));
|
|
50
|
+
checks.push(["installed hooks manifest field", installedManifest?.hooks === "./hooks/hooks.json"]);
|
|
51
|
+
checks.push(["hooks file", await exists(join(installedPluginRoot, "hooks", "hooks.json"))]);
|
|
52
|
+
const config = await readText(configPath);
|
|
53
|
+
checks.push(["config features.plugins", hasSetting(config, "features", "plugins", "true")]);
|
|
54
|
+
checks.push(["config features.plugin_hooks", hasSetting(config, "features", "plugin_hooks", "true")]);
|
|
55
|
+
checks.push(["config marketplace", config.includes("[marketplaces.cairn]")]);
|
|
56
|
+
checks.push(["config plugin enabled", config.includes('[plugins."cairn@cairn"]') && hasSetting(config, 'plugins."cairn@cairn"', "enabled", "true")]);
|
|
57
|
+
const expectedStates = await trustedHookStates();
|
|
58
|
+
const missingStates = expectedStates.filter((state) => !config.includes(`[hooks.state.${JSON.stringify(state.key)}]`));
|
|
59
|
+
checks.push(["trusted hook states", expectedStates.length > 0 && missingStates.length === 0]);
|
|
60
|
+
checks.push(["Claude commands", await exists(join(claudeHome, "commands", "cairn-plan.md"))]);
|
|
61
|
+
checks.push(["Claude agents", await exists(join(claudeHome, "agents", "cairn-explorer.md"))]);
|
|
62
|
+
checks.push(["Antigravity skills", await exists(join(antigravityHome, "skills", "cairn-plan", "SKILL.md"))]);
|
|
63
|
+
checks.push(["Antigravity workflows", await exists(join(antigravityHome, "workflows", "cairn-plan.md"))]);
|
|
64
|
+
checks.push(["Antigravity CLI skills", await exists(join(antigravityCliHome, "skills", "cairn-plan", "SKILL.md"))]);
|
|
65
|
+
checks.push(["Antigravity CLI workflows", await exists(join(antigravityCliHome, "workflows", "cairn-plan.md"))]);
|
|
66
|
+
for (const [name, ok] of checks) console.log(`${ok ? "OK" : "FAIL"} ${name}`);
|
|
67
|
+
if (checks.some(([, ok]) => !ok)) process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function uninstall() {
|
|
71
|
+
const config = await readText(configPath);
|
|
72
|
+
await backupConfig();
|
|
73
|
+
await writeFile(configPath, `${removeCairnConfig(config).trimEnd()}\n`);
|
|
74
|
+
await rm(marketplaceRoot, { recursive: true, force: true });
|
|
75
|
+
for (const name of commandNames()) {
|
|
76
|
+
await rm(join(claudeHome, "commands", `cairn-${name}.md`), { force: true });
|
|
77
|
+
}
|
|
78
|
+
for (const name of ["architect", "planner", "builder", "reviewer", "explorer", "worker"]) {
|
|
79
|
+
await rm(join(claudeHome, "agents", `cairn-${name}.md`), { force: true });
|
|
80
|
+
}
|
|
81
|
+
await uninstallAntigravityFiles();
|
|
82
|
+
console.log(t("uninstallComplete"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function copyPlugin() {
|
|
86
|
+
await rm(installedPluginRoot, { recursive: true, force: true });
|
|
87
|
+
await mkdir(dirname(installedPluginRoot), { recursive: true });
|
|
88
|
+
await cp(pluginRoot, installedPluginRoot, {
|
|
89
|
+
recursive: true,
|
|
90
|
+
filter: (source) => !source.includes(`${pluginRoot}/.git`) && !source.includes(`${pluginRoot}/node_modules`),
|
|
91
|
+
});
|
|
92
|
+
const manifestPath = join(installedPluginRoot, ".codex-plugin", "plugin.json");
|
|
93
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
94
|
+
manifest.hooks = "./hooks/hooks.json";
|
|
95
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeMarketplace() {
|
|
99
|
+
await mkdir(dirname(marketplaceJsonPath), { recursive: true });
|
|
100
|
+
const marketplace = {
|
|
101
|
+
name: marketplaceName,
|
|
102
|
+
interface: { displayName: "Cairn" },
|
|
103
|
+
plugins: [{
|
|
104
|
+
name: pluginName,
|
|
105
|
+
source: { source: "local", path: "./plugins/cairn" },
|
|
106
|
+
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
|
|
107
|
+
category: "Productivity",
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
await writeFile(marketplaceJsonPath, `${JSON.stringify(marketplace, null, 2)}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function installClaudeFiles() {
|
|
114
|
+
await mkdir(join(claudeHome, "commands"), { recursive: true });
|
|
115
|
+
await mkdir(join(claudeHome, "agents"), { recursive: true });
|
|
116
|
+
for (const name of commandNames()) {
|
|
117
|
+
await cp(join(pluginRoot, ".claude", "commands", `cairn-${name}.md`), join(claudeHome, "commands", `cairn-${name}.md`));
|
|
118
|
+
}
|
|
119
|
+
for (const name of ["explorer", "worker"]) {
|
|
120
|
+
await cp(join(pluginRoot, ".claude", "agents", `${name}.md`), join(claudeHome, "agents", `cairn-${name}.md`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function installAntigravityFiles() {
|
|
125
|
+
for (const root of [antigravityHome, antigravityCliHome]) {
|
|
126
|
+
await mkdir(join(root, "skills"), { recursive: true });
|
|
127
|
+
await mkdir(join(root, "workflows"), { recursive: true });
|
|
128
|
+
for (const name of skillNames()) {
|
|
129
|
+
await rm(join(root, "skills", name), { recursive: true, force: true });
|
|
130
|
+
await cp(join(pluginRoot, "skills", name), join(root, "skills", name), { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
for (const name of commandNames()) {
|
|
133
|
+
await cp(join(pluginRoot, ".agents", "workflows", `cairn-${name}.md`), join(root, "workflows", `cairn-${name}.md`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function uninstallAntigravityFiles() {
|
|
139
|
+
for (const root of [antigravityHome, antigravityCliHome]) {
|
|
140
|
+
for (const name of skillNames()) {
|
|
141
|
+
await rm(join(root, "skills", name), { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
for (const name of commandNames()) {
|
|
144
|
+
await rm(join(root, "workflows", `cairn-${name}.md`), { force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function updateConfig(config, hookStates) {
|
|
150
|
+
let next = removeCairnConfig(config);
|
|
151
|
+
next = ensureSetting(next, "features", "plugins", "true");
|
|
152
|
+
next = ensureSetting(next, "features", "plugin_hooks", "true");
|
|
153
|
+
next = append(next, `[marketplaces.cairn]\nlast_updated = ${JSON.stringify(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))}\nsource_type = "local"\nsource = ${JSON.stringify(marketplaceRoot)}\n`);
|
|
154
|
+
next = append(next, '[plugins."cairn@cairn"]\nenabled = true\n');
|
|
155
|
+
for (const state of hookStates) next = append(next, `[hooks.state.${JSON.stringify(state.key)}]\ntrusted_hash = ${JSON.stringify(state.trustedHash)}\n`);
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function removeCairnConfig(config) {
|
|
160
|
+
return splitSections(config).filter((section) => {
|
|
161
|
+
if (section.header === null) return true;
|
|
162
|
+
if (section.header === "marketplaces.cairn") return false;
|
|
163
|
+
if (section.header === 'plugins."cairn@cairn"') return false;
|
|
164
|
+
return !(section.header.startsWith("hooks.state.") && section.header.includes("cairn@cairn:"));
|
|
165
|
+
}).map((section) => section.text).join("").replace(/\n{3,}/g, "\n\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function ensureSetting(config, sectionName, key, value) {
|
|
169
|
+
const sections = splitSections(config);
|
|
170
|
+
const index = sections.findIndex((section) => section.header === sectionName);
|
|
171
|
+
if (index === -1) return append(config, `[${sectionName}]\n${key} = ${value}\n`);
|
|
172
|
+
const lines = sections[index].text.trimEnd().split("\n");
|
|
173
|
+
const settingIndex = lines.findIndex((line) => line.trim().startsWith(`${key} =`));
|
|
174
|
+
if (settingIndex === -1) lines.push(`${key} = ${value}`);
|
|
175
|
+
else lines[settingIndex] = `${key} = ${value}`;
|
|
176
|
+
sections[index].text = `${lines.join("\n")}\n`;
|
|
177
|
+
return sections.map((section) => section.text).join("");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function hasSetting(config, sectionName, key, value) {
|
|
181
|
+
const section = splitSections(config).find((candidate) => candidate.header === sectionName);
|
|
182
|
+
return section?.text.split("\n").some((line) => line.trim() === `${key} = ${value}`) ?? false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function splitSections(config) {
|
|
186
|
+
const result = [];
|
|
187
|
+
let current = { header: null, text: "" };
|
|
188
|
+
for (const line of config.split(/(?<=\n)/)) {
|
|
189
|
+
const match = line.trim().match(/^\[([^\]]+)\]$/);
|
|
190
|
+
if (match) {
|
|
191
|
+
if (current.text.length > 0) result.push(current);
|
|
192
|
+
current = { header: match[1], text: line };
|
|
193
|
+
} else current.text += line;
|
|
194
|
+
}
|
|
195
|
+
if (current.text.length > 0) result.push(current);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function trustedHookStates() {
|
|
200
|
+
const hooksPath = join(installedPluginRoot, "hooks", "hooks.json");
|
|
201
|
+
const parsed = await readJson(hooksPath);
|
|
202
|
+
const states = [];
|
|
203
|
+
const labels = { SessionStart: "session_start", UserPromptSubmit: "user_prompt_submit", PostToolUse: "post_tool_use", Stop: "stop", SubagentStop: "subagent_stop" };
|
|
204
|
+
for (const [eventName, groups] of Object.entries(parsed?.hooks ?? {})) {
|
|
205
|
+
const eventLabel = labels[eventName];
|
|
206
|
+
if (!eventLabel || !Array.isArray(groups)) continue;
|
|
207
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
208
|
+
for (const [handlerIndex, handler] of (group.hooks ?? []).entries()) {
|
|
209
|
+
const key = `cairn@cairn:hooks/hooks.json:${eventLabel}:${groupIndex}:${handlerIndex}`;
|
|
210
|
+
states.push({ key, trustedHash: hookHash(eventLabel, group.matcher, handler) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return states;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function hookHash(eventName, matcher, handler) {
|
|
218
|
+
const normalized = { type: "command", command: handler.command, timeout: Math.max(Number(handler.timeout ?? 600), 1), async: false };
|
|
219
|
+
if (typeof handler.statusMessage === "string") normalized.statusMessage = handler.statusMessage;
|
|
220
|
+
const identity = { event_name: eventName, hooks: [normalized] };
|
|
221
|
+
if (typeof matcher === "string") identity.matcher = matcher;
|
|
222
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(canonical(identity))).digest("hex")}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function canonical(value) {
|
|
226
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
227
|
+
if (value === null || typeof value !== "object") return value;
|
|
228
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function backupConfig() {
|
|
232
|
+
if (!(await exists(configPath))) return;
|
|
233
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
234
|
+
await cp(configPath, `${configPath}.cairn-backup-${stamp}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readJson(path) {
|
|
238
|
+
if (!(await exists(path))) return null;
|
|
239
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function readText(path) {
|
|
243
|
+
if (!(await exists(path))) return "";
|
|
244
|
+
return readFile(path, "utf8");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function exists(path) {
|
|
248
|
+
try {
|
|
249
|
+
await stat(path);
|
|
250
|
+
return true;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function append(config, block) {
|
|
257
|
+
return `${config.trimEnd()}${config.trimEnd().length === 0 ? "" : "\n\n"}${block.trimEnd()}\n`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function commandNames() {
|
|
261
|
+
return ["install", "upgrade", "doctor", "uninstall", "memory", "plan", "work", "review", "toolcheck"];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function skillNames() {
|
|
265
|
+
return ["cairn-memory", "cairn-plan", "cairn-work", "cairn-review"];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function help() {
|
|
269
|
+
console.log(t("usage"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function t(key) {
|
|
273
|
+
const messages = {
|
|
274
|
+
en: {
|
|
275
|
+
installComplete: "Cairn install complete",
|
|
276
|
+
upgradeComplete: "Cairn upgrade complete",
|
|
277
|
+
uninstallComplete: "Cairn uninstall complete",
|
|
278
|
+
usage: "Usage: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
279
|
+
},
|
|
280
|
+
ko: {
|
|
281
|
+
installComplete: "Cairn 설치 완료",
|
|
282
|
+
upgradeComplete: "Cairn 업그레이드 완료",
|
|
283
|
+
uninstallComplete: "Cairn 언인스톨 완료",
|
|
284
|
+
usage: "사용법: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
285
|
+
},
|
|
286
|
+
ja: {
|
|
287
|
+
installComplete: "Cairn のインストールが完了しました",
|
|
288
|
+
upgradeComplete: "Cairn のアップグレードが完了しました",
|
|
289
|
+
uninstallComplete: "Cairn のアンインストールが完了しました",
|
|
290
|
+
usage: "使い方: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
291
|
+
},
|
|
292
|
+
zh: {
|
|
293
|
+
installComplete: "Cairn 安装完成",
|
|
294
|
+
upgradeComplete: "Cairn 升级完成",
|
|
295
|
+
uninstallComplete: "Cairn 卸载完成",
|
|
296
|
+
usage: "用法: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
297
|
+
},
|
|
298
|
+
es: {
|
|
299
|
+
installComplete: "Instalación de Cairn completada",
|
|
300
|
+
upgradeComplete: "Actualización de Cairn completada",
|
|
301
|
+
uninstallComplete: "Desinstalación de Cairn completada",
|
|
302
|
+
usage: "Uso: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
303
|
+
},
|
|
304
|
+
fr: {
|
|
305
|
+
installComplete: "Installation de Cairn terminée",
|
|
306
|
+
upgradeComplete: "Mise à niveau de Cairn terminée",
|
|
307
|
+
uninstallComplete: "Désinstallation de Cairn terminée",
|
|
308
|
+
usage: "Utilisation: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
309
|
+
},
|
|
310
|
+
de: {
|
|
311
|
+
installComplete: "Cairn-Installation abgeschlossen",
|
|
312
|
+
upgradeComplete: "Cairn-Upgrade abgeschlossen",
|
|
313
|
+
uninstallComplete: "Cairn-Deinstallation abgeschlossen",
|
|
314
|
+
usage: "Verwendung: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
315
|
+
},
|
|
316
|
+
pt: {
|
|
317
|
+
installComplete: "Instalação do Cairn concluída",
|
|
318
|
+
upgradeComplete: "Atualização do Cairn concluída",
|
|
319
|
+
uninstallComplete: "Desinstalação do Cairn concluída",
|
|
320
|
+
usage: "Uso: cairn install|upgrade|doctor|uninstall|toolcheck",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
return messages[localeFamily()]?.[key] ?? messages.en[key];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function localeFamily() {
|
|
327
|
+
const locale = [process.env.LC_ALL, process.env.LC_MESSAGES, process.env.LANG]
|
|
328
|
+
.find((value) => typeof value === "string" && value.length > 0) ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
329
|
+
const normalized = locale.toLowerCase();
|
|
330
|
+
for (const family of ["ko", "ja", "zh", "es", "fr", "de", "pt"]) {
|
|
331
|
+
if (normalized.startsWith(family)) return family;
|
|
332
|
+
}
|
|
333
|
+
return "en";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isCliEntry() {
|
|
337
|
+
if (!process.argv[1]) return false;
|
|
338
|
+
try {
|
|
339
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
340
|
+
} catch {
|
|
341
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
342
|
+
}
|
|
343
|
+
}
|