karajan-code 4.3.1 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/commands/env.js +39 -3
- package/src/commands/rag.js +4 -4
- package/src/config/defaults.js +5 -1
- package/src/mcp/handlers/rag-handler.js +3 -3
- package/src/orchestrator/stages/rag-context-stage.js +2 -2
- package/src/rag/auto-update.js +2 -2
- package/src/rag/governed-embedder.js +39 -0
- package/src/rag/onnx-fallback.js +69 -0
- package/src/rag/watcher.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -109,11 +109,13 @@
|
|
|
109
109
|
"helmet": "^8.1.0",
|
|
110
110
|
"js-yaml": "^4.2.0",
|
|
111
111
|
"karajan-core": "^1.0.0",
|
|
112
|
+
"karajan-rag": "^1.2.0",
|
|
112
113
|
"knip": "^6.15.0",
|
|
113
114
|
"madge": "^8.0.0",
|
|
114
115
|
"sqlite-vec": "^0.1.9",
|
|
115
116
|
"valibot": "^1.4.1",
|
|
116
|
-
"web-tree-sitter": "^0.26.9"
|
|
117
|
+
"web-tree-sitter": "^0.26.9",
|
|
118
|
+
"yaml": "^2.9.0"
|
|
117
119
|
},
|
|
118
120
|
"devDependencies": {
|
|
119
121
|
"@eslint/js": "^10.0.1",
|
package/src/commands/env.js
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { installPlaybook } from "../environment/playbook.js";
|
|
8
8
|
import { renderBrief, listBriefs } from "../environment/briefs.js";
|
|
9
|
-
import {
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { openVecStore, projectSlug, getLastIndexedCommit, dbPath } from "../rag/vec-store.js";
|
|
10
11
|
import { ragIndexCommand } from "./rag.js";
|
|
11
12
|
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
13
|
+
import { onnxConfig, persistOnnxChoice, resetEmptyStore } from "../rag/onnx-fallback.js";
|
|
12
14
|
|
|
13
15
|
function hasRagIndex(config, projectDir) {
|
|
16
|
+
// KJC-BUG-0128: probing must never CREATE the store — openVecStore runs
|
|
17
|
+
// the DDL, and a table born at the default dim (768) breaks the ONNX
|
|
18
|
+
// fallback (384) later. No file → no index, without side effects.
|
|
19
|
+
if (!existsSync(dbPath())) return false;
|
|
14
20
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
15
21
|
try { return Boolean(getLastIndexedCommit(db, projectSlug(projectDir))); }
|
|
16
22
|
finally { db.close(); }
|
|
@@ -59,6 +65,35 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
59
65
|
: { tool: `${provider} embedder`, action: "needs-user", reason: `the RAG cannot index: ${why} — check rag.embedder in kj config`, manualUrl: "kj config" };
|
|
60
66
|
console.log(renderPendingBlock([item], { retry: "kj rag index --with-sources (then re-run: kj env install)" }));
|
|
61
67
|
};
|
|
68
|
+
// KJC-TSK-0683: on limited machines (no Ollama, no Docker) the DEFAULT
|
|
69
|
+
// provider falls back to the built-in ONNX embedder instead of blocking.
|
|
70
|
+
// Only for the first index and only when the user didn't pick a provider
|
|
71
|
+
// explicitly — the election is persisted (dims differ, it must stick).
|
|
72
|
+
const explicitProvider = Boolean(config?.rag?.embedder?.provider);
|
|
73
|
+
const tryOnnxFallback = async (why) => {
|
|
74
|
+
if (explicitProvider) return false;
|
|
75
|
+
console.log(`⚠ default embedder unavailable (${why}) — trying the built-in ONNX embedder (no install needed)…`);
|
|
76
|
+
try {
|
|
77
|
+
// KJC-BUG-0128: an empty store may carry a vec table at the wrong
|
|
78
|
+
// dim (the old probe created it at 768) — reset it so ONNX (384)
|
|
79
|
+
// can index. A store with data is never touched.
|
|
80
|
+
if (!resetEmptyStore()) {
|
|
81
|
+
console.log(" the vector store already has data at another dimension — not switching automatically");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const totals = await ragIndexCommand({ config: onnxConfig(config), logger, flags: { withSources: true } });
|
|
85
|
+
if ((totals?.indexed ?? 0) > 0) {
|
|
86
|
+
const p = await persistOnnxChoice(projectDir);
|
|
87
|
+
console.log(`✓ RAG indexed with the built-in ONNX embedder — persisted in ${p}`);
|
|
88
|
+
console.log(" To move to Ollama later: install it, remove the rag.embedder block from .karajan/kj.config.yml,");
|
|
89
|
+
console.log(" delete the store (~/.karajan/rag.db) and run: kj rag index --with-sources");
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
} catch (fallbackErr) {
|
|
93
|
+
console.log(` ONNX fallback also failed: ${fallbackErr.message}`);
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
62
97
|
try {
|
|
63
98
|
if (hasRagIndex(config, projectDir)) {
|
|
64
99
|
console.log("✓ RAG index present");
|
|
@@ -66,11 +101,12 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
66
101
|
console.log("⏳ no RAG index for this project — building it (first time only)…");
|
|
67
102
|
const totals = await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
68
103
|
if ((totals?.indexed ?? 0) === 0 && (totals?.files ?? 0) > 0) {
|
|
69
|
-
|
|
104
|
+
const why = `0 of ${totals.files} files indexed — is the embedder running?`;
|
|
105
|
+
if (!(await tryOnnxFallback(why))) blockRag(why);
|
|
70
106
|
}
|
|
71
107
|
}
|
|
72
108
|
} catch (err) {
|
|
73
|
-
blockRag(err.message);
|
|
109
|
+
if (!(await tryOnnxFallback(err.message))) blockRag(err.message);
|
|
74
110
|
}
|
|
75
111
|
}
|
|
76
112
|
if (result.exitCode !== PENDING_EXIT_CODE) {
|
package/src/commands/rag.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// kj rag query <text> [--scope plans|code|onboarding|all] [--top-k N] [--json]
|
|
4
4
|
// Closes the v2.22.0 RAG MVP end-to-end from the terminal.
|
|
5
5
|
import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
|
-
import {
|
|
6
|
+
import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
8
|
import { query } from "../rag/retriever.js";
|
|
9
9
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
@@ -19,7 +19,7 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
19
19
|
const db = openDb(config);
|
|
20
20
|
try {
|
|
21
21
|
const slug = projectSlug(projectDir);
|
|
22
|
-
const embedder =
|
|
22
|
+
const embedder = makeGovernedEmbedder(config);
|
|
23
23
|
const sinceFlag = flags.since;
|
|
24
24
|
// KJC-TSK-0455 — `--since auto` resolves to the last commit we indexed;
|
|
25
25
|
// an explicit ref is honoured as-is. Without a baseline (first-time
|
|
@@ -103,7 +103,7 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
103
103
|
const alpha = Math.max(0, Math.min(1, Number(flags.alpha) || 0.6));
|
|
104
104
|
const where = flags.where || null;
|
|
105
105
|
const rerankOpts = flags.rerank ? { model: flags.rerankModel } : null;
|
|
106
|
-
const hits = await query(db,
|
|
106
|
+
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
|
|
107
107
|
if (flags.json) {
|
|
108
108
|
process.stdout.write(`${JSON.stringify(hits)}\n`);
|
|
109
109
|
} else {
|
|
@@ -133,7 +133,7 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
|
133
133
|
const topK = Math.max(1, Number(flags.topK) || 10);
|
|
134
134
|
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
135
135
|
const project = flags.project === "all" ? null : (flags.project || detected || null);
|
|
136
|
-
const embedder =
|
|
136
|
+
const embedder = makeGovernedEmbedder(config);
|
|
137
137
|
const runQuery = async (text, k) => query(db, embedder, text, { topK: k, scope: "all", project });
|
|
138
138
|
const report = await runEval(queries, runQuery, { topK, ks: [5, 10] });
|
|
139
139
|
if (flags.json) {
|
package/src/config/defaults.js
CHANGED
|
@@ -216,7 +216,11 @@ const DEFAULTS = {
|
|
|
216
216
|
// delta re-index before every `kj run`; `onCommit` is consulted by
|
|
217
217
|
// `kj rag install-hooks` to decide whether to install the post-merge
|
|
218
218
|
// hook. Both default ON; flip to false to opt out without uninstalling.
|
|
219
|
-
autoUpdate: { onCommit: true, onRun: true }
|
|
219
|
+
autoUpdate: { onCommit: true, onRun: true },
|
|
220
|
+
// KJC-TSK-0682 — sensitivity of the indexed code. Cloud embedders
|
|
221
|
+
// (openai/voyage/cohere/mistral) require an explicit "public"; the
|
|
222
|
+
// safe default blocks them (local ollama/onnx always allowed).
|
|
223
|
+
sensitivity: "internal"
|
|
220
224
|
},
|
|
221
225
|
// Opt-in. Set by the `kj init` wizard (explicit consent prompt in the OS
|
|
222
226
|
// locale). When the key is absent or false, no events are sent. See
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// from src/commands/* are forbidden by the layer-boundaries test
|
|
3
3
|
// (MCP and CLI are peer layers); we hit the pure rag/* modules instead.
|
|
4
4
|
import { countChunks, openVecStore } from "../../rag/vec-store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
6
6
|
import { indexProject } from "../../rag/indexer.js";
|
|
7
7
|
import { query } from "../../rag/retriever.js";
|
|
8
8
|
import { getKarajanHome } from "../../utils/paths.js";
|
|
@@ -25,7 +25,7 @@ export async function handleRagQuery(args, server) {
|
|
|
25
25
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
26
26
|
try {
|
|
27
27
|
if (countChunks(db) === 0) return responseText({ hits: [], empty: true, topK, scope });
|
|
28
|
-
const hits = await query(db,
|
|
28
|
+
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope });
|
|
29
29
|
return responseText({ hits, empty: false, topK, scope });
|
|
30
30
|
} finally { db.close(); }
|
|
31
31
|
} catch (err) {
|
|
@@ -40,7 +40,7 @@ export async function handleRagIndex(args, server) {
|
|
|
40
40
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
41
41
|
try {
|
|
42
42
|
const totals = await indexProject(projectDir, {
|
|
43
|
-
db, embedder:
|
|
43
|
+
db, embedder: makeGovernedEmbedder(config),
|
|
44
44
|
karajanHome: getKarajanHome(), logger: silentLogger,
|
|
45
45
|
withSources: Boolean(args?.withSources),
|
|
46
46
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// never sees an exception from this stage.
|
|
7
7
|
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
8
|
import { openVecStore, countChunks } from "../../rag/vec-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import { makeGovernedEmbedder } from "../../rag/governed-embedder.js";
|
|
10
10
|
import { query } from "../../rag/retriever.js";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TOP_K = 5;
|
|
@@ -52,7 +52,7 @@ export async function runRagContextStage({ config, logger, emitter, eventBase, t
|
|
|
52
52
|
}
|
|
53
53
|
const topK = preload.topK || DEFAULT_TOP_K;
|
|
54
54
|
const scope = preload.scope || DEFAULT_SCOPE;
|
|
55
|
-
const embedder =
|
|
55
|
+
const embedder = makeGovernedEmbedder(config);
|
|
56
56
|
const searchOpts = resolveSearchOpts(config);
|
|
57
57
|
const hits = await query(db, embedder, task, { topK, scope, ...searchOpts });
|
|
58
58
|
if (hits.length === 0) return { skipped: true, reason: "no-hits" };
|
package/src/rag/auto-update.js
CHANGED
|
@@ -8,7 +8,7 @@ import { execa } from "execa";
|
|
|
8
8
|
|
|
9
9
|
import { openVecStore, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
10
10
|
import { indexProjectDelta } from "./indexer.js";
|
|
11
|
-
import {
|
|
11
|
+
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
12
12
|
|
|
13
13
|
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
25
25
|
const since = getLastIndexedCommit(db, slug);
|
|
26
26
|
if (!since || since === head) return { skipped: true, head };
|
|
27
27
|
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}); running delta update`);
|
|
28
|
-
const totals = await indexProjectDelta(projectDir, { db, embedder:
|
|
28
|
+
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger });
|
|
29
29
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
30
30
|
return { ran: true, totals };
|
|
31
31
|
} catch (err) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governed embedder (KJC-TSK-0682 — card filed by the karajan-rag session
|
|
3
|
+
* after its independent security audit closed the same risk there, H2).
|
|
4
|
+
* Chunks of project code must not travel to a cloud embedder without
|
|
5
|
+
* governance: local providers (ollama/onnx) pass through untouched; cloud
|
|
6
|
+
* providers (openai/voyage/cohere/mistral) require an EXPLICIT
|
|
7
|
+
* `rag.sensitivity: public` — the default (`internal`) blocks with an
|
|
8
|
+
* actionable error, never a silent downgrade. Even when allowed, every
|
|
9
|
+
* text is PII-redacted (karajan-rag's audited redactPII) before leaving
|
|
10
|
+
* the machine — defense in depth, not a substitute for the policy.
|
|
11
|
+
*/
|
|
12
|
+
import { redactPII } from "karajan-rag";
|
|
13
|
+
import { makeEmbedder } from "./embedders/factory.js";
|
|
14
|
+
|
|
15
|
+
const LOCAL_PROVIDERS = new Set(["ollama", "onnx"]);
|
|
16
|
+
const redact = (text) => redactPII(String(text)).text;
|
|
17
|
+
|
|
18
|
+
class RedactingEmbedder {
|
|
19
|
+
constructor(inner) { this.inner = inner; }
|
|
20
|
+
get dim() { return this.inner.dim; }
|
|
21
|
+
get model() { return this.inner.model; }
|
|
22
|
+
async embed(text) { return this.inner.embed(redact(text)); }
|
|
23
|
+
async embedBatch(texts) { return this.inner.embedBatch(texts.map(redact)); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Drop-in replacement for makeEmbedder(config) at every kj call site. */
|
|
27
|
+
export function makeGovernedEmbedder(config) {
|
|
28
|
+
const provider = config?.rag?.embedder?.provider || "ollama";
|
|
29
|
+
if (LOCAL_PROVIDERS.has(provider)) return makeEmbedder(config);
|
|
30
|
+
|
|
31
|
+
const sensitivity = config?.rag?.sensitivity || "internal";
|
|
32
|
+
if (sensitivity !== "public") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`embedder "${provider}" sends your code to a cloud provider, but rag.sensitivity is "${sensitivity}". `
|
|
35
|
+
+ `Use a local embedder (rag.embedder.provider: ollama | onnx) or declare rag.sensitivity: public in kj.config.yml.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return new RedactingEmbedder(makeEmbedder(config));
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONNX embedder fallback (KJC-TSK-0683) — limited machines where installing
|
|
3
|
+
* Ollama/Docker is not viable still get a working RAG: the built-in ONNX
|
|
4
|
+
* embedder (all-MiniLM via @huggingface/transformers, in-process, CPU).
|
|
5
|
+
* The choice is PERSISTED in the project config: onnx (384) and ollama
|
|
6
|
+
* (768) have different vector dims, so the election must be sticky —
|
|
7
|
+
* an index can never be written by one and queried by the other.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import { existsSync, rmSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import Database from "better-sqlite3";
|
|
13
|
+
import { parseDocument } from "yaml";
|
|
14
|
+
import { getProjectConfigPath } from "../config/loader.js";
|
|
15
|
+
import { dbPath } from "./vec-store.js";
|
|
16
|
+
|
|
17
|
+
const ONNX_EMBEDDER = { provider: "onnx", dim: 384 };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* KJC-BUG-0128: the hasRagIndex probe could leave behind an EMPTY store
|
|
21
|
+
* whose vec table was created at the default dim (768) — ONNX inserts (384)
|
|
22
|
+
* then failed on every chunk. Before the fallback indexes, delete an empty
|
|
23
|
+
* store (plus WAL siblings) so it is recreated at the right dim. A store
|
|
24
|
+
* that already HAS chunks is never touched — switching dims there would
|
|
25
|
+
* destroy other projects' indexes.
|
|
26
|
+
*/
|
|
27
|
+
export function resetEmptyStore() {
|
|
28
|
+
const storePath = dbPath();
|
|
29
|
+
if (!existsSync(storePath)) return true;
|
|
30
|
+
// Read-only, schema-agnostic inspection: no DDL, no vec extension, no
|
|
31
|
+
// dimension assumptions — the store is only ever OPENED to be counted.
|
|
32
|
+
const db = new Database(storePath, { readonly: true });
|
|
33
|
+
try {
|
|
34
|
+
const hasChunksTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'").get();
|
|
35
|
+
if (hasChunksTable && db.prepare("SELECT COUNT(*) AS n FROM chunks").get().n > 0) return false;
|
|
36
|
+
} finally {
|
|
37
|
+
db.close();
|
|
38
|
+
}
|
|
39
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(storePath + suffix, { force: true });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The same config, with the embedder swapped to the built-in ONNX. */
|
|
44
|
+
export function onnxConfig(config) {
|
|
45
|
+
return {
|
|
46
|
+
...config,
|
|
47
|
+
rag: { ...(config?.rag || {}), embedder: { ...(config?.rag?.embedder || {}), ...ONNX_EMBEDDER } },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Persist the ONNX election in the PROJECT config file. Surgical edit via
|
|
53
|
+
* the yaml Document API: only rag.embedder.{provider,dim} change — the
|
|
54
|
+
* user's comments, ordering and every other key survive untouched.
|
|
55
|
+
*/
|
|
56
|
+
export async function persistOnnxChoice(projectDir) {
|
|
57
|
+
const configPath = getProjectConfigPath(projectDir);
|
|
58
|
+
let source = "";
|
|
59
|
+
try {
|
|
60
|
+
source = await fs.readFile(configPath, "utf8");
|
|
61
|
+
} catch { /* no project config yet — create it */ }
|
|
62
|
+
const doc = parseDocument(source || "{}");
|
|
63
|
+
for (const [key, value] of Object.entries(ONNX_EMBEDDER)) {
|
|
64
|
+
doc.setIn(["rag", "embedder", key], value);
|
|
65
|
+
}
|
|
66
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
67
|
+
await fs.writeFile(configPath, doc.toString(), "utf8");
|
|
68
|
+
return configPath;
|
|
69
|
+
}
|
package/src/rag/watcher.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
|
5
5
|
import { extname, join } from "node:path";
|
|
6
6
|
import chokidar from "chokidar";
|
|
7
7
|
import { openVecStore, deleteChunksBySource, projectSlug } from "./vec-store.js";
|
|
8
|
-
import {
|
|
8
|
+
import { makeGovernedEmbedder } from "./governed-embedder.js";
|
|
9
9
|
import { indexFile } from "./indexer.js";
|
|
10
10
|
import { getKarajanHome } from "../utils/paths.js";
|
|
11
11
|
import { getAllCodeExtensions } from "../lang/registry.js";
|
|
@@ -27,7 +27,7 @@ export function startWatcher({ projectDir, config, logger = console, debounceMs
|
|
|
27
27
|
const slug = projectSlug(projectDir);
|
|
28
28
|
const dim = config?.rag?.embedder?.dim || 768;
|
|
29
29
|
const db = openVecStore({ dim });
|
|
30
|
-
const embedder =
|
|
30
|
+
const embedder = makeGovernedEmbedder(config);
|
|
31
31
|
const paths = [join(getKarajanHome(), "onboarding"), join(getKarajanHome(), "plans")];
|
|
32
32
|
if (withSources) paths.push(projectDir);
|
|
33
33
|
const watcher = chokidar.watch(paths, { ignoreInitial: true, persistent: true });
|