@graypark/loophaus 2.1.0 → 2.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/bin/uninstall.mjs +16 -0
- package/lib/paths.mjs +9 -1
- package/package.json +1 -1
- package/platforms/codex-cli/installer.mjs +28 -6
- package/platforms/kiro-cli/installer.mjs +158 -70
package/bin/uninstall.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getClaudePluginCacheDir,
|
|
13
13
|
getClaudeSettingsPath,
|
|
14
14
|
getClaudeInstalledPluginsPath,
|
|
15
|
+
getAgentsSkillsDir,
|
|
15
16
|
} from "../lib/paths.mjs";
|
|
16
17
|
import { getStatePath } from "../lib/state.mjs";
|
|
17
18
|
|
|
@@ -202,6 +203,21 @@ export async function uninstall({
|
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
// 3b. Remove skills from ~/.agents/skills/ (new Codex CLI standard path)
|
|
207
|
+
if (!local) {
|
|
208
|
+
const agentsSkillsDir = getAgentsSkillsDir();
|
|
209
|
+
const agentsSkillNames = ["loop", "loop-stop", "loop-plan", "loop-pulse"];
|
|
210
|
+
for (const name of agentsSkillNames) {
|
|
211
|
+
const skillDir = join(agentsSkillsDir, name);
|
|
212
|
+
if (await fileExists(skillDir)) {
|
|
213
|
+
log(">", `Remove agents skill: ${skillDir}`);
|
|
214
|
+
if (!dryRun) {
|
|
215
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
// 4. Remove state file
|
|
206
222
|
const statePath = getStatePath();
|
|
207
223
|
if (await fileExists(statePath)) {
|
package/lib/paths.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export function isWindows() {
|
|
|
24
24
|
return process.platform === "win32";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// --- Codex CLI paths ---
|
|
27
|
+
// --- Codex CLI paths (legacy ~/.codex + new ~/.agents) ---
|
|
28
28
|
|
|
29
29
|
export function getCodexHome() {
|
|
30
30
|
if (process.env.CODEX_HOME) {
|
|
@@ -33,6 +33,14 @@ export function getCodexHome() {
|
|
|
33
33
|
return join(homedir(), ".codex");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function getAgentsHome() {
|
|
37
|
+
return join(homedir(), ".agents");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getAgentsSkillsDir() {
|
|
41
|
+
return join(getAgentsHome(), "skills");
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
export function getHooksJsonPath() {
|
|
37
45
|
return join(getCodexHome(), "hooks.json");
|
|
38
46
|
}
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
getHooksJsonPath,
|
|
8
8
|
getPluginInstallDir,
|
|
9
9
|
getSkillsDir,
|
|
10
|
+
getAgentsHome,
|
|
11
|
+
getAgentsSkillsDir,
|
|
10
12
|
} from "../../lib/paths.mjs";
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -17,7 +19,7 @@ async function fileExists(p) {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export async function detect() {
|
|
20
|
-
return fileExists(getCodexHome());
|
|
22
|
+
return (await fileExists(getCodexHome())) || (await fileExists(getAgentsHome()));
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// Legacy ralph-* skill names to clean up
|
|
@@ -143,8 +145,10 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
const totalSteps = local ? 4 : 5;
|
|
149
|
+
|
|
146
150
|
// Step 1: Clean up legacy ralph-* skills
|
|
147
|
-
console.log(
|
|
151
|
+
console.log(`[1/${totalSteps}] Cleaning up legacy skills...`);
|
|
148
152
|
for (const name of LEGACY_SKILLS) {
|
|
149
153
|
const legacyDir = join(skillsDir, name);
|
|
150
154
|
if (await fileExists(legacyDir)) {
|
|
@@ -156,7 +160,7 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
// Step 2: Copy files
|
|
159
|
-
console.log(
|
|
163
|
+
console.log(`[2/${totalSteps}] Copying plugin files...`);
|
|
160
164
|
for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
|
|
161
165
|
const src = join(PROJECT_ROOT, dir);
|
|
162
166
|
if (!(await fileExists(src))) continue;
|
|
@@ -171,7 +175,7 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
171
175
|
if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
|
|
172
176
|
|
|
173
177
|
// Step 3: Hooks
|
|
174
|
-
console.log(
|
|
178
|
+
console.log(`[3/${totalSteps}] Configuring Stop hook...`);
|
|
175
179
|
const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
|
|
176
180
|
let existing = { hooks: {} };
|
|
177
181
|
if (await fileExists(hooksJsonPath)) {
|
|
@@ -188,8 +192,8 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
188
192
|
await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
// Step 4: Install
|
|
192
|
-
console.log(
|
|
195
|
+
// Step 4: Install skills to ~/.codex/skills/
|
|
196
|
+
console.log(`[4/${totalSteps}] Installing skills to ~/.codex/skills/...`);
|
|
193
197
|
for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
|
|
194
198
|
const skillDir = join(skillsDir, name);
|
|
195
199
|
console.log(` > Install skill: ${name}`);
|
|
@@ -203,6 +207,24 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
// Step 5: Mirror skills to ~/.agents/skills/ (new Codex CLI standard path)
|
|
211
|
+
if (!local) {
|
|
212
|
+
const agentsSkillsDir = getAgentsSkillsDir();
|
|
213
|
+
console.log(`[5/${totalSteps}] Installing skills to ~/.agents/skills/...`);
|
|
214
|
+
for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
|
|
215
|
+
const skillDir = join(agentsSkillsDir, name);
|
|
216
|
+
console.log(` > Install skill: ${name}`);
|
|
217
|
+
if (!dryRun) {
|
|
218
|
+
await mkdir(skillDir, { recursive: true });
|
|
219
|
+
await writeFile(
|
|
220
|
+
join(skillDir, "SKILL.md"),
|
|
221
|
+
skill.content.replaceAll("${RALPH_CODEX_ROOT}", pluginDir).replaceAll("${LOOPHAUS_ROOT}", pluginDir),
|
|
222
|
+
"utf-8",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
console.log("");
|
|
207
229
|
if (dryRun) {
|
|
208
230
|
console.log(" \u2714 Dry run complete. No files were modified.");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// platforms/kiro-cli/installer.mjs
|
|
2
|
-
import { readFile, writeFile, mkdir,
|
|
2
|
+
import { readFile, writeFile, mkdir, access, rm } from "node:fs/promises";
|
|
3
3
|
import { join, resolve, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -15,27 +15,97 @@ async function fileExists(p) {
|
|
|
15
15
|
try { await access(p); return true; } catch { return false; }
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
// Kiro CLI skill definitions (description-based auto-matching)
|
|
19
|
+
const KIRO_SKILLS = {
|
|
20
|
+
loop: {
|
|
21
|
+
content: `---
|
|
22
|
+
name: loop
|
|
23
|
+
description: "Start iterative dev loop — use when user says 'start loop', 'loop this', 'iterate on task', 'run loop'"
|
|
24
|
+
argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Start Iterative Dev Loop
|
|
28
|
+
|
|
29
|
+
Parse the user's arguments:
|
|
30
|
+
1. Extract \`--max-iterations N\` (default: 20)
|
|
31
|
+
2. Extract \`--completion-promise TEXT\` (default: "TADA")
|
|
32
|
+
3. Everything else is the prompt
|
|
33
|
+
|
|
34
|
+
Create \`.loophaus/state.json\`:
|
|
35
|
+
\`\`\`json
|
|
36
|
+
{
|
|
37
|
+
"active": true,
|
|
38
|
+
"prompt": "<user's prompt>",
|
|
39
|
+
"completionPromise": "<promise text>",
|
|
40
|
+
"maxIterations": 20,
|
|
41
|
+
"currentIteration": 0,
|
|
42
|
+
"sessionId": ""
|
|
38
43
|
}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
Then begin working on the task. The stop hook intercepts exit and feeds the SAME PROMPT back.
|
|
47
|
+
|
|
48
|
+
CRITICAL: If a completion promise is set, ONLY output \`<promise>TEXT</promise>\` when genuinely complete.
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
"loop-stop": {
|
|
52
|
+
content: `---
|
|
53
|
+
name: loop-stop
|
|
54
|
+
description: "Stop active loop — use when user says 'stop loop', 'cancel loop', 'halt', 'stop iterating'"
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Stop Active Loop
|
|
58
|
+
|
|
59
|
+
1. Check if \`.loophaus/state.json\` exists
|
|
60
|
+
2. If not found: "No active loop."
|
|
61
|
+
3. If found: read currentIteration, set active: false, report "Stopped loop at iteration N."
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
"loop-plan": {
|
|
65
|
+
content: `---
|
|
66
|
+
name: loop-plan
|
|
67
|
+
description: "Plan and start loop via interactive interview — use when user says 'plan loop', 'interview', 'create PRD', 'plan task'"
|
|
68
|
+
argument-hint: "TASK_DESCRIPTION"
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# Interactive Planning & Loop
|
|
72
|
+
|
|
73
|
+
## Phase 1: Discovery Interview
|
|
74
|
+
Ask 3-5 focused questions about the task.
|
|
75
|
+
|
|
76
|
+
## Phase 2: PRD Generation
|
|
77
|
+
Generate prd.json with right-sized user stories.
|
|
78
|
+
|
|
79
|
+
## Phase 3: Loop Activation
|
|
80
|
+
Create .loophaus/state.json and start working on US-001 immediately.
|
|
81
|
+
|
|
82
|
+
Use \`<promise>TASK COMPLETE</promise>\` ONLY when ALL stories pass.
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
"loop-pulse": {
|
|
86
|
+
content: `---
|
|
87
|
+
name: loop-pulse
|
|
88
|
+
description: "Check loop status — use when user says 'loop status', 'check progress', 'how is the loop', 'pulse'"
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# Check Loop Status
|
|
92
|
+
|
|
93
|
+
1. Read .loophaus/state.json
|
|
94
|
+
2. If active: show iteration, promise, progress
|
|
95
|
+
3. If prd.json exists: show story progress (done/total)
|
|
96
|
+
`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Legacy files to clean up
|
|
101
|
+
const LEGACY_STEERING = [
|
|
102
|
+
"steering/ralph-loop.md",
|
|
103
|
+
"steering/cancel-ralph.md",
|
|
104
|
+
"steering/loop.md",
|
|
105
|
+
"steering/loop-plan.md",
|
|
106
|
+
"steering/loop-stop.md",
|
|
107
|
+
"steering/loop-pulse.md",
|
|
108
|
+
];
|
|
39
109
|
|
|
40
110
|
export async function detect() {
|
|
41
111
|
return fileExists(getKiroHome());
|
|
@@ -44,7 +114,7 @@ export async function detect() {
|
|
|
44
114
|
export async function install({ dryRun = false, force = false } = {}) {
|
|
45
115
|
const kiroHome = getKiroHome();
|
|
46
116
|
const agentsDir = join(kiroHome, "agents");
|
|
47
|
-
const
|
|
117
|
+
const skillsDir = join(kiroHome, "skills");
|
|
48
118
|
|
|
49
119
|
console.log("");
|
|
50
120
|
console.log(`loophaus installer — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
|
|
@@ -52,7 +122,7 @@ export async function install({ dryRun = false, force = false } = {}) {
|
|
|
52
122
|
console.log("");
|
|
53
123
|
|
|
54
124
|
// Step 1: Create agent config with stop hook
|
|
55
|
-
console.log("[1/
|
|
125
|
+
console.log("[1/4] Configuring agent with stop hook...");
|
|
56
126
|
const agentConfig = {
|
|
57
127
|
name: "loophaus",
|
|
58
128
|
description: "loophaus — iterative dev loop agent",
|
|
@@ -77,39 +147,35 @@ export async function install({ dryRun = false, force = false } = {}) {
|
|
|
77
147
|
await writeFile(agentPath, JSON.stringify(agentConfig, null, 2), "utf-8");
|
|
78
148
|
}
|
|
79
149
|
|
|
80
|
-
// Step 2: Clean up legacy
|
|
81
|
-
console.log("[2/
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
150
|
+
// Step 2: Clean up legacy steering files
|
|
151
|
+
console.log("[2/4] Cleaning up legacy steering files...");
|
|
152
|
+
for (const relPath of LEGACY_STEERING) {
|
|
153
|
+
const fullPath = join(kiroHome, relPath);
|
|
154
|
+
if (await fileExists(fullPath)) {
|
|
155
|
+
console.log(` > Remove legacy: ${relPath}`);
|
|
156
|
+
if (!dryRun) await rm(fullPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Step 3: Clean up legacy skill directories (in case of partial migration)
|
|
161
|
+
console.log("[3/4] Cleaning up legacy skill directories...");
|
|
162
|
+
const legacySkillNames = ["ralph-loop", "cancel-ralph"];
|
|
163
|
+
for (const name of legacySkillNames) {
|
|
164
|
+
const legacySkillDir = join(skillsDir, name);
|
|
165
|
+
if (await fileExists(legacySkillDir)) {
|
|
166
|
+
console.log(` > Remove legacy skill: ${name}`);
|
|
167
|
+
if (!dryRun) await rm(legacySkillDir, { recursive: true, force: true });
|
|
91
168
|
}
|
|
92
169
|
}
|
|
93
170
|
|
|
94
|
-
// Step
|
|
95
|
-
console.log("[
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (const { src, dest } of commands) {
|
|
103
|
-
const srcPath = join(PROJECT_ROOT, src);
|
|
104
|
-
if (await fileExists(srcPath)) {
|
|
105
|
-
const destPath = join(steeringDir, dest);
|
|
106
|
-
console.log(` > Copy ${src} -> ${destPath} (Kiro frontmatter)`);
|
|
107
|
-
if (!dryRun) {
|
|
108
|
-
await mkdir(steeringDir, { recursive: true });
|
|
109
|
-
const content = await readFile(srcPath, "utf-8");
|
|
110
|
-
const kiroContent = convertToKiroFrontmatter(content);
|
|
111
|
-
await writeFile(destPath, kiroContent, "utf-8");
|
|
112
|
-
}
|
|
171
|
+
// Step 4: Install skills (description-based auto-matching)
|
|
172
|
+
console.log("[4/4] Installing skills...");
|
|
173
|
+
for (const [name, skill] of Object.entries(KIRO_SKILLS)) {
|
|
174
|
+
const skillDir = join(skillsDir, name);
|
|
175
|
+
console.log(` > Install skill: ${name}`);
|
|
176
|
+
if (!dryRun) {
|
|
177
|
+
await mkdir(skillDir, { recursive: true });
|
|
178
|
+
await writeFile(join(skillDir, "SKILL.md"), skill.content, "utf-8");
|
|
113
179
|
}
|
|
114
180
|
}
|
|
115
181
|
|
|
@@ -118,7 +184,7 @@ export async function install({ dryRun = false, force = false } = {}) {
|
|
|
118
184
|
console.log(" \u2714 Dry run complete. No files were modified.");
|
|
119
185
|
} else {
|
|
120
186
|
console.log(" \u2714 loophaus installed for Kiro CLI!");
|
|
121
|
-
console.log("
|
|
187
|
+
console.log(" Skills auto-match via description keywords.");
|
|
122
188
|
console.log(" To uninstall: npx @graypark/loophaus uninstall --kiro");
|
|
123
189
|
}
|
|
124
190
|
console.log("");
|
|
@@ -128,29 +194,51 @@ export async function install({ dryRun = false, force = false } = {}) {
|
|
|
128
194
|
export async function uninstall({ dryRun = false } = {}) {
|
|
129
195
|
const kiroHome = getKiroHome();
|
|
130
196
|
|
|
131
|
-
const targets = [
|
|
132
|
-
join(kiroHome, "agents", "loophaus.json"),
|
|
133
|
-
join(kiroHome, "steering", "loop.md"),
|
|
134
|
-
join(kiroHome, "steering", "loop-plan.md"),
|
|
135
|
-
join(kiroHome, "steering", "loop-stop.md"),
|
|
136
|
-
join(kiroHome, "steering", "loop-pulse.md"),
|
|
137
|
-
// Legacy steering files
|
|
138
|
-
join(kiroHome, "steering", "ralph-loop.md"),
|
|
139
|
-
join(kiroHome, "steering", "cancel-ralph.md"),
|
|
140
|
-
];
|
|
141
|
-
|
|
142
197
|
console.log("");
|
|
143
198
|
console.log(`loophaus uninstaller — Kiro CLI${dryRun ? " (DRY RUN)" : ""}`);
|
|
144
199
|
console.log("");
|
|
145
200
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
// Remove agent config
|
|
202
|
+
const agentPath = join(kiroHome, "agents", "loophaus.json");
|
|
203
|
+
if (await fileExists(agentPath)) {
|
|
204
|
+
console.log(` > Remove ${agentPath}`);
|
|
205
|
+
if (!dryRun) await rm(agentPath);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Remove skill directories
|
|
209
|
+
const skillNames = ["loop", "loop-stop", "loop-plan", "loop-pulse"];
|
|
210
|
+
for (const name of skillNames) {
|
|
211
|
+
const skillDir = join(kiroHome, "skills", name);
|
|
212
|
+
if (await fileExists(skillDir)) {
|
|
213
|
+
console.log(` > Remove skill: ${skillDir}`);
|
|
214
|
+
if (!dryRun) await rm(skillDir, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove legacy steering files
|
|
219
|
+
for (const relPath of LEGACY_STEERING) {
|
|
220
|
+
const fullPath = join(kiroHome, relPath);
|
|
221
|
+
if (await fileExists(fullPath)) {
|
|
222
|
+
console.log(` > Remove legacy: ${fullPath}`);
|
|
223
|
+
if (!dryRun) await rm(fullPath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove legacy skill directories
|
|
228
|
+
const legacySkillNames = ["ralph-loop", "cancel-ralph"];
|
|
229
|
+
for (const name of legacySkillNames) {
|
|
230
|
+
const legacySkillDir = join(kiroHome, "skills", name);
|
|
231
|
+
if (await fileExists(legacySkillDir)) {
|
|
232
|
+
console.log(` > Remove legacy skill: ${legacySkillDir}`);
|
|
233
|
+
if (!dryRun) await rm(legacySkillDir, { recursive: true, force: true });
|
|
150
234
|
}
|
|
151
235
|
}
|
|
152
236
|
|
|
153
237
|
console.log("");
|
|
154
|
-
|
|
238
|
+
if (dryRun) {
|
|
239
|
+
console.log(" \u2714 Dry run complete. No files were modified.");
|
|
240
|
+
} else {
|
|
241
|
+
console.log(" \u2714 loophaus removed from Kiro CLI.");
|
|
242
|
+
}
|
|
155
243
|
console.log("");
|
|
156
244
|
}
|