karajan-code 2.33.0 → 3.0.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 +136 -36
- package/bin/kj-rag-mcp.js +75 -0
- package/docs/README.es.md +4 -4
- package/package.json +22 -16
- package/packages/hu-board/package.json +5 -2
- package/scripts/git-hooks/post-merge +7 -0
- package/src/audit/basal-cost.js +17 -12
- package/src/audit/deterministic-summary.js +20 -0
- package/src/audit/injection-findings.js +105 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/node.js +9 -10
- package/src/cli/register-meta.js +57 -1
- package/src/cli/register-pipeline.js +1 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/init.js +41 -1
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/rag.js +82 -8
- package/src/commands/run.js +8 -0
- package/src/commands/telemetry.js +70 -0
- package/src/config/defaults.js +11 -1
- package/src/lang/chunk-go.js +94 -0
- package/src/lang/chunk-java.js +90 -0
- package/src/lang/chunk-python.js +88 -0
- package/src/lang/chunk-rust.js +102 -0
- package/src/lang/go.js +14 -0
- package/src/lang/java.js +16 -0
- package/src/lang/js.js +8 -0
- package/src/lang/python.js +14 -0
- package/src/lang/registry.js +79 -0
- package/src/lang/rust.js +14 -0
- package/src/lang/tree-sitter-loader.js +49 -0
- package/src/onboarder/collectors/framework-multilang.js +64 -0
- package/src/onboarder/collectors/index.js +18 -1
- package/src/orchestrator/stages/coder-stage.js +3 -0
- package/src/orchestrator/stages/reviewer-stage.js +11 -0
- package/src/rag/auto-update.js +50 -0
- package/src/rag/chunker.js +7 -3
- package/src/rag/eval.js +61 -0
- package/src/rag/indexer.js +81 -10
- package/src/rag/retriever.js +57 -5
- package/src/rag/vec-store.js +69 -3
- package/src/rag/watcher.js +6 -4
- package/src/roles/audit-role.js +9 -1
- package/src/sonar/scanner.js +51 -16
- package/src/utils/manifest-sniff.js +82 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/telemetry.js +34 -12
- package/templates/roles/onboarder.md +10 -1
- package/vendor/tree-sitter-grammars/README.md +21 -0
- package/vendor/tree-sitter-grammars/go.wasm +0 -0
- package/vendor/tree-sitter-grammars/java.wasm +0 -0
- package/vendor/tree-sitter-grammars/python.wasm +0 -0
- package/vendor/tree-sitter-grammars/rust.wasm +0 -0
package/src/rag/watcher.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
// KJC-TSK-0441 (RAG v2.28.0 PR1) — chokidar watcher. Live re-index of plans
|
|
2
2
|
// + onboarding + (opt) sources, debounced. PID file arbitrates a single daemon.
|
|
3
|
+
// KJC-TSK-0482 — source matcher derivado del registry (multi-lang: JS/Py/Rust/Go/Java).
|
|
3
4
|
import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { extname, join } from "node:path";
|
|
5
6
|
import chokidar from "chokidar";
|
|
6
7
|
import { openVecStore, deleteChunksBySource, projectSlug } from "./vec-store.js";
|
|
7
8
|
import { makeEmbedder } from "./embedders/factory.js";
|
|
8
9
|
import { indexFile } from "./indexer.js";
|
|
9
10
|
import { getKarajanHome } from "../utils/paths.js";
|
|
11
|
+
import { getAllCodeExtensions } from "../lang/registry.js";
|
|
10
12
|
|
|
11
13
|
const PIDFILE = () => join(getKarajanHome(), "watcher.pid");
|
|
12
14
|
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
13
15
|
const SKIP_SEGMENTS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".next", ".kj", "_diet"]);
|
|
14
|
-
const
|
|
16
|
+
const SOURCE_EXTS = new Set(getAllCodeExtensions().map((e) => e.toLowerCase()));
|
|
15
17
|
|
|
16
|
-
function isWatchable(path) {
|
|
18
|
+
export function isWatchable(path) {
|
|
17
19
|
if (path.includes("/.karajan/onboarding/")) return /\.md$/.test(path);
|
|
18
20
|
if (path.includes("/.karajan/plans/")) return /\.json$/.test(path);
|
|
19
21
|
if (path.split("/").some((seg) => SKIP_SEGMENTS.has(seg))) return false;
|
|
20
|
-
return
|
|
22
|
+
return SOURCE_EXTS.has(extname(path).toLowerCase());
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function startWatcher({ projectDir, config, logger = console, debounceMs = DEFAULT_DEBOUNCE_MS, withSources = false } = {}) {
|
package/src/roles/audit-role.js
CHANGED
|
@@ -8,6 +8,7 @@ import { collectOsvFindings } from "../audit/osv-findings.js";
|
|
|
8
8
|
import { collectSemgrepFindings } from "../audit/semgrep-findings.js";
|
|
9
9
|
import { collectCircularDeps } from "../audit/circular-deps.js";
|
|
10
10
|
import { collectDeadExports } from "../audit/dead-exports.js";
|
|
11
|
+
import { collectInjectionFindings } from "../audit/injection-findings.js";
|
|
11
12
|
|
|
12
13
|
function parseDimensions(dimensionsStr) {
|
|
13
14
|
if (!dimensionsStr || dimensionsStr === "all") return null;
|
|
@@ -51,6 +52,7 @@ export class AuditRole extends AgentRole {
|
|
|
51
52
|
const noSemgrep = typeof input === "object" ? Boolean(input?.noSemgrep) : false;
|
|
52
53
|
const noMadge = typeof input === "object" ? Boolean(input?.noMadge) : false;
|
|
53
54
|
const noKnip = typeof input === "object" ? Boolean(input?.noKnip) : false;
|
|
55
|
+
const noInjectionScan = typeof input === "object" ? Boolean(input?.noInjectionScan) : false;
|
|
54
56
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
55
57
|
let basalCost = null;
|
|
56
58
|
let growthDelta = null;
|
|
@@ -61,6 +63,7 @@ export class AuditRole extends AgentRole {
|
|
|
61
63
|
let semgrepFindings = null;
|
|
62
64
|
let circularDeps = null;
|
|
63
65
|
let deadExports = null;
|
|
66
|
+
let injectionFindings = null;
|
|
64
67
|
try {
|
|
65
68
|
basalCost = await measureBasalCost(projectDir);
|
|
66
69
|
const previous = await loadPreviousAudit(projectDir);
|
|
@@ -108,7 +111,12 @@ export class AuditRole extends AgentRole {
|
|
|
108
111
|
deadExports = await collectDeadExports(projectDir, stack, this.config, this.logger);
|
|
109
112
|
} catch { /* knip is best-effort */ }
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
if (!noInjectionScan) {
|
|
115
|
+
try {
|
|
116
|
+
injectionFindings = await collectInjectionFindings(projectDir, this.logger);
|
|
117
|
+
} catch { /* injection scan is best-effort (KJC-TSK-0498) */ }
|
|
118
|
+
}
|
|
119
|
+
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, injectionFindings };
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/**
|
package/src/sonar/scanner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import fsPromises from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import { runCommand } from "../utils/process.js";
|
|
5
6
|
import { sonarUp } from "./manager.js";
|
|
6
7
|
import { resolveSonarProjectKey } from "./project-key.js";
|
|
@@ -10,6 +11,32 @@ import {
|
|
|
10
11
|
resolveSonarCredentials,
|
|
11
12
|
} from "./config-resolver.js";
|
|
12
13
|
|
|
14
|
+
export async function findNativeSonarScanner() {
|
|
15
|
+
const which = await runCommand("sh", ["-c", "command -v sonar-scanner"]);
|
|
16
|
+
if (which.exitCode === 0) {
|
|
17
|
+
const bin = String(which.stdout || "").trim();
|
|
18
|
+
if (bin) return bin;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function pickSonarScanner(scannerConfig = {}, deps = {}) {
|
|
24
|
+
const find = deps.findNativeSonarScanner || findNativeSonarScanner;
|
|
25
|
+
const arch = deps.arch || os.arch();
|
|
26
|
+
const forced = String(scannerConfig.command || "").trim().toLowerCase();
|
|
27
|
+
if (forced === "docker") return { type: "docker" };
|
|
28
|
+
if (forced === "native" || forced === "sonar-scanner" || forced.endsWith("/sonar-scanner")) {
|
|
29
|
+
const bin = (forced === "native" || forced === "sonar-scanner") ? await find() : forced;
|
|
30
|
+
if (bin) return { type: "native", bin };
|
|
31
|
+
return { type: "docker", reason: "native sonar-scanner not found on PATH" };
|
|
32
|
+
}
|
|
33
|
+
if (arch === "arm64") {
|
|
34
|
+
const bin = await find();
|
|
35
|
+
if (bin) return { type: "native", bin, reason: "ARM64 host — preferring native binary" };
|
|
36
|
+
}
|
|
37
|
+
return { type: "docker" };
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export function buildScannerOpts(projectKey, scanner = {}) {
|
|
14
41
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
15
42
|
if (scanner.sources) opts.push(`-Dsonar.sources=${scanner.sources}`);
|
|
@@ -236,31 +263,39 @@ export async function runSonarScan(config, projectKey = null) {
|
|
|
236
263
|
...coverage.scannerPatch
|
|
237
264
|
});
|
|
238
265
|
|
|
239
|
-
const
|
|
240
|
-
"run",
|
|
241
|
-
"--rm",
|
|
242
|
-
"-v",
|
|
243
|
-
`${process.cwd()}:/usr/src`,
|
|
244
|
-
...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
|
|
245
|
-
...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
|
|
246
|
-
"-e", "SONAR_HOST_URL",
|
|
247
|
-
"-e", "SONAR_TOKEN",
|
|
248
|
-
"-e", "SONAR_SCANNER_OPTS",
|
|
249
|
-
"sonarsource/sonar-scanner-cli"
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
// Pass secrets via process env, not CLI args — invisible in `ps aux`
|
|
266
|
+
const pick = await pickSonarScanner(sonarConfig.scanner);
|
|
253
267
|
const env = {
|
|
254
268
|
...process.env,
|
|
255
|
-
SONAR_HOST_URL: host,
|
|
269
|
+
SONAR_HOST_URL: pick.type === "native" ? rawHost : host,
|
|
256
270
|
SONAR_TOKEN: token || "",
|
|
257
271
|
SONAR_SCANNER_OPTS: buildScannerOpts(effectiveProjectKey, scannerConfig)
|
|
258
272
|
};
|
|
259
273
|
|
|
260
|
-
|
|
274
|
+
let cmd, args;
|
|
275
|
+
if (pick.type === "native") {
|
|
276
|
+
cmd = pick.bin;
|
|
277
|
+
args = [];
|
|
278
|
+
} else {
|
|
279
|
+
cmd = "docker";
|
|
280
|
+
args = [
|
|
281
|
+
"run",
|
|
282
|
+
"--rm",
|
|
283
|
+
"-v",
|
|
284
|
+
`${process.cwd()}:/usr/src`,
|
|
285
|
+
...(isLocalHost ? ["--add-host", "host.docker.internal:host-gateway"] : []),
|
|
286
|
+
...(isLocalHost || isExternalSonar ? [] : ["--network", sonarNetwork]),
|
|
287
|
+
"-e", "SONAR_HOST_URL",
|
|
288
|
+
"-e", "SONAR_TOKEN",
|
|
289
|
+
"-e", "SONAR_SCANNER_OPTS",
|
|
290
|
+
"sonarsource/sonar-scanner-cli"
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = await runCommand(cmd, args, { timeout: scannerTimeout, env });
|
|
261
295
|
return {
|
|
262
296
|
ok: result.exitCode === 0,
|
|
263
297
|
projectKey: effectiveProjectKey,
|
|
298
|
+
scanner: pick.type,
|
|
264
299
|
stdout: result.stdout,
|
|
265
300
|
stderr: result.stderr,
|
|
266
301
|
exitCode: result.exitCode
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// KJC-TSK-0476 — Multi-stack manifest sniffer. Sin libs externas: JSON.parse
|
|
2
|
+
// para package.json, regex para TOML y go.mod. Lo consumen audit (basal-cost)
|
|
3
|
+
// y, vía PR-D, el onboarder.
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
async function readMaybe(file) {
|
|
8
|
+
try { return await fs.readFile(file, "utf8"); } catch { return null; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function sniffNode(dir) {
|
|
12
|
+
const raw = await readMaybe(path.join(dir, "package.json"));
|
|
13
|
+
if (!raw) return null;
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(raw);
|
|
16
|
+
return { stack: "node", manifest: "package.json", name: pkg.name || null,
|
|
17
|
+
deps: Object.keys(pkg.dependencies || {}).length,
|
|
18
|
+
devDeps: Object.keys(pkg.devDependencies || {}).length };
|
|
19
|
+
} catch {
|
|
20
|
+
return { stack: "node", manifest: "package.json", name: null, deps: 0, devDeps: 0, malformed: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Cuerpo TOML de `[section]` hasta el siguiente `\n[` o EOF. Lookahead con `/m`
|
|
25
|
+
// cambia el significado de `$` y producía capturas vacías.
|
|
26
|
+
function tomlSection(toml, section) {
|
|
27
|
+
const i = toml.indexOf(`[${section}]`);
|
|
28
|
+
if (i === -1) return "";
|
|
29
|
+
const rest = toml.slice(i + section.length + 2);
|
|
30
|
+
const n = rest.search(/\n\[/);
|
|
31
|
+
return n === -1 ? rest : rest.slice(0, n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEP_LINE_RE = /^\s*[A-Za-z0-9_-]+\s*=/;
|
|
35
|
+
const countDepLines = (block, excludePython) => block.split("\n")
|
|
36
|
+
.filter((l) => DEP_LINE_RE.test(l) && !(excludePython && /^\s*python\s*=/.test(l))).length;
|
|
37
|
+
|
|
38
|
+
async function sniffPython(dir) {
|
|
39
|
+
const py = await readMaybe(path.join(dir, "pyproject.toml"));
|
|
40
|
+
if (py) {
|
|
41
|
+
const name = /^name\s*=\s*["']([^"']+)["']/m.exec(py)?.[1] || null;
|
|
42
|
+
const projectDeps = /\[project\][\s\S]*?dependencies\s*=\s*\[([^\]]*)\]/m.exec(py)?.[1] || "";
|
|
43
|
+
const projectCount = projectDeps.split(",").map((s) => s.trim()).filter(Boolean).length;
|
|
44
|
+
const poetryCount = countDepLines(tomlSection(py, "tool.poetry.dependencies"), true);
|
|
45
|
+
return { stack: "python", manifest: "pyproject.toml", name, deps: projectCount + poetryCount, devDeps: 0 };
|
|
46
|
+
}
|
|
47
|
+
const req = await readMaybe(path.join(dir, "requirements.txt"));
|
|
48
|
+
if (!req) return null;
|
|
49
|
+
const lines = req.split("\n").map((l) => l.replace(/#.*$/, "").trim()).filter(Boolean);
|
|
50
|
+
return { stack: "python", manifest: "requirements.txt", name: null, deps: lines.length, devDeps: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function sniffRust(dir) {
|
|
54
|
+
const toml = await readMaybe(path.join(dir, "Cargo.toml"));
|
|
55
|
+
if (!toml) return null;
|
|
56
|
+
return { stack: "rust", manifest: "Cargo.toml",
|
|
57
|
+
name: /^name\s*=\s*["']([^"']+)["']/m.exec(toml)?.[1] || null,
|
|
58
|
+
deps: countDepLines(tomlSection(toml, "dependencies")),
|
|
59
|
+
devDeps: countDepLines(tomlSection(toml, "dev-dependencies")) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function sniffGo(dir) {
|
|
63
|
+
const mod = await readMaybe(path.join(dir, "go.mod"));
|
|
64
|
+
if (!mod) return null;
|
|
65
|
+
const block = /require\s*\(([\s\S]*?)\)/m.exec(mod)?.[1] || "";
|
|
66
|
+
const blockCount = block.split("\n").filter((l) => /\S/.test(l) && !l.trim().startsWith("//")).length;
|
|
67
|
+
const singleCount = mod.split("\n").filter((l) => /^require\s+\S+\s+\S+/.test(l)).length;
|
|
68
|
+
return { stack: "go", manifest: "go.mod",
|
|
69
|
+
name: /^module\s+(\S+)/m.exec(mod)?.[1] || null,
|
|
70
|
+
deps: blockCount + singleCount, devDeps: 0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function sniffManifests(dir) {
|
|
74
|
+
const r = await Promise.all([sniffNode(dir), sniffPython(dir), sniffRust(dir), sniffGo(dir)]);
|
|
75
|
+
return r.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function summariseStack(manifests) {
|
|
79
|
+
if (!manifests.length) return { primary: null, multi: false, manifests: [] };
|
|
80
|
+
const ranked = [...manifests].sort((a, b) => (b.deps + b.devDeps) - (a.deps + a.devDeps));
|
|
81
|
+
return { primary: ranked[0].stack, multi: manifests.length > 1, manifests };
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// KJ_SIMULATE_QUOTA — testing hook for the standby flow. See
|
|
2
|
+
// docs/quota-simulation.md. KJC-TSK-0493.
|
|
3
|
+
|
|
4
|
+
let _warned = false;
|
|
5
|
+
|
|
6
|
+
export function parseQuotaSimulationConfig(env = process.env) {
|
|
7
|
+
const trigger = env.KJ_SIMULATE_QUOTA;
|
|
8
|
+
if (!trigger) return null;
|
|
9
|
+
const m = /^after-iter-(\d+)$/i.exec(String(trigger).trim());
|
|
10
|
+
if (!m) {
|
|
11
|
+
if (!_warned) {
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.warn(`[KJ_SIMULATE_QUOTA] Ignoring malformed value "${trigger}". Expected "after-iter-<N>".`);
|
|
14
|
+
_warned = true;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
afterIter: Number.parseInt(m[1], 10),
|
|
20
|
+
resetIso: env.KJ_SIMULATE_QUOTA_RESET || new Date(Date.now() + 3600_000).toISOString(),
|
|
21
|
+
agent: String(env.KJ_SIMULATE_QUOTA_AGENT || "claude").toLowerCase(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SYNTHETIC_MSG = {
|
|
26
|
+
claude: (iso) => `[KJ_SIMULATE_QUOTA] Claude Pro usage limit reached. weekly limit. resets at ${iso}`,
|
|
27
|
+
codex: () => `[KJ_SIMULATE_QUOTA] You have exceeded your current quota. retry after 60 seconds`,
|
|
28
|
+
gemini: () => `[KJ_SIMULATE_QUOTA] Resource exhausted: quota exceeded. retry after 60 seconds`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function buildSyntheticQuotaMessage(agent, resetIso) {
|
|
32
|
+
return (SYNTHETIC_MSG[agent] || SYNTHETIC_MSG.claude)(resetIso);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldSimulateQuota({ iteration, agent }, env = process.env) {
|
|
36
|
+
const cfg = parseQuotaSimulationConfig(env);
|
|
37
|
+
if (!cfg) return null;
|
|
38
|
+
if (Number(iteration) < cfg.afterIter) return null;
|
|
39
|
+
const a = String(agent || "").toLowerCase();
|
|
40
|
+
if (cfg.agent !== "any" && cfg.agent !== a) return null;
|
|
41
|
+
return { resetIso: cfg.resetIso, agent: a || cfg.agent };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mutates execResult: forces ok=false and prepends the synthetic quota
|
|
45
|
+
// line to result.error so the existing detectRateLimit branch fires.
|
|
46
|
+
export function applyQuotaSimulation(execResult, { iteration, agent }, env = process.env) {
|
|
47
|
+
const fire = shouldSimulateQuota({ iteration, agent }, env);
|
|
48
|
+
if (!fire || !execResult || typeof execResult !== "object") return false;
|
|
49
|
+
if (!execResult.result || typeof execResult.result !== "object") execResult.result = {};
|
|
50
|
+
execResult.result.error = `${buildSyntheticQuotaMessage(fire.agent, fire.resetIso)}\n${execResult.result.error || ""}`;
|
|
51
|
+
execResult.ok = false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Test seam — reset the one-shot warning between unit tests.
|
|
56
|
+
export function _resetSimulatorWarning() { _warned = false; }
|
package/src/utils/telemetry.js
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
const TELEMETRY_ENDPOINT = "https://karajan-code.web.app/api/telemetry";
|
|
1
|
+
export const TELEMETRY_ENDPOINT = "https://karajan-code.web.app/api/telemetry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the exact JSON payload that would be POSTed for an event.
|
|
5
|
+
* Pure function: deterministic given (eventName, data) — exposed so
|
|
6
|
+
* `kj telemetry preview` can render the literal bytes the backend
|
|
7
|
+
* receives without performing any network call.
|
|
8
|
+
*
|
|
9
|
+
* The `data.version` field is folded into `v`; the rest of the
|
|
10
|
+
* event-specific keys ride through `...data` after the common ones,
|
|
11
|
+
* matching the order sent over the wire.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} eventName
|
|
14
|
+
* @param {object} [data]
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
export function buildTelemetryPayload(eventName, data = {}) {
|
|
18
|
+
const { version, ...rest } = data;
|
|
19
|
+
return {
|
|
20
|
+
event: eventName,
|
|
21
|
+
v: version || "unknown",
|
|
22
|
+
os: process.platform,
|
|
23
|
+
node: process.version,
|
|
24
|
+
ts: Date.now(),
|
|
25
|
+
...rest
|
|
26
|
+
};
|
|
27
|
+
}
|
|
2
28
|
|
|
3
29
|
/**
|
|
4
30
|
* Send an anonymous telemetry event. Non-blocking, fire-and-forget.
|
|
@@ -12,14 +38,7 @@ export async function sendTelemetryEvent(eventName, data, config) {
|
|
|
12
38
|
if (!isTelemetryEnabled(config)) return;
|
|
13
39
|
|
|
14
40
|
try {
|
|
15
|
-
const payload =
|
|
16
|
-
event: eventName,
|
|
17
|
-
v: data.version || "unknown",
|
|
18
|
-
os: process.platform,
|
|
19
|
-
node: process.version,
|
|
20
|
-
ts: Date.now(),
|
|
21
|
-
...data
|
|
22
|
-
};
|
|
41
|
+
const payload = buildTelemetryPayload(eventName, data);
|
|
23
42
|
const controller = new AbortController();
|
|
24
43
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
25
44
|
try {
|
|
@@ -48,12 +67,15 @@ export async function sendTelemetryEvent(eventName, data, config) {
|
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
|
-
* Check if telemetry is enabled (opt-
|
|
52
|
-
*
|
|
70
|
+
* Check if telemetry is enabled (opt-in model).
|
|
71
|
+
*
|
|
72
|
+
* Returns true ONLY when the user explicitly answered "yes" in the
|
|
73
|
+
* `kj init` wizard and we persisted `telemetry: true` to their config.
|
|
74
|
+
* Anything else — undefined, null, missing key, false — disables sending.
|
|
53
75
|
*
|
|
54
76
|
* @param {object} [config] - Karajan config
|
|
55
77
|
* @returns {boolean}
|
|
56
78
|
*/
|
|
57
79
|
export function isTelemetryEnabled(config) {
|
|
58
|
-
return config?.telemetry
|
|
80
|
+
return config?.telemetry === true;
|
|
59
81
|
}
|
|
@@ -14,7 +14,10 @@ If the project is greenfield (no git, no configs, empty tree) emit
|
|
|
14
14
|
|
|
15
15
|
## Input
|
|
16
16
|
|
|
17
|
-
JSON bundle with `projectDir`, `stack`, `
|
|
17
|
+
JSON bundle with `projectDir`, `stack`, `manifests` (per-stack: node /
|
|
18
|
+
python / rust / go with deps count), `frameworksMultiLang` (Django / Flask /
|
|
19
|
+
FastAPI / Axum / Actix / Rocket / Gin / Echo / Fiber + Rust crate type + Go
|
|
20
|
+
module path + canonical layout dirs), `tree`, `git` (or null), `configs`,
|
|
18
21
|
`adrs`. Slots may be null/empty.
|
|
19
22
|
|
|
20
23
|
## Output
|
|
@@ -24,6 +27,8 @@ Markdown only — skip any section without data:
|
|
|
24
27
|
```markdown
|
|
25
28
|
# Architecture Brief — <projectDir>
|
|
26
29
|
## Stack
|
|
30
|
+
## Manifests # only when manifests has entries beyond node
|
|
31
|
+
## Frameworks # only when frameworksMultiLang has entries
|
|
27
32
|
## Layout
|
|
28
33
|
## Configs
|
|
29
34
|
## Git history
|
|
@@ -32,5 +37,9 @@ Markdown only — skip any section without data:
|
|
|
32
37
|
## Recommendations for new work
|
|
33
38
|
```
|
|
34
39
|
|
|
40
|
+
For non-Node stacks (`frameworksMultiLang[].stack` in {python, rust, go}),
|
|
41
|
+
mention the detected framework(s), crate type or module path, and canonical
|
|
42
|
+
layout dirs (apps/, cmd/, internal/, src/, tests/) when present.
|
|
43
|
+
|
|
35
44
|
Be conservative: only assert what the bundle shows. "Likely" / "appears to"
|
|
36
45
|
are fine; invention is not. Aim for ~40–80 lines total.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Vendored tree-sitter grammars
|
|
2
|
+
|
|
3
|
+
WASM binaries used by `src/lang/tree-sitter-loader.js`. Vendored (rather
|
|
4
|
+
than pulled as runtime deps) so the npm package and the SEA binary ship
|
|
5
|
+
exactly the grammars we support — adding all of `@vscode/tree-sitter-wasm`
|
|
6
|
+
would balloon the install by ~22 MB for languages we never chunk.
|
|
7
|
+
|
|
8
|
+
Source: extracted from `@vscode/tree-sitter-wasm@0.3.1`. That package
|
|
9
|
+
re-builds upstream `tree-sitter-*` grammars against the tree-sitter
|
|
10
|
+
0.25.x ABI, which matches the runtime `web-tree-sitter@0.26.x` we depend
|
|
11
|
+
on. Older prebuilds (e.g. `tree-sitter-wasms@0.1.13`) target an older ABI
|
|
12
|
+
and fail to load.
|
|
13
|
+
|
|
14
|
+
Re-extract with:
|
|
15
|
+
|
|
16
|
+
npm pack @vscode/tree-sitter-wasm
|
|
17
|
+
tar -xzf vscode-tree-sitter-wasm-*.tgz
|
|
18
|
+
cp package/wasm/tree-sitter-python.wasm vendor/tree-sitter-grammars/python.wasm
|
|
19
|
+
cp package/wasm/tree-sitter-rust.wasm vendor/tree-sitter-grammars/rust.wasm
|
|
20
|
+
cp package/wasm/tree-sitter-go.wasm vendor/tree-sitter-grammars/go.wasm
|
|
21
|
+
cp package/wasm/tree-sitter-java.wasm vendor/tree-sitter-grammars/java.wasm
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|