karajan-code 1.56.0 → 1.57.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/README.md +9 -2
- package/docs/README.es.md +9 -2
- package/package.json +1 -1
- package/scripts/ai-attribution-guard.yml +77 -0
- package/scripts/esbuild-sea.config.mjs +83 -15
- package/scripts/install-guard-all-repos.sh +85 -0
- package/scripts/protect-all-repos.sh +88 -0
- package/src/bootstrap.js +8 -4
- package/src/cli.js +6 -0
- package/src/commands/init.js +41 -0
- package/src/config.js +20 -6
- package/src/mcp/error-classifiers.js +66 -0
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/hu-handlers.js +1 -1
- package/src/mcp/handlers/management-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +6 -4
- package/src/mcp/orphan-guard.js +41 -4
- package/src/mcp/server-handlers.js +44 -229
- package/src/mcp/server.js +8 -1
- package/src/mcp/shared-helpers.js +144 -0
- package/src/orchestrator/preflight-checks.js +16 -5
- package/src/orchestrator.js +14 -0
- package/src/utils/process.js +4 -0
- package/src/utils/telemetry.js +47 -0
package/README.md
CHANGED
|
@@ -60,8 +60,7 @@ npm install -g karajan-code
|
|
|
60
60
|
|
|
61
61
|
**Homebrew** (macOS):
|
|
62
62
|
```bash
|
|
63
|
-
brew
|
|
64
|
-
brew install karajan-code
|
|
63
|
+
brew install manufosela/tap/karajan-code
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
**One-liner** (detects OS, installs via npm):
|
|
@@ -264,6 +263,14 @@ npm run validate # Lint + test
|
|
|
264
263
|
|
|
265
264
|
Issues and pull requests welcome. If something doesn't work as documented, [open an issue](https://github.com/manufosela/karajan-code/issues). That's the most useful contribution at this stage.
|
|
266
265
|
|
|
266
|
+
## Telemetry
|
|
267
|
+
|
|
268
|
+
Karajan collects anonymous usage statistics to improve the tool:
|
|
269
|
+
version, OS, command used, pipeline duration and success rate.
|
|
270
|
+
No code, task descriptions, or personal data is ever sent.
|
|
271
|
+
|
|
272
|
+
Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
|
|
273
|
+
|
|
267
274
|
## Links
|
|
268
275
|
|
|
269
276
|
- [Website](https://karajancode.com) (also [kj-code.com](https://kj-code.com))
|
package/docs/README.es.md
CHANGED
|
@@ -60,8 +60,7 @@ npm install -g karajan-code
|
|
|
60
60
|
|
|
61
61
|
**Homebrew** (macOS):
|
|
62
62
|
```bash
|
|
63
|
-
brew
|
|
64
|
-
brew install karajan-code
|
|
63
|
+
brew install manufosela/tap/karajan-code
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
**One-liner** (detecta SO, instala via npm):
|
|
@@ -237,6 +236,14 @@ npm run validate # Lint + test
|
|
|
237
236
|
|
|
238
237
|
Issues y pull requests bienvenidos. Si algo no funciona como esta documentado, [abre un issue](https://github.com/manufosela/karajan-code/issues). Es la contribucion mas util en esta fase.
|
|
239
238
|
|
|
239
|
+
## Telemetria
|
|
240
|
+
|
|
241
|
+
Karajan recopila estadisticas de uso anonimas para mejorar la herramienta:
|
|
242
|
+
version, sistema operativo, comando utilizado, duracion del pipeline y tasa de exito.
|
|
243
|
+
No se envia nunca codigo, descripciones de tareas ni datos personales.
|
|
244
|
+
|
|
245
|
+
Desactivar: establece `telemetry: false` en `~/.karajan/kj.config.yml`
|
|
246
|
+
|
|
240
247
|
## Enlaces
|
|
241
248
|
|
|
242
249
|
- [Web](https://karajancode.com) (tambien [kj-code.com](https://kj-code.com))
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# AI Attribution Guard — GitHub Action
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to .github/workflows/ai-attribution-guard.yml in any repo.
|
|
4
|
+
# Blocks PRs that contain AI attribution comments or Co-Authored-By lines.
|
|
5
|
+
# No dependencies, no Node.js required. Pure bash + grep.
|
|
6
|
+
|
|
7
|
+
name: AI Attribution Guard
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
types: [opened, synchronize, reopened]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
scan:
|
|
15
|
+
name: Scan for AI attribution
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- name: Check PR diff for AI attribution
|
|
23
|
+
run: |
|
|
24
|
+
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD \
|
|
25
|
+
-- ':!*.png' ':!*.jpg' ':!*.svg' ':!*.ico' \
|
|
26
|
+
':!*.lock' ':!package-lock.json' ':!yarn.lock' \
|
|
27
|
+
':!*.min.js' ':!*.min.css' \
|
|
28
|
+
':!.githooks/*' \
|
|
29
|
+
| grep '^+' \
|
|
30
|
+
| grep -v '^+++' \
|
|
31
|
+
|| true)
|
|
32
|
+
|
|
33
|
+
[ -z "$DIFF" ] && echo "No diff to scan" && exit 0
|
|
34
|
+
|
|
35
|
+
# Attribution patterns (not functional references like provider: "claude")
|
|
36
|
+
ATTR='(Generated|Created|Written|Made|Built|Authored|Assisted|Powered|Produced) (by|with|using|via) .*(Claude|Anthropic|OpenAI|ChatGPT|GPT-[0-9]|Copilot|Gemini|Cursor|Windsurf|Codeium)'
|
|
37
|
+
COAUTH='Co-Authored-By:.*\b(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)\b'
|
|
38
|
+
COMMENT='(//|#|/\*|\*|<!--|--)\s*.*(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini).*(generated|assisted|helped|created|wrote|built|made)'
|
|
39
|
+
|
|
40
|
+
FOUND=""
|
|
41
|
+
for PAT in "$ATTR" "$COAUTH" "$COMMENT"; do
|
|
42
|
+
MATCH=$(echo "$DIFF" | grep -i -E "$PAT" || true)
|
|
43
|
+
[ -n "$MATCH" ] && FOUND="${FOUND}${MATCH}\n"
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [ -n "$FOUND" ]; then
|
|
47
|
+
echo "::error::AI attribution detected in PR diff"
|
|
48
|
+
echo -e "$FOUND"
|
|
49
|
+
echo ""
|
|
50
|
+
echo "Remove AI attribution comments/lines and push again."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "AI attribution guard: clean"
|
|
55
|
+
|
|
56
|
+
- name: Check commit messages
|
|
57
|
+
run: |
|
|
58
|
+
COMMITS=$(git log --format='%s%n%b' origin/${{ github.base_ref }}...HEAD)
|
|
59
|
+
|
|
60
|
+
COAUTH='Co-Authored-By:.*\b(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)\b'
|
|
61
|
+
ATTR='(Generated|Created|Written|Built|Authored) (by|with|using) .*(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)'
|
|
62
|
+
|
|
63
|
+
FOUND=""
|
|
64
|
+
for PAT in "$COAUTH" "$ATTR"; do
|
|
65
|
+
MATCH=$(echo "$COMMITS" | grep -i -E "$PAT" || true)
|
|
66
|
+
[ -n "$MATCH" ] && FOUND="${FOUND}${MATCH}\n"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
if [ -n "$FOUND" ]; then
|
|
70
|
+
echo "::error::AI attribution detected in commit messages"
|
|
71
|
+
echo -e "$FOUND"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Amend commits to remove AI attribution and force-push."
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo "Commit messages: clean"
|
|
@@ -2,9 +2,90 @@
|
|
|
2
2
|
* esbuild configuration for Node.js SEA (Single Executable Application) bundle.
|
|
3
3
|
*
|
|
4
4
|
* Bundles the entire CLI into a single CJS file suitable for SEA injection.
|
|
5
|
-
*
|
|
5
|
+
* A plugin handles:
|
|
6
|
+
* 1. import.meta.url / import.meta.dirname -> CJS equivalents
|
|
7
|
+
* 2. Top-level await in cli.js -> async IIFE wrapper
|
|
8
|
+
* 3. Runtime package.json reads -> inlined version string
|
|
6
9
|
*/
|
|
7
10
|
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const PKG_VERSION = JSON.parse(readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
|
18
|
+
|
|
19
|
+
const seaTransformPlugin = {
|
|
20
|
+
name: "sea-transform",
|
|
21
|
+
setup(build) {
|
|
22
|
+
build.onLoad({ filter: /\.m?js$/ }, async (args) => {
|
|
23
|
+
let contents = await fs.readFile(args.path, "utf8");
|
|
24
|
+
let modified = false;
|
|
25
|
+
|
|
26
|
+
// Replace import.meta.dirname with __dirname
|
|
27
|
+
if (contents.includes("import.meta.dirname")) {
|
|
28
|
+
contents = contents.replaceAll("import.meta.dirname", "__dirname");
|
|
29
|
+
modified = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Replace import.meta.url with CJS equivalent
|
|
33
|
+
if (contents.includes("import.meta.url")) {
|
|
34
|
+
contents = contents.replaceAll(
|
|
35
|
+
"import.meta.url",
|
|
36
|
+
'require("node:url").pathToFileURL(__filename).href'
|
|
37
|
+
);
|
|
38
|
+
modified = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Inline version: replace any pattern that reads ../../package.json or ../package.json
|
|
42
|
+
// to extract the version, with the hardcoded version string.
|
|
43
|
+
// Covers cli.js, display.js, doctor.js, bootstrap.js, init.js, server.js
|
|
44
|
+
if (contents.includes("package.json") && contents.match(/["']\.\.\/.*package\.json/)) {
|
|
45
|
+
// Pattern: const X = path.resolve(..., "../../package.json");
|
|
46
|
+
// const Y = JSON.parse(readFileSync(X, "utf8")).version;
|
|
47
|
+
// Replace both lines with: const Y = "1.57.0";
|
|
48
|
+
contents = contents.replace(
|
|
49
|
+
/const (\w+) = (?:path\.resolve|resolve)\(.*?["']\.\.\/.*?package\.json["']\);\s*\n\s*(?:const|return) (\w+) = JSON\.parse\(.*?readFileSync\(\1.*?\).*?\.version;/g,
|
|
50
|
+
`const $2 = ${JSON.stringify(PKG_VERSION)};`
|
|
51
|
+
);
|
|
52
|
+
// Single-line pattern: return JSON.parse(readFileSync(pkgPath...)).version
|
|
53
|
+
contents = contents.replace(
|
|
54
|
+
/const (\w+) = (?:path\.resolve|resolve)\(.*?["']\.\.\/.*?package\.json["']\);\s*\n\s*return JSON\.parse\(.*?readFileSync\(\1.*?\).*?\.version;/g,
|
|
55
|
+
`return ${JSON.stringify(PKG_VERSION)};`
|
|
56
|
+
);
|
|
57
|
+
// Pattern in cli.js: const PKG_PATH = ...; const PKG_VERSION = ...;
|
|
58
|
+
contents = contents.replace(
|
|
59
|
+
/const PKG_PATH = .*?package\.json.*?;\s*\nconst PKG_VERSION = .*?\.version;/,
|
|
60
|
+
`const PKG_VERSION = ${JSON.stringify(PKG_VERSION)};`
|
|
61
|
+
);
|
|
62
|
+
// Pattern in display.js: const DISPLAY_PKG_PATH = ...; const DISPLAY_VERSION = ...;
|
|
63
|
+
contents = contents.replace(
|
|
64
|
+
/const DISPLAY_PKG_PATH = .*?package\.json.*?;\s*\nconst DISPLAY_VERSION = .*?\.version;/,
|
|
65
|
+
`const DISPLAY_VERSION = ${JSON.stringify(PKG_VERSION)};`
|
|
66
|
+
);
|
|
67
|
+
// Pattern in server.js: const PKG_PATH = ...; (used later to read version)
|
|
68
|
+
contents = contents.replace(
|
|
69
|
+
/const PKG_PATH = .*?package\.json.*?;\s*\nconst PKG = JSON\.parse\(readFileSync\(PKG_PATH.*?\)\);/,
|
|
70
|
+
`const PKG = { version: ${JSON.stringify(PKG_VERSION)}, name: "karajan-code" };`
|
|
71
|
+
);
|
|
72
|
+
modified = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Wrap top-level await in cli.js inside an async IIFE
|
|
76
|
+
if (args.path.endsWith("src/cli.js") || args.path.endsWith("src\\cli.js")) {
|
|
77
|
+
contents = contents.replace(
|
|
78
|
+
/^(try\s*\{[\s\S]*?await\s+program\.parseAsync\(\)[\s\S]*?^\})\s*$/m,
|
|
79
|
+
"(async () => {\n$1\n})();"
|
|
80
|
+
);
|
|
81
|
+
modified = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return modified ? { contents, loader: "js" } : undefined;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
8
89
|
/** @type {import('esbuild').BuildOptions} */
|
|
9
90
|
export const seaBuildOptions = {
|
|
10
91
|
entryPoints: ["src/cli.js"],
|
|
@@ -14,20 +95,7 @@ export const seaBuildOptions = {
|
|
|
14
95
|
target: "node20",
|
|
15
96
|
bundle: true,
|
|
16
97
|
minify: false,
|
|
17
|
-
// Bundle everything — no externals. The binary must be fully self-contained.
|
|
18
98
|
external: [],
|
|
19
|
-
|
|
20
|
-
banner: {
|
|
21
|
-
js: [
|
|
22
|
-
"/* Karajan Code — SEA bundle */",
|
|
23
|
-
"/* eslint-disable */",
|
|
24
|
-
'const __importMetaUrl = require("node:url").pathToFileURL(__filename).href;',
|
|
25
|
-
].join("\n"),
|
|
26
|
-
},
|
|
27
|
-
// Replace import.meta.url references with the CJS-compatible shim variable.
|
|
28
|
-
define: {
|
|
29
|
-
"import.meta.url": "__importMetaUrl",
|
|
30
|
-
},
|
|
31
|
-
// Log level for debugging build issues.
|
|
99
|
+
plugins: [seaTransformPlugin],
|
|
32
100
|
logLevel: "info",
|
|
33
101
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Installs the AI Attribution Guard workflow in all repos of a GitHub user.
|
|
4
|
+
# Requires: gh CLI authenticated with admin/push permissions.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ./scripts/install-guard-all-repos.sh # dry-run
|
|
8
|
+
# ./scripts/install-guard-all-repos.sh --apply # install in all repos
|
|
9
|
+
#
|
|
10
|
+
# What it does:
|
|
11
|
+
# - Creates .github/workflows/ai-attribution-guard.yml in each repo
|
|
12
|
+
# - Skips repos that already have the file
|
|
13
|
+
# - Skips archived repos
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
OWNER="${GH_OWNER:-manufosela}"
|
|
18
|
+
APPLY=false
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
+
WORKFLOW_FILE="$SCRIPT_DIR/ai-attribution-guard.yml"
|
|
21
|
+
|
|
22
|
+
if [[ "${1:-}" == "--apply" ]]; then
|
|
23
|
+
APPLY=true
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [[ ! -f "$WORKFLOW_FILE" ]]; then
|
|
27
|
+
echo "ERROR: $WORKFLOW_FILE not found"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CONTENT=$(base64 -w 0 "$WORKFLOW_FILE")
|
|
32
|
+
|
|
33
|
+
echo "=== Install AI Attribution Guard in all repos of $OWNER ==="
|
|
34
|
+
echo ""
|
|
35
|
+
|
|
36
|
+
if [[ "$APPLY" == false ]]; then
|
|
37
|
+
echo " DRY RUN mode. Use --apply to actually install."
|
|
38
|
+
echo ""
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
REPOS=$(gh repo list "$OWNER" --limit 500 --json name,isArchived \
|
|
42
|
+
--jq '.[] | select(.isArchived == false) | .name')
|
|
43
|
+
|
|
44
|
+
TOTAL=0
|
|
45
|
+
INSTALLED=0
|
|
46
|
+
SKIPPED=0
|
|
47
|
+
FAILED=0
|
|
48
|
+
|
|
49
|
+
while read -r REPO; do
|
|
50
|
+
[ -z "$REPO" ] && continue
|
|
51
|
+
TOTAL=$((TOTAL + 1))
|
|
52
|
+
|
|
53
|
+
# Check if workflow already exists
|
|
54
|
+
EXISTS=$(gh api "repos/$OWNER/$REPO/contents/.github/workflows/ai-attribution-guard.yml" --jq '.sha' 2>/dev/null || echo "")
|
|
55
|
+
|
|
56
|
+
if [[ -n "$EXISTS" ]]; then
|
|
57
|
+
echo " SKIP $REPO — workflow already exists"
|
|
58
|
+
SKIPPED=$((SKIPPED + 1))
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [[ "$APPLY" == false ]]; then
|
|
63
|
+
echo " WOULD $REPO"
|
|
64
|
+
continue
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if gh api "repos/$OWNER/$REPO/contents/.github/workflows/ai-attribution-guard.yml" -X PUT \
|
|
68
|
+
-f message="chore: add AI attribution guard workflow" \
|
|
69
|
+
-f content="$CONTENT" \
|
|
70
|
+
--jq '.commit.sha' 2>/dev/null; then
|
|
71
|
+
echo " OK $REPO"
|
|
72
|
+
INSTALLED=$((INSTALLED + 1))
|
|
73
|
+
else
|
|
74
|
+
echo " FAIL $REPO — check permissions"
|
|
75
|
+
FAILED=$((FAILED + 1))
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
done <<< "$REPOS"
|
|
79
|
+
|
|
80
|
+
echo ""
|
|
81
|
+
echo "=== Summary ==="
|
|
82
|
+
echo " Total: $TOTAL"
|
|
83
|
+
echo " Installed: $INSTALLED"
|
|
84
|
+
echo " Skipped: $SKIPPED"
|
|
85
|
+
echo " Failed: $FAILED"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Configures branch protection on all repos of a GitHub user.
|
|
4
|
+
# Requires: gh CLI authenticated with admin permissions.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ./scripts/protect-all-repos.sh # dry-run (shows what it would do)
|
|
8
|
+
# ./scripts/protect-all-repos.sh --apply # apply protection rules
|
|
9
|
+
#
|
|
10
|
+
# What it does:
|
|
11
|
+
# - Require PRs before merging (no direct push to default branch)
|
|
12
|
+
# - Block force push
|
|
13
|
+
# - Block branch deletion
|
|
14
|
+
#
|
|
15
|
+
# Skips repos where branch protection is already configured.
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
OWNER="${GH_OWNER:-manufosela}"
|
|
20
|
+
APPLY=false
|
|
21
|
+
|
|
22
|
+
if [[ "${1:-}" == "--apply" ]]; then
|
|
23
|
+
APPLY=true
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "=== Branch Protection for all repos of $OWNER ==="
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
if [[ "$APPLY" == false ]]; then
|
|
30
|
+
echo " DRY RUN mode. Use --apply to actually configure."
|
|
31
|
+
echo ""
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
REPOS=$(gh repo list "$OWNER" --limit 500 --json name,defaultBranchRef,isArchived \
|
|
35
|
+
--jq '.[] | select(.isArchived == false) | "\(.name)\t\(.defaultBranchRef.name // "main")"')
|
|
36
|
+
|
|
37
|
+
TOTAL=0
|
|
38
|
+
PROTECTED=0
|
|
39
|
+
SKIPPED=0
|
|
40
|
+
FAILED=0
|
|
41
|
+
|
|
42
|
+
while IFS=$'\t' read -r REPO BRANCH; do
|
|
43
|
+
TOTAL=$((TOTAL + 1))
|
|
44
|
+
|
|
45
|
+
# Check if already protected
|
|
46
|
+
STATUS=$(gh api "repos/$OWNER/$REPO/branches/$BRANCH/protection" --jq '.required_pull_request_reviews.required_approving_review_count // 0' 2>/dev/null || echo "none")
|
|
47
|
+
|
|
48
|
+
if [[ "$STATUS" != "none" ]]; then
|
|
49
|
+
echo " SKIP $REPO ($BRANCH) — already protected"
|
|
50
|
+
SKIPPED=$((SKIPPED + 1))
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ "$APPLY" == false ]]; then
|
|
55
|
+
echo " WOULD $REPO ($BRANCH)"
|
|
56
|
+
continue
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Apply protection
|
|
60
|
+
if gh api "repos/$OWNER/$REPO/branches/$BRANCH/protection" -X PUT \
|
|
61
|
+
--input - <<EOF 2>/dev/null; then
|
|
62
|
+
{
|
|
63
|
+
"required_pull_request_reviews": {
|
|
64
|
+
"required_approving_review_count": 0,
|
|
65
|
+
"dismiss_stale_reviews": false
|
|
66
|
+
},
|
|
67
|
+
"enforce_admins": false,
|
|
68
|
+
"required_status_checks": null,
|
|
69
|
+
"restrictions": null,
|
|
70
|
+
"allow_force_pushes": false,
|
|
71
|
+
"allow_deletions": false
|
|
72
|
+
}
|
|
73
|
+
EOF
|
|
74
|
+
echo " OK $REPO ($BRANCH)"
|
|
75
|
+
PROTECTED=$((PROTECTED + 1))
|
|
76
|
+
else
|
|
77
|
+
echo " FAIL $REPO ($BRANCH) — check permissions"
|
|
78
|
+
FAILED=$((FAILED + 1))
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
done <<< "$REPOS"
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "=== Summary ==="
|
|
85
|
+
echo " Total: $TOTAL"
|
|
86
|
+
echo " Protected: $PROTECTED"
|
|
87
|
+
echo " Skipped: $SKIPPED"
|
|
88
|
+
echo " Failed: $FAILED"
|
package/src/bootstrap.js
CHANGED
|
@@ -130,12 +130,16 @@ async function checkSonarQubeReady(config) {
|
|
|
130
130
|
return { name: "sonarqube", ok: true, detail: `Reachable at ${host}`, fix: null };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// Auto-remediation:
|
|
133
|
+
// Auto-remediation: start and wait up to 60s for SonarQube to be ready
|
|
134
134
|
try {
|
|
135
135
|
await sonarUp(host);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const deadline = Date.now() + 60000;
|
|
137
|
+
while (Date.now() < deadline) {
|
|
138
|
+
reachable = await isSonarReachable(host);
|
|
139
|
+
if (reachable) {
|
|
140
|
+
return { name: "sonarqube", ok: true, detail: `Started and reachable at ${host}`, fix: null };
|
|
141
|
+
}
|
|
142
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
139
143
|
}
|
|
140
144
|
} catch { /* fall through */ }
|
|
141
145
|
|
package/src/cli.js
CHANGED
|
@@ -40,6 +40,12 @@ async function withConfig(commandName, flags, fn) {
|
|
|
40
40
|
const merged = applyRunOverrides(config, flags || {});
|
|
41
41
|
validateConfig(merged, commandName);
|
|
42
42
|
const logger = createLogger(merged.output.log_level);
|
|
43
|
+
|
|
44
|
+
// Telemetry: anonymous cli_command event (non-blocking)
|
|
45
|
+
import("./utils/telemetry.js")
|
|
46
|
+
.then(({ sendTelemetryEvent }) => sendTelemetryEvent("cli_command", { command: commandName, version: PKG_VERSION }, merged))
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
|
|
43
49
|
await fn({ config: merged, logger, flags });
|
|
44
50
|
}
|
|
45
51
|
|
package/src/commands/init.js
CHANGED
|
@@ -305,6 +305,35 @@ async function installSkills(logger, interactive) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
const GITIGNORE_ENTRIES = [
|
|
309
|
+
{ pattern: ".kj/", comment: "Karajan runtime (logs, worktrees)" },
|
|
310
|
+
{ pattern: ".agent/", comment: "Agent skills (OpenSkills)" },
|
|
311
|
+
{ pattern: ".scannerwork/", comment: "SonarQube scanner temp" },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
async function ensureGitignoreEntries(projectDir, logger) {
|
|
315
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
316
|
+
let content;
|
|
317
|
+
try {
|
|
318
|
+
content = await fs.readFile(gitignorePath, "utf8");
|
|
319
|
+
} catch {
|
|
320
|
+
// No .gitignore yet — will create one
|
|
321
|
+
}
|
|
322
|
+
if (!content) content = "";
|
|
323
|
+
|
|
324
|
+
const missing = GITIGNORE_ENTRIES.filter(e => !content.includes(e.pattern));
|
|
325
|
+
if (missing.length === 0) return;
|
|
326
|
+
|
|
327
|
+
const block = [
|
|
328
|
+
"",
|
|
329
|
+
"# Karajan Code",
|
|
330
|
+
...missing.map(e => e.pattern)
|
|
331
|
+
].join("\n") + "\n";
|
|
332
|
+
|
|
333
|
+
await fs.appendFile(gitignorePath, block, "utf8");
|
|
334
|
+
logger.info(`Added to .gitignore: ${missing.map(e => e.pattern).join(", ")}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
308
337
|
export async function initCommand({ logger, flags = {} }) {
|
|
309
338
|
const karajanHome = getKarajanHome();
|
|
310
339
|
await ensureDir(karajanHome);
|
|
@@ -329,6 +358,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
329
358
|
await handleConfigSetup({ config, configExists, interactive, configPath, logger });
|
|
330
359
|
await ensureReviewRules(reviewRulesPath, logger);
|
|
331
360
|
await ensureCoderRules(coderRulesPath, logger);
|
|
361
|
+
await ensureGitignoreEntries(process.cwd(), logger);
|
|
332
362
|
await installSkills(logger, interactive);
|
|
333
363
|
|
|
334
364
|
// Auto-detect project stack and preconfigure
|
|
@@ -374,4 +404,15 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
374
404
|
|
|
375
405
|
await setupSonarQube(config, logger);
|
|
376
406
|
await scaffoldBecariaGateway(config, flags, logger);
|
|
407
|
+
|
|
408
|
+
// Telemetry: anonymous install event (non-blocking)
|
|
409
|
+
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
410
|
+
const { readFileSync } = await import("node:fs");
|
|
411
|
+
const { resolve, dirname } = await import("node:path");
|
|
412
|
+
const { fileURLToPath } = await import("node:url");
|
|
413
|
+
try {
|
|
414
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
415
|
+
const version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
416
|
+
sendTelemetryEvent("install", { version }, config).catch(() => {});
|
|
417
|
+
} catch { /* non-blocking */ }
|
|
377
418
|
}
|
package/src/config.js
CHANGED
|
@@ -171,6 +171,7 @@ const DEFAULTS = {
|
|
|
171
171
|
devtools_mcp: false,
|
|
172
172
|
thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
|
|
173
173
|
},
|
|
174
|
+
telemetry: true,
|
|
174
175
|
guards: {
|
|
175
176
|
output: {
|
|
176
177
|
enabled: true,
|
|
@@ -220,7 +221,7 @@ export async function loadProjectConfig(projectDir = process.cwd()) {
|
|
|
220
221
|
return null;
|
|
221
222
|
}
|
|
222
223
|
const raw = await fs.readFile(projectConfigPath, "utf8");
|
|
223
|
-
return yaml.load(raw) || {};
|
|
224
|
+
return yaml.load(raw, { json: true }) || {};
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
async function loadProjectPricingOverrides(projectDir = process.cwd()) {
|
|
@@ -230,7 +231,7 @@ async function loadProjectPricingOverrides(projectDir = process.cwd()) {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
const raw = await fs.readFile(projectConfigPath, "utf8");
|
|
233
|
-
const parsed = yaml.load(raw) || {};
|
|
234
|
+
const parsed = yaml.load(raw, { json: true }) || {};
|
|
234
235
|
const pricing = parsed?.budget?.pricing;
|
|
235
236
|
if (!pricing || typeof pricing !== "object") {
|
|
236
237
|
return null;
|
|
@@ -248,7 +249,7 @@ export async function loadConfig(projectDir) {
|
|
|
248
249
|
const globalExists = await exists(configPath);
|
|
249
250
|
if (globalExists) {
|
|
250
251
|
const raw = await fs.readFile(configPath, "utf8");
|
|
251
|
-
globalConfig = yaml.load(raw) || {};
|
|
252
|
+
globalConfig = yaml.load(raw, { json: true }) || {};
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
// Load project config (.karajan/kj.config.yml)
|
|
@@ -468,6 +469,13 @@ const CODER_INHERITED_ROLES = new Set([
|
|
|
468
469
|
|
|
469
470
|
function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
|
|
470
471
|
if (roleConfig.provider) return roleConfig.provider;
|
|
472
|
+
|
|
473
|
+
// If model has "provider/model" format (e.g. "gemini/pro"), extract the provider
|
|
474
|
+
if (roleConfig.model && roleConfig.model.includes("/")) {
|
|
475
|
+
const inferredProvider = roleConfig.model.split("/")[0].toLowerCase();
|
|
476
|
+
if (AGENT_MODEL_SIGNATURES[inferredProvider]) return inferredProvider;
|
|
477
|
+
}
|
|
478
|
+
|
|
471
479
|
if (role === "coder") return legacyCoder;
|
|
472
480
|
if (role === "reviewer") return legacyReviewer;
|
|
473
481
|
if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
|
|
@@ -475,7 +483,13 @@ function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
|
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
function resolveModel(roleConfig, role, config) {
|
|
478
|
-
if (roleConfig.model)
|
|
486
|
+
if (roleConfig.model) {
|
|
487
|
+
// Strip "provider/" prefix from models like "gemini/pro" → "pro"
|
|
488
|
+
const model = roleConfig.model.includes("/")
|
|
489
|
+
? roleConfig.model.split("/").slice(1).join("/")
|
|
490
|
+
: roleConfig.model;
|
|
491
|
+
return { model, inherited: false };
|
|
492
|
+
}
|
|
479
493
|
if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
|
|
480
494
|
if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
|
|
481
495
|
if (CODER_INHERITED_ROLES.has(role)) {
|
|
@@ -494,8 +508,8 @@ export function resolveRole(config, role) {
|
|
|
494
508
|
const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
|
|
495
509
|
let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
|
|
496
510
|
|
|
497
|
-
// Drop
|
|
498
|
-
if (
|
|
511
|
+
// Drop model if incompatible with the resolved provider (inherited or explicit)
|
|
512
|
+
if (provider && model && !isModelCompatible(provider, model)) {
|
|
499
513
|
model = null;
|
|
500
514
|
}
|
|
501
515
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classification for MCP tool responses.
|
|
3
|
+
* Extracted to its own module to avoid circular import TDZ issues
|
|
4
|
+
* (server-handlers.js ↔ handlers/*.js circular chain).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
8
|
+
|
|
9
|
+
const ERROR_CLASSIFIERS = [
|
|
10
|
+
{
|
|
11
|
+
test: (lower) => lower.includes("without output") || lower.includes("silent for") || lower.includes("unresponsive") || lower.includes("exceeded max silence"),
|
|
12
|
+
category: "agent_stall",
|
|
13
|
+
suggestion: "Agent output stalled. Check live details with kj_status, then retry with a smaller prompt or increase session.max_agent_silence_minutes if needed."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
test: (lower) => lower.includes("sonar") && (lower.includes("connect") || lower.includes("econnrefused") || lower.includes("not available") || lower.includes("not running")),
|
|
17
|
+
category: "sonar_unavailable",
|
|
18
|
+
suggestion: withDocLink("SonarQube is not reachable. Try: kj_init to set up SonarQube, or run 'docker start sonarqube' if already installed. Use --no-sonar to skip SonarQube.", "sonar_docker")
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
test: (lower) => lower.includes("401") || lower.includes("unauthorized") || lower.includes("invalid token"),
|
|
22
|
+
category: "auth_error",
|
|
23
|
+
suggestion: withDocLink("Authentication failed. Regenerate the SonarQube token and update it via kj_init or in ~/.karajan/kj.config.yml under sonarqube.token.", "sonar_token")
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
test: (lower) => lower.includes("config") && (lower.includes("missing") || lower.includes("not found") || lower.includes("invalid")),
|
|
27
|
+
category: "config_error",
|
|
28
|
+
suggestion: withDocLink("Configuration issue detected. Run kj_doctor to diagnose, or kj_init to create a fresh config.", "config_missing")
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
test: (lower) => lower.includes("missing provider") || lower.includes("not found") && (lower.includes("claude") || lower.includes("codex") || lower.includes("gemini") || lower.includes("aider")),
|
|
32
|
+
category: "agent_missing",
|
|
33
|
+
suggestion: withDocLink("Required agent CLI not found. Run kj_doctor to check which agents are installed and get installation instructions.", "agent_not_found")
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
test: (lower) => lower.includes("timed out") || lower.includes("timeout"),
|
|
37
|
+
category: "timeout",
|
|
38
|
+
suggestion: "The agent did not complete in time. Try: (1) increase --max-iteration-minutes (default: 5), (2) split the task into smaller pieces, (3) use kj_code for single-agent tasks. If a SonarQube scan timed out, check Docker health."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
test: (lower) => lower.includes("you are on the base branch"),
|
|
42
|
+
category: "branch_error",
|
|
43
|
+
suggestion: withDocLink("Create a feature branch before running Karajan. Use 'git checkout -b feat/<task-description>' and then retry. Do NOT run kj_code directly on the base branch.", "branch_error")
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
test: (lower) => lower.includes("not a git repository"),
|
|
47
|
+
category: "git_error",
|
|
48
|
+
suggestion: "Current directory is not a git repository. Navigate to your project root or initialize git with 'git init'."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
test: (lower) => lower.includes("bootstrap failed"),
|
|
52
|
+
category: "bootstrap_error",
|
|
53
|
+
suggestion: withDocLink("Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly.", "bootstrap_failed")
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function classifyError(error) {
|
|
58
|
+
const msg = error?.message || String(error);
|
|
59
|
+
const lower = msg.toLowerCase();
|
|
60
|
+
|
|
61
|
+
const match = ERROR_CLASSIFIERS.find(c => c.test(lower));
|
|
62
|
+
if (match) {
|
|
63
|
+
return { category: match.category, suggestion: match.suggestion };
|
|
64
|
+
}
|
|
65
|
+
return { category: "unknown", suggestion: null };
|
|
66
|
+
}
|