@graypark/loophaus 2.1.0 → 2.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/bin/uninstall.mjs +16 -0
- package/lib/paths.mjs +9 -1
- package/package.json +1 -1
- package/platforms/codex-cli/installer.mjs +18 -10
- 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,12 +145,15 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
const totalSteps = 4;
|
|
149
|
+
|
|
150
|
+
// Step 1: Clean up legacy skills from ~/.codex/skills/
|
|
151
|
+
console.log(`[1/${totalSteps}] Cleaning up legacy skills...`);
|
|
152
|
+
const CLEANUP_FROM_CODEX = [...LEGACY_SKILLS, "loop", "loop-stop", "loop-plan", "loop-pulse"];
|
|
153
|
+
for (const name of CLEANUP_FROM_CODEX) {
|
|
149
154
|
const legacyDir = join(skillsDir, name);
|
|
150
155
|
if (await fileExists(legacyDir)) {
|
|
151
|
-
console.log(` > Remove
|
|
156
|
+
console.log(` > Remove from ~/.codex/skills/: ${name}`);
|
|
152
157
|
if (!dryRun) {
|
|
153
158
|
await rm(legacyDir, { recursive: true, force: true });
|
|
154
159
|
}
|
|
@@ -156,7 +161,7 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
// Step 2: Copy files
|
|
159
|
-
console.log(
|
|
164
|
+
console.log(`[2/${totalSteps}] Copying plugin files...`);
|
|
160
165
|
for (const dir of ["hooks", "codex/commands", "lib", "core", "store"]) {
|
|
161
166
|
const src = join(PROJECT_ROOT, dir);
|
|
162
167
|
if (!(await fileExists(src))) continue;
|
|
@@ -171,7 +176,7 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
171
176
|
if (!dryRun) await cp(join(PROJECT_ROOT, "package.json"), join(pluginDir, "package.json"));
|
|
172
177
|
|
|
173
178
|
// Step 3: Hooks
|
|
174
|
-
console.log(
|
|
179
|
+
console.log(`[3/${totalSteps}] Configuring Stop hook...`);
|
|
175
180
|
const stopCmd = `node "${join(pluginDir, "hooks", "stop-hook.mjs")}"`;
|
|
176
181
|
let existing = { hooks: {} };
|
|
177
182
|
if (await fileExists(hooksJsonPath)) {
|
|
@@ -188,10 +193,13 @@ export async function install({ dryRun = false, force = false, local = false } =
|
|
|
188
193
|
await writeFile(hooksJsonPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
189
194
|
}
|
|
190
195
|
|
|
191
|
-
// Step 4: Install
|
|
192
|
-
|
|
196
|
+
// Step 4: Install skills to standard path
|
|
197
|
+
// Global: ~/.agents/skills/ (new Codex CLI standard — avoids duplicates with ~/.codex/skills/)
|
|
198
|
+
// Local: .codex/skills/ (project-scoped)
|
|
199
|
+
const targetSkillsDir = local ? skillsDir : getAgentsSkillsDir();
|
|
200
|
+
console.log(`[4/${totalSteps}] Installing skills to ${local ? ".codex/skills/" : "~/.agents/skills/"}...`);
|
|
193
201
|
for (const [name, skill] of Object.entries(CODEX_SKILLS)) {
|
|
194
|
-
const skillDir = join(
|
|
202
|
+
const skillDir = join(targetSkillsDir, name);
|
|
195
203
|
console.log(` > Install skill: ${name}`);
|
|
196
204
|
if (!dryRun) {
|
|
197
205
|
await mkdir(skillDir, { recursive: true });
|
|
@@ -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
|
}
|