kracked-core 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/bin/kracked-core.mjs +29 -2
- package/package.json +1 -1
- package/src/detect.mjs +34 -12
- package/src/prompt.mjs +110 -6
- package/src/scaffold.mjs +92 -19
- package/src/uninstall.mjs +214 -0
- package/src/update.mjs +150 -0
- package/src/wizard.mjs +77 -4
- package/templates/loaders/AGENTS.md +1 -0
- package/templates/loaders/CLAUDE.md +1 -0
- package/templates/loaders/antigravity-rules.md +1 -0
- package/templates/project/sdd/README.md +46 -0
- package/templates/project/sdd/architecture/.gitkeep +0 -0
- package/templates/project/sdd/architecture/_TEMPLATE.md +51 -0
- package/templates/project/sdd/architecture/decisions/_TEMPLATE.md +40 -0
- package/templates/project/sdd/epics/.gitkeep +0 -0
- package/templates/project/sdd/epics/_TEMPLATE.md +34 -0
- package/templates/project/sdd/specs/_TEMPLATE.md +44 -0
- package/templates/project/sdd/stories/.gitkeep +0 -0
- package/templates/project/sdd/stories/_TEMPLATE.md +33 -0
- package/templates/skills/kracked-boot/SKILL.md +1 -0
- package/templates/skills/kracked-sdd/SKILL.md +36 -1
package/README.md
CHANGED
|
@@ -49,7 +49,12 @@ npx kracked-core init
|
|
|
49
49
|
|
|
50
50
|
The wizard asks a few questions — name your agent, is this a new or existing project, which editor you use — and writes the files. Hold Enter through it and you'll get a sane setup.
|
|
51
51
|
|
|
52
|
-
**
|
|
52
|
+
**Full support** (memory + all five commands):
|
|
53
|
+
**Antigravity** · **Claude Code** · **Kilo Code** (VS Code) · **Roo Code** (VS Code)
|
|
54
|
+
|
|
55
|
+
**Memory only:** Cursor, Windsurf, and anything else that reads `AGENTS.md` — your agent loads its
|
|
56
|
+
memory, but the `/kracked-*` commands aren't installed, because those editors use a different
|
|
57
|
+
skill format. You can still run the flow by asking in plain language.
|
|
53
58
|
|
|
54
59
|
## Use
|
|
55
60
|
|
|
@@ -82,7 +87,13 @@ your-project/.kracked/ PROJECT — this codebase only
|
|
|
82
87
|
project.md stack, conventions, how to run it
|
|
83
88
|
session.md where we are right now
|
|
84
89
|
decisions.md why things are the way they are
|
|
85
|
-
sdd/
|
|
90
|
+
sdd/
|
|
91
|
+
tracker.md story status + evidence
|
|
92
|
+
specs/ what to build and why
|
|
93
|
+
epics/ groups of related stories
|
|
94
|
+
stories/ one shippable slice each
|
|
95
|
+
architecture/ how it's built (large work only)
|
|
96
|
+
decisions/ ADRs, numbered, never renumbered
|
|
86
97
|
```
|
|
87
98
|
|
|
88
99
|
### The boundary rule
|
|
@@ -123,6 +134,30 @@ Then it tracks the work:
|
|
|
123
134
|
|
|
124
135
|
**A story cannot move to `done` without evidence.** Not "the tests pass" — what did you actually verify, and what did you *not*? Green tests are not a working feature. This one rule catches more bugs than any other part of the system.
|
|
125
136
|
|
|
137
|
+
## Updating
|
|
138
|
+
|
|
139
|
+
When a new version ships:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx kracked-core@latest update
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Refreshes the skills and loaders. **Your memory is never touched** — identity, preferences,
|
|
146
|
+
lessons, and everything under `.kracked/` stay exactly as they are. It keeps your agent's name too.
|
|
147
|
+
|
|
148
|
+
Restart your editor afterwards so it picks up the new skills.
|
|
149
|
+
|
|
150
|
+
## Uninstalling
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx kracked-core uninstall
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Shows what it will remove and asks before deleting — project files and global memory are separate
|
|
157
|
+
questions. It never touches a `CLAUDE.md` or `AGENTS.md` another tool wrote.
|
|
158
|
+
|
|
159
|
+
Full guide, including manual removal and how to keep your docs: [docs/UNINSTALL.md](https://github.com/Krackeddevs-Org/kracked-core/blob/main/docs/UNINSTALL.md)
|
|
160
|
+
|
|
126
161
|
## FAQ
|
|
127
162
|
|
|
128
163
|
**Does this send my code anywhere?**
|
package/bin/kracked-core.mjs
CHANGED
|
@@ -1,24 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { runInit } from '../src/wizard.mjs';
|
|
4
|
+
import { runUninstall } from '../src/uninstall.mjs';
|
|
5
|
+
import { runUpdate } from '../src/update.mjs';
|
|
6
|
+
import { beginTypeAhead } from '../src/prompt.mjs';
|
|
4
7
|
|
|
5
8
|
const USAGE = `kracked-core — memory & workflow installer for AI coding agents
|
|
6
9
|
|
|
7
10
|
Usage:
|
|
8
|
-
npx kracked-core init
|
|
11
|
+
npx kracked-core init Set up global + project memory
|
|
12
|
+
npx kracked-core update Refresh skills + loaders, keeping your memory
|
|
13
|
+
npx kracked-core uninstall Remove kracked-core files (asks before deleting)
|
|
9
14
|
|
|
10
15
|
Options:
|
|
11
|
-
-h, --help
|
|
16
|
+
-h, --help Show this help
|
|
17
|
+
-v, --version Show the installed version
|
|
12
18
|
`;
|
|
13
19
|
|
|
14
20
|
async function main() {
|
|
15
21
|
const [, , subcommand] = process.argv;
|
|
16
22
|
|
|
23
|
+
// Capture keystrokes from the very first millisecond — a student holding
|
|
24
|
+
// Enter starts before Node has finished booting.
|
|
25
|
+
if (['init', 'update', 'uninstall'].includes(subcommand)) beginTypeAhead();
|
|
26
|
+
|
|
17
27
|
if (subcommand === 'init') {
|
|
18
28
|
await runInit();
|
|
19
29
|
return;
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
if (subcommand === 'update') {
|
|
33
|
+
await runUpdate();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (subcommand === 'uninstall') {
|
|
38
|
+
await runUninstall();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (subcommand === '--version' || subcommand === '-v') {
|
|
43
|
+
const { createRequire } = await import('node:module');
|
|
44
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
45
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
if (subcommand === '--help' || subcommand === '-h' || !subcommand) {
|
|
23
50
|
process.stdout.write(USAGE);
|
|
24
51
|
process.exit(subcommand ? 0 : 1);
|
package/package.json
CHANGED
package/src/detect.mjs
CHANGED
|
@@ -5,6 +5,30 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Scrub a value read from an untrusted repo before it can reach a template.
|
|
10
|
+
*
|
|
11
|
+
* Everything scanned from a project (package.json name, script names) gets
|
|
12
|
+
* interpolated into AGENTS.md and ~/.kracked/projects.md — files an AI agent
|
|
13
|
+
* READS AS INSTRUCTIONS on every boot. A crafted `name` containing newlines and
|
|
14
|
+
* a markdown heading can therefore inject text that reads as package-authored
|
|
15
|
+
* instruction, and via the global registry it persists into every OTHER project
|
|
16
|
+
* the user opens. Sanitize here, at the boundary, so no caller can forget.
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeScanned(value, maxLength = 64) {
|
|
19
|
+
if (typeof value !== 'string') return '';
|
|
20
|
+
return value
|
|
21
|
+
.replace(/[\r\n\t]+/g, ' ') // no line breaks — kills injected blocks
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
.replace(/[\u0000-\u001f\u007f]/g, '') // strip remaining control chars
|
|
24
|
+
.replace(/[|`<>]/g, '') // markdown table + code + html structure
|
|
25
|
+
.replace(/^[\s#>*\-=+]+/, '') // no leading heading/quote/list markers
|
|
26
|
+
.replace(/\s+/g, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
.slice(0, maxLength)
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
/** Does ~/.kracked/ already exist? */
|
|
9
33
|
export function globalMemoryExists() {
|
|
10
34
|
const dir = path.join(os.homedir(), '.kracked');
|
|
@@ -99,9 +123,17 @@ export function scanExistingProject(targetDir) {
|
|
|
99
123
|
if (fs.existsSync(pkgPath)) {
|
|
100
124
|
try {
|
|
101
125
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
102
|
-
|
|
126
|
+
|
|
127
|
+
// Everything below comes from an untrusted repo and ends up inside files
|
|
128
|
+
// the agent reads as instructions — sanitize before it goes anywhere.
|
|
129
|
+
const cleanName = sanitizeScanned(pkg.name);
|
|
130
|
+
if (cleanName) summary.name = cleanName;
|
|
131
|
+
|
|
103
132
|
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
104
|
-
|
|
133
|
+
// Keep only plausible script names; a hostile key is dropped, not rendered.
|
|
134
|
+
for (const key of Object.keys(pkg.scripts)) {
|
|
135
|
+
if (/^[a-zA-Z0-9:_-]{1,40}$/.test(key)) summary.scripts[key] = true;
|
|
136
|
+
}
|
|
105
137
|
}
|
|
106
138
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
107
139
|
const frameworkHints = [
|
|
@@ -127,13 +159,3 @@ export function stackSummaryLine(summary) {
|
|
|
127
159
|
return parts.length ? parts.join(' ') : 'unknown — not auto-detected';
|
|
128
160
|
}
|
|
129
161
|
|
|
130
|
-
/** Which harnesses already have a footprint in this project dir? */
|
|
131
|
-
export function detectHarnesses(targetDir) {
|
|
132
|
-
return {
|
|
133
|
-
claudeCode: fs.existsSync(path.join(targetDir, '.claude')) ||
|
|
134
|
-
fs.existsSync(path.join(targetDir, 'CLAUDE.md')),
|
|
135
|
-
antigravity: fs.existsSync(path.join(targetDir, '.agents')) ||
|
|
136
|
-
fs.existsSync(path.join(targetDir, 'AGENTS.md')),
|
|
137
|
-
cursor: fs.existsSync(path.join(targetDir, '.cursor')),
|
|
138
|
-
};
|
|
139
|
-
}
|
package/src/prompt.mjs
CHANGED
|
@@ -27,32 +27,112 @@ function setCursor(visible) {
|
|
|
27
27
|
stdout.write(visible ? `${ESC}[?25h` : `${ESC}[?25l`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// A throw anywhere outside the keypress handler would otherwise exit with the
|
|
31
|
+
// terminal still in raw mode and the cursor hidden — leaving the user's shell
|
|
32
|
+
// with invisible, unechoed input. One idempotent restore covers every exit path,
|
|
33
|
+
// including process.exit().
|
|
34
|
+
let restoreRegistered = false;
|
|
35
|
+
function registerTerminalRestore() {
|
|
36
|
+
if (restoreRegistered) return;
|
|
37
|
+
restoreRegistered = true;
|
|
38
|
+
process.on('exit', () => {
|
|
39
|
+
try {
|
|
40
|
+
if (stdin.isTTY && stdin.isRaw) stdin.setRawMode(false);
|
|
41
|
+
stdout.write(`${ESC}[?25h`);
|
|
42
|
+
} catch {
|
|
43
|
+
// Nothing useful to do while the process is already exiting.
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
/** Move up n lines and clear from there down — used to redraw the menu in place. */
|
|
31
49
|
function clearLines(n) {
|
|
32
50
|
if (n > 0) stdout.write(`${ESC}[${n}A`);
|
|
33
51
|
stdout.write(`${ESC}[0J`);
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Split one stdin chunk into individual keys.
|
|
56
|
+
*
|
|
57
|
+
* stdin delivers DATA, not keystrokes: holding Enter (key auto-repeat), typing
|
|
58
|
+
* fast, pasting, or running over SSH/tmux routinely packs several keys into one
|
|
59
|
+
* chunk. Comparing a whole chunk against '\r' then matches nothing, so the key
|
|
60
|
+
* is silently dropped and the prompt hangs — the worst kind of failure, because
|
|
61
|
+
* there is no error to see. Escape sequences (arrows) must stay intact as one
|
|
62
|
+
* key; everything else is per-character.
|
|
63
|
+
*/
|
|
64
|
+
function splitKeys(chunk) {
|
|
65
|
+
const keys = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
|
|
68
|
+
while (i < chunk.length) {
|
|
69
|
+
if (chunk[i] === ESC) {
|
|
70
|
+
// CSI sequence: ESC [ ... final-byte (@ through ~). Arrows are ESC[A..D.
|
|
71
|
+
const match = /^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(chunk.slice(i));
|
|
72
|
+
if (match) {
|
|
73
|
+
keys.push(match[0]);
|
|
74
|
+
i += match[0].length;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// ESC alone, or an unrecognised sequence — take just the ESC.
|
|
78
|
+
keys.push(chunk[i]);
|
|
79
|
+
i += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
keys.push(chunk[i]);
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return keys;
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
/**
|
|
37
90
|
* Read single keypresses until `onKey` signals completion.
|
|
38
91
|
* onKey(key) returns { done: true, value } to finish, or falsy to keep listening.
|
|
39
92
|
*/
|
|
93
|
+
// Keys that arrive between prompts — while one question has resolved and the
|
|
94
|
+
// next hasn't attached its listener yet — would otherwise be dropped on the
|
|
95
|
+
// floor. Holding Enter produces exactly that. Buffer them and replay into the
|
|
96
|
+
// next prompt so no keystroke is ever silently lost.
|
|
97
|
+
const pendingKeys = [];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start capturing keystrokes immediately, before the first prompt renders.
|
|
101
|
+
* Node takes a moment to boot and draw; anything typed in that window is
|
|
102
|
+
* otherwise lost. Call once at CLI start.
|
|
103
|
+
*/
|
|
104
|
+
export function beginTypeAhead() {
|
|
105
|
+
if (!stdin.isTTY) return;
|
|
106
|
+
registerTerminalRestore();
|
|
107
|
+
stdin.setRawMode(true);
|
|
108
|
+
stdin.resume();
|
|
109
|
+
stdin.setEncoding('utf8');
|
|
110
|
+
stdin.on('data', bufferEarlyKeys);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function bufferEarlyKeys(chunk) {
|
|
114
|
+
pendingKeys.push(...splitKeys(chunk));
|
|
115
|
+
}
|
|
116
|
+
|
|
40
117
|
function readKeys(onKey) {
|
|
41
118
|
return new Promise((resolve, reject) => {
|
|
119
|
+
registerTerminalRestore();
|
|
42
120
|
const wasRaw = stdin.isRaw;
|
|
43
121
|
stdin.setRawMode(true);
|
|
44
122
|
stdin.resume();
|
|
45
123
|
stdin.setEncoding('utf8');
|
|
124
|
+
stdin.removeListener('data', bufferEarlyKeys);
|
|
46
125
|
|
|
47
126
|
const cleanup = () => {
|
|
48
127
|
stdin.setRawMode(wasRaw ?? false);
|
|
49
|
-
stdin.pause();
|
|
50
128
|
stdin.removeListener('data', handler);
|
|
129
|
+
// Resume early-capture so keys typed between prompts aren't lost.
|
|
130
|
+
stdin.on('data', bufferEarlyKeys);
|
|
51
131
|
setCursor(true);
|
|
52
132
|
};
|
|
53
133
|
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
/** Feed one key to onKey. Returns true when the prompt is finished. */
|
|
135
|
+
const dispatch = (key) => {
|
|
56
136
|
if (key === KEY.ctrlC || key === KEY.ctrlD) {
|
|
57
137
|
cleanup();
|
|
58
138
|
stdout.write('\n\nCancelled — nothing was written.\n');
|
|
@@ -65,12 +145,32 @@ function readKeys(onKey) {
|
|
|
65
145
|
} catch (err) {
|
|
66
146
|
cleanup();
|
|
67
147
|
reject(err);
|
|
68
|
-
return;
|
|
148
|
+
return true;
|
|
69
149
|
}
|
|
70
150
|
|
|
71
151
|
if (result && result.done) {
|
|
72
152
|
cleanup();
|
|
73
153
|
resolve(result.value);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Replay anything typed ahead of this prompt before listening for more.
|
|
160
|
+
while (pendingKeys.length) {
|
|
161
|
+
if (dispatch(pendingKeys.shift())) return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handler(chunk) {
|
|
165
|
+
// One chunk can carry several keys. Dispatch them one at a time; once a
|
|
166
|
+
// key completes this prompt, park the rest for the next one rather than
|
|
167
|
+
// discarding them.
|
|
168
|
+
const keys = splitKeys(chunk);
|
|
169
|
+
for (let i = 0; i < keys.length; i++) {
|
|
170
|
+
if (dispatch(keys[i])) {
|
|
171
|
+
pendingKeys.push(...keys.slice(i + 1));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
74
174
|
}
|
|
75
175
|
}
|
|
76
176
|
|
|
@@ -210,8 +310,12 @@ export async function input(question, defaultValue = '') {
|
|
|
210
310
|
render();
|
|
211
311
|
return null;
|
|
212
312
|
}
|
|
213
|
-
// Ignore escape sequences (arrows etc.) and
|
|
214
|
-
|
|
313
|
+
// Ignore escape sequences (arrows etc.) and any control character. Compare
|
|
314
|
+
// the code point, not the string — `key < ' '` on a multi-char value tests
|
|
315
|
+
// lexicographic order, not "is this a control char".
|
|
316
|
+
if (key.startsWith(ESC)) return null;
|
|
317
|
+
const code = key.codePointAt(0);
|
|
318
|
+
if (code < 0x20 || code === 0x7f) return null;
|
|
215
319
|
|
|
216
320
|
buffer += key;
|
|
217
321
|
render();
|
package/src/scaffold.mjs
CHANGED
|
@@ -9,7 +9,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const TEMPLATES_ROOT = path.join(__dirname, '..', 'templates');
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Written into every loader kracked-core generates. `uninstall` requires this
|
|
13
|
+
// exact string before deleting a shared-name file (AGENTS.md / CLAUDE.md), so
|
|
14
|
+
// another tool's loader is never removed. Never change it without a migration.
|
|
15
|
+
export const OWNED_MARKER = '<!-- kracked-core:owned -->';
|
|
16
|
+
|
|
17
|
+
export const SKILL_NAMES = [
|
|
13
18
|
'kracked-boot',
|
|
14
19
|
'kracked-sdd',
|
|
15
20
|
'kracked-wrap',
|
|
@@ -95,8 +100,10 @@ export async function writeGlobalMemory({ tokens, ask, report }) {
|
|
|
95
100
|
export function registerProject({ tokens }) {
|
|
96
101
|
const globalDir = path.join(os.homedir(), '.kracked');
|
|
97
102
|
const projectsPath = path.join(globalDir, 'projects.md');
|
|
103
|
+
// Escape pipes — a path or name containing `|` would add phantom columns.
|
|
104
|
+
const cell = (v) => String(v ?? '').replace(/\|/g, '\\|');
|
|
98
105
|
const stack = tokens.STACK && tokens.STACK.trim() ? tokens.STACK : '—';
|
|
99
|
-
const row = `| ${tokens.PROJECT_NAME} | ${tokens.PROJECT_PATH} | ${stack} | active |`;
|
|
106
|
+
const row = `| ${cell(tokens.PROJECT_NAME)} | ${cell(tokens.PROJECT_PATH)} | ${cell(stack)} | active |`;
|
|
100
107
|
|
|
101
108
|
fs.mkdirSync(globalDir, { recursive: true });
|
|
102
109
|
|
|
@@ -148,34 +155,62 @@ export async function writeProjectMemory({ projectDir, tokens, ask, report }) {
|
|
|
148
155
|
await writeFileWithConflictCheck(destPath, content, ask, report);
|
|
149
156
|
}
|
|
150
157
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
158
|
+
// SDD docs: tracker + the artifact folders, each with a template that
|
|
159
|
+
// explains what belongs there. An empty folder teaches nothing.
|
|
160
|
+
const sddDocs = [
|
|
161
|
+
'sdd/tracker.md',
|
|
162
|
+
'sdd/README.md',
|
|
163
|
+
'sdd/specs/_TEMPLATE.md',
|
|
164
|
+
'sdd/epics/_TEMPLATE.md',
|
|
165
|
+
'sdd/stories/_TEMPLATE.md',
|
|
166
|
+
'sdd/architecture/_TEMPLATE.md',
|
|
167
|
+
'sdd/architecture/decisions/_TEMPLATE.md',
|
|
168
|
+
];
|
|
155
169
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
report({ path: gitkeepDest, action: 'written' });
|
|
170
|
+
for (const rel of sddDocs) {
|
|
171
|
+
const destPath = path.join(krackedDir, rel);
|
|
172
|
+
const content = applyTokens(readTemplate(`project/${rel}`), tokens);
|
|
173
|
+
await writeFileWithConflictCheck(destPath, content, ask, report);
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
/** Write AGENTS.md, CLAUDE.md (shim), and .agents/rules/kracked.md. */
|
|
165
|
-
export async function writeLoaders({ projectDir, tokens, ask, report }) {
|
|
178
|
+
export async function writeLoaders({ projectDir, tokens, ask, report, editors }) {
|
|
166
179
|
const agentsContent = applyTokens(readTemplate('loaders/AGENTS.md'), tokens);
|
|
167
180
|
await writeFileWithConflictCheck(path.join(projectDir, 'AGENTS.md'), agentsContent, ask, report);
|
|
168
181
|
|
|
169
182
|
const claudeContent = applyTokens(readTemplate('loaders/CLAUDE.md'), tokens);
|
|
170
183
|
await writeFileWithConflictCheck(path.join(projectDir, 'CLAUDE.md'), claudeContent, ask, report);
|
|
171
184
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
// Roo reads .roo/rules/ only — it does NOT read .agents/rules/, so without
|
|
186
|
+
// this mirror the pointer is invisible to Roo.
|
|
187
|
+
if (editors && editors.includes('roo')) {
|
|
188
|
+
const rooRules = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
|
|
189
|
+
await writeFileWithConflictCheck(
|
|
190
|
+
path.join(projectDir, '.roo', 'rules', 'kracked.md'),
|
|
191
|
+
rooRules,
|
|
192
|
+
ask,
|
|
193
|
+
report
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Kilo only loads a rules file if it's declared in kilo.jsonc's `instructions`
|
|
198
|
+
// array. Merge into an existing config rather than clobbering the user's.
|
|
199
|
+
if (editors && editors.includes('kilo')) {
|
|
200
|
+
await writeKiloConfig({ projectDir, report });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Only write the Antigravity pointer when Antigravity is actually in use —
|
|
204
|
+
// `update` was creating .agents/ in Claude-only projects.
|
|
205
|
+
if (!editors || editors.includes('antigravity')) {
|
|
206
|
+
const krackedRulesContent = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
|
|
207
|
+
await writeFileWithConflictCheck(
|
|
208
|
+
path.join(projectDir, '.agents', 'rules', 'kracked.md'),
|
|
209
|
+
krackedRulesContent,
|
|
210
|
+
ask,
|
|
211
|
+
report
|
|
212
|
+
);
|
|
213
|
+
}
|
|
179
214
|
}
|
|
180
215
|
|
|
181
216
|
/**
|
|
@@ -196,3 +231,41 @@ export async function writeSkills({ projectDir, tokens, editors, ask, report })
|
|
|
196
231
|
}
|
|
197
232
|
}
|
|
198
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Add our rules file to kilo.jsonc's `instructions` array. Kilo does not
|
|
237
|
+
* auto-load `.kilo/rules/` — a file is only read if it's listed here, so
|
|
238
|
+
* without this the pointer is silently ignored.
|
|
239
|
+
* Merges into an existing config; never overwrites the user's settings.
|
|
240
|
+
*/
|
|
241
|
+
async function writeKiloConfig({ projectDir, report }) {
|
|
242
|
+
const configPath = path.join(projectDir, 'kilo.jsonc');
|
|
243
|
+
const entry = '.agents/rules/kracked.md';
|
|
244
|
+
|
|
245
|
+
if (fs.existsSync(configPath)) {
|
|
246
|
+
const body = fs.readFileSync(configPath, 'utf8');
|
|
247
|
+
if (body.includes(entry)) {
|
|
248
|
+
report({ path: configPath, action: 'already configured' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Append to the existing instructions array if there is one, else add it.
|
|
252
|
+
let updated;
|
|
253
|
+
if (/"instructions"\s*:\s*\[/.test(body)) {
|
|
254
|
+
updated = body.replace(/("instructions"\s*:\s*\[)/, `$1\n "${entry}",`);
|
|
255
|
+
} else {
|
|
256
|
+
updated = body.replace(/^\s*\{/, `{\n "instructions": ["${entry}"],`);
|
|
257
|
+
}
|
|
258
|
+
fs.writeFileSync(configPath, updated, 'utf8');
|
|
259
|
+
report({ path: configPath, action: 'updated (added instructions entry)' });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const fresh = `{
|
|
264
|
+
// Kilo reads AGENTS.md automatically. This entry additionally loads the
|
|
265
|
+
// kracked-core rules pointer. Add your own settings alongside it.
|
|
266
|
+
"instructions": ["${entry}"]
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
fs.writeFileSync(configPath, fresh, 'utf8');
|
|
270
|
+
report({ path: configPath, action: 'written' });
|
|
271
|
+
}
|