knosky 0.3.0 → 0.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/CHANGELOG.md +25 -0
- package/README.md +8 -7
- package/core/churn.mjs +20 -0
- package/core/contract.mjs +1 -1
- package/core/edges.mjs +44 -0
- package/core/fs-indexer.mjs +52 -13
- package/core/retrieve.mjs +10 -0
- package/mcp/server.mjs +18 -2
- package/package.json +1 -1
- package/renderer/city.template.html +25 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KnoSky. Versions are git-tagged on this repo.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-06-29 — Security review fixes
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
- **The fail-closed secret scan now actually fails closed.** It previously ran *after* scrubbing, so a secret in a title/heading/summary was silently redacted instead of blocking the build. It now scans the **un-scrubbed** projections before serialization and blocks (the post-scrub residual is kept as a defense-in-depth gap detector). A secret in a heading now stops the build.
|
|
9
|
+
- **Embedded mode is fail-closed.** The postMessage bridge now requires a non-empty `window.__KC_ALLOWED_ORIGINS` allowlist (the bridge is disabled if it is empty), only accepts messages from the embedding parent frame, and never broadcasts readiness to `"*"`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `test/security-fixtures.mjs` — pure-Node regression tests (secret block, ignore rules, XSS escaping, embed fail-closed). Run `node test/security-fixtures.mjs`.
|
|
13
|
+
- `.github/workflows/ci.yml` — runs the fixtures + `npm audit` on every PR.
|
|
14
|
+
- Root `package-lock.json` for reproducible `npx` / `npm ci` installs.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- README: removed a "zero data risk" line that contradicted PRIVACY.md.
|
|
18
|
+
|
|
19
|
+
## [0.4.0] - 2026-06-29 — File Connections + churn (code-intel)
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **File Connections:** import/dependency edges drawn as roads between buildings — select a file to see what it imports and what imports it. Per-language import scan over a bounded prefix; specifiers are resolved to repo files and then discarded (file-to-file edges only).
|
|
23
|
+
- **Churn heat:** recently-changed files glow on the map, from `git log` (per-file commit count only).
|
|
24
|
+
- **MCP `kc_related`:** ask your assistant "what connects to this file?" — out-edges, in-edges, and churn, with citations.
|
|
25
|
+
- Flags: `--no-graph`, `--no-churn` to omit either signal.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
- Strictly **file-level metadata** (decision D-155): no symbol names, no ASTs, no code bodies, no commit messages/diffs. KnoSky maps how files connect; it does not analyze your code.
|
|
29
|
+
|
|
5
30
|
## [0.3.0] - 2026-06-29 — One-command launcher (npx)
|
|
6
31
|
|
|
7
32
|
### Added
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Your project grows faster than anyone can hold in their head. Hundreds of files,
|
|
|
21
21
|
- **See your whole project in one screen.** Instead of scrolling a file tree, you see the *shape* of everything — which areas are big, how they connect, where the gaps are. New collaborators get oriented in minutes, not weeks.
|
|
22
22
|
- **Find anything in seconds.** Search the city — or ask your assistant *"where does auth live / what did we decide about billing"* — and jump straight to the **live file**.
|
|
23
23
|
- **Your AI answers from YOUR source, with citations.** Connect it to Claude / Cursor / VS Code / Gemini and your assistant stops guessing about your codebase — it cites the real file, every time.
|
|
24
|
-
- **Zero setup tax,
|
|
24
|
+
- **Zero setup tax, local-first by default.** Point it at a folder → a city in under a minute. Deterministic, **$0 tokens** to keep fresh, **nothing ever leaves your machine**, and a fail-closed secret scan before you share. (KnoSky does not claim "zero data risk" — see [PRIVACY.md](./PRIVACY.md).)
|
|
25
25
|
|
|
26
26
|
**Net:** faster comprehension, reliable recall of your own knowledge, and a grounded AI — without giving up privacy or paying a cent.
|
|
27
27
|
|
|
@@ -32,14 +32,13 @@ Developers, founders, and architects sitting on a sprawling repo or knowledge ba
|
|
|
32
32
|
|
|
33
33
|
## How it works
|
|
34
34
|
|
|
35
|
-
**Quickstart:**
|
|
35
|
+
**Quickstart — one command:**
|
|
36
36
|
```bash
|
|
37
|
-
|
|
38
|
-
node bin/knosky.mjs /path/to/your/repo
|
|
37
|
+
npx knosky .
|
|
39
38
|
```
|
|
40
|
-
Indexes the folder, opens the city, prints the MCP config for your AI assistant (Claude Code / Claude Desktop / Cursor / VS Code) plus a few starter prompts, and starts the local connector. Flags: `--no-open`, `--no-serve`.
|
|
39
|
+
Indexes the current folder, opens the city, prints the MCP config for your AI assistant (Claude Code / Claude Desktop / Cursor / VS Code) plus a few starter prompts, and starts the local connector. Point it anywhere with `npx knosky /path/to/your/repo`. Flags: `--no-open`, `--no-serve`.
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
Prefer a clone? `git clone https://github.com/SathiaAI/knosky && cd knosky && npm install && node bin/knosky.mjs .`
|
|
43
42
|
|
|
44
43
|
Want the individual pieces instead? Read on.
|
|
45
44
|
|
|
@@ -68,7 +67,9 @@ Or add to your Claude Desktop / Cursor / VS Code MCP config:
|
|
|
68
67
|
"env": { "KC_CITY": "/abs/path/city-data.json" }
|
|
69
68
|
}
|
|
70
69
|
```
|
|
71
|
-
Then ask: *"search KnoSky for what we decided about authentication."* Read-only tools exposed: `kc_search`, `kc_get_node`, `kc_list_categories`, `kc_get_provenance`.
|
|
70
|
+
Then ask: *"search KnoSky for what we decided about authentication."* Read-only tools exposed: `kc_search`, `kc_get_node`, `kc_list_categories`, `kc_get_provenance`, `kc_related`.
|
|
71
|
+
|
|
72
|
+
**See how your code connects.** Select a file in the city to see its **connections** (roads to the files it imports / that import it) and **churn** (recently-changed files glow). Or ask your assistant *"what connects to src/auth.js?"* — file-level structure only, not code analysis.
|
|
72
73
|
|
|
73
74
|
---
|
|
74
75
|
|
package/core/churn.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Git churn (D-155): per-file commit count (windowed) + last-commit timestamp ONLY.
|
|
2
|
+
// No commit messages, diffs, hunks, authors, or line-level churn retained.
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export function gitChurn(root) {
|
|
6
|
+
const counts = {}, last = {};
|
|
7
|
+
try {
|
|
8
|
+
const out = execSync('git log --since=90.days.ago --name-only --pretty=format:%ct -- .', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 });
|
|
9
|
+
let ts = 0;
|
|
10
|
+
for (const raw of out.split(/\r?\n/)) {
|
|
11
|
+
const line = raw.trim();
|
|
12
|
+
if (!line) continue;
|
|
13
|
+
if (/^\d+$/.test(line)) { ts = parseInt(line, 10); continue; }
|
|
14
|
+
const rel = line.replace(/\\/g, '/');
|
|
15
|
+
counts[rel] = (counts[rel] || 0) + 1;
|
|
16
|
+
if (!last[rel] || ts > last[rel]) last[rel] = ts;
|
|
17
|
+
}
|
|
18
|
+
} catch { /* no git / no history -> empty */ }
|
|
19
|
+
return { counts, last };
|
|
20
|
+
}
|
package/core/contract.mjs
CHANGED
|
@@ -7,7 +7,7 @@ export const SCHEMA_VERSION = '2.0';
|
|
|
7
7
|
// The ONLY node fields that may ever be serialized into the index. Anything else is dropped/flagged.
|
|
8
8
|
export const NODE_FIELD_ALLOWLIST = [
|
|
9
9
|
'id', 'kind', 'title', 'summary', 'category', 'status',
|
|
10
|
-
'fact_date', 'tags', 'headings', 'links', 'provenance', 'visibility', 'sensitive',
|
|
10
|
+
'fact_date', 'tags', 'headings', 'links', 'churn', 'provenance', 'visibility', 'sensitive',
|
|
11
11
|
];
|
|
12
12
|
export const NODE_REQUIRED = ['id', 'kind', 'title', 'category', 'links', 'provenance'];
|
|
13
13
|
export const PROVENANCE_REQUIRED = ['store', 'ref'];
|
package/core/edges.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// File-level import edges (D-155): file-to-file ONLY. Import specifiers are read transiently
|
|
2
|
+
// and discarded after resolution; nothing but the resolved file-to-file edge is ever stored.
|
|
3
|
+
// No symbol names, no raw import lines, no body text. Reviewer rule: only file paths survive.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const PATTERNS = [
|
|
8
|
+
/^\s*import\s+[^'"]*?from\s*['"]([^'"]+)['"]/, // js/ts: import x from 'y'
|
|
9
|
+
/^\s*import\s*['"]([^'"]+)['"]/, // js/ts: import 'y'
|
|
10
|
+
/\brequire\(\s*['"]([^'"]+)['"]\s*\)/, // js: require('y')
|
|
11
|
+
/^\s*export\s+[^'"]*?from\s*['"]([^'"]+)['"]/, // js/ts: export ... from 'y'
|
|
12
|
+
/^\s*from\s+([.\w/]+)\s+import\b/, // python: from x import
|
|
13
|
+
/^\s*#include\s*["<]([^">]+)[">]/, // c/c++: #include "y"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Read up to 8KB / 150 lines, scan for import specifiers, then drop the buffer. Returns specifier strings only.
|
|
17
|
+
export function extractImportSpecifiers(filePath) {
|
|
18
|
+
let txt = '';
|
|
19
|
+
try { const fd = fs.openSync(filePath, 'r'); const buf = Buffer.alloc(8192); const n = fs.readSync(fd, buf, 0, 8192, 0); fs.closeSync(fd); txt = buf.slice(0, n).toString('utf8'); }
|
|
20
|
+
catch { return []; }
|
|
21
|
+
const lines = txt.split(/\r?\n/);
|
|
22
|
+
txt = '';
|
|
23
|
+
const specs = new Set();
|
|
24
|
+
for (let i = 0; i < lines.length && i < 150; i++) {
|
|
25
|
+
for (const re of PATTERNS) { const m = lines[i].match(re); if (m && m[1]) { specs.add(m[1]); break; } }
|
|
26
|
+
}
|
|
27
|
+
return [...specs];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.h', '.cpp', '.hpp', '.cs', '.kt', '.swift'];
|
|
31
|
+
|
|
32
|
+
// Resolve a relative specifier to an indexed repo file (rel path). Returns the target rel or null.
|
|
33
|
+
// Only relative/internal specifiers resolve; bare package names return null (external = no edge).
|
|
34
|
+
export function resolveSpec(fileRel, spec, relSet) {
|
|
35
|
+
if (!spec || !(spec.startsWith('.') || spec.startsWith('/'))) return null;
|
|
36
|
+
const baseDir = path.posix.dirname(fileRel);
|
|
37
|
+
const target = path.posix.normalize(path.posix.join(spec.startsWith('/') ? '.' : baseDir, spec)).replace(/^\.\//, '');
|
|
38
|
+
for (const e of EXTS) {
|
|
39
|
+
if (relSet.has(target + e)) return target + e;
|
|
40
|
+
const idx = path.posix.join(target, 'index' + (e || '.js'));
|
|
41
|
+
if (relSet.has(idx)) return idx;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
package/core/fs-indexer.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import { SCHEMA_VERSION, IGNORE_DEFAULTS, deriveCategories, serializeNode, validateCity, setRedactTerms, findSecrets } from './contract.mjs';
|
|
8
|
+
import { extractImportSpecifiers, resolveSpec } from './edges.mjs';
|
|
9
|
+
import { gitChurn } from './churn.mjs';
|
|
8
10
|
|
|
9
11
|
const args = Object.fromEntries(process.argv.slice(2).map((a, i, arr) => a.startsWith('--') ? [a.slice(2), arr[i + 1]] : []).filter(Boolean));
|
|
10
12
|
const ROOT = args.root && path.resolve(args.root);
|
|
@@ -13,6 +15,8 @@ const MAX = parseInt(args.max || '6000', 10);
|
|
|
13
15
|
const SHARE_SAFE = process.argv.includes('--share-safe');
|
|
14
16
|
const INCLUDE_ABS = process.argv.includes('--include-absolute-root');
|
|
15
17
|
const ALLOW_LEAKS = process.argv.includes('--allow-leaks');
|
|
18
|
+
const NO_GRAPH = process.argv.includes('--no-graph');
|
|
19
|
+
const NO_CHURN = process.argv.includes('--no-churn');
|
|
16
20
|
if (!ROOT || !fs.existsSync(ROOT)) { console.log('usage: node fs-indexer.mjs --root <dir> [--out <file>] [--max N] [--redact a,b]'); process.exit(1); }
|
|
17
21
|
const REDACT = (args.redact || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
18
22
|
if (REDACT.length) setRedactTerms(REDACT);
|
|
@@ -94,6 +98,7 @@ function walk(dir, depth) {
|
|
|
94
98
|
tags: ext ? [ext.slice(1)] : [],
|
|
95
99
|
headings,
|
|
96
100
|
links: [],
|
|
101
|
+
importSpecs: (!NO_GRAPH && readable) ? extractImportSpecifiers(fp) : [],
|
|
97
102
|
provenance: { store: 'fs', ref: rel, source_rev: REV, fetched_at: new Date().toISOString() },
|
|
98
103
|
visibility: 'internal',
|
|
99
104
|
});
|
|
@@ -115,12 +120,42 @@ try {
|
|
|
115
120
|
}
|
|
116
121
|
} catch (e) { /* git unavailable: keep the conservative-parser result */ }
|
|
117
122
|
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
// File Connections (D-155): resolve import specifiers to indexed files; store ONLY file-to-file edges.
|
|
124
|
+
const relSet = new Set(raw.map(n => n.provenance.ref));
|
|
125
|
+
if (!NO_GRAPH) {
|
|
126
|
+
for (const n of raw) {
|
|
127
|
+
const targets = new Set();
|
|
128
|
+
for (const spec of (n.importSpecs || [])) {
|
|
129
|
+
const t = resolveSpec(n.provenance.ref, spec, relSet);
|
|
130
|
+
if (t) { const id = 'fs:' + t; if (id !== n.id) targets.add(id); }
|
|
131
|
+
}
|
|
132
|
+
n.links = [...targets];
|
|
133
|
+
}
|
|
123
134
|
}
|
|
135
|
+
for (const n of raw) delete n.importSpecs; // discard specifiers; only the resolved file-to-file edge survives
|
|
136
|
+
|
|
137
|
+
// Churn overlay (D-155): per-file commit count + last-commit ts only (no messages/diffs/authors/lines).
|
|
138
|
+
if (!NO_CHURN) {
|
|
139
|
+
const ch = gitChurn(ROOT);
|
|
140
|
+
for (const n of raw) {
|
|
141
|
+
const c = ch.counts[n.provenance.ref] || 0;
|
|
142
|
+
if (c > 0) n.churn = { c, t: ch.last[n.provenance.ref] || 0, b: c >= 6 ? 3 : c >= 3 ? 2 : 1 };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- fail-closed secret scan on RAW projections, BEFORE scrubbing (the decisive boundary).
|
|
147
|
+
// Scans exactly the text that will be emitted (title/summary/headings/tags + relative path) in
|
|
148
|
+
// its un-scrubbed form, so detection cannot be defeated by serializeNode()'s scrub. ---
|
|
149
|
+
const _NL = String.fromCharCode(10);
|
|
150
|
+
const rawText = raw.map(n => [
|
|
151
|
+
n.title, n.summary,
|
|
152
|
+
(n.headings || []).join(_NL),
|
|
153
|
+
(n.tags || []).join(' '),
|
|
154
|
+
n.provenance && n.provenance.ref,
|
|
155
|
+
].filter(Boolean).join(_NL)).join(_NL);
|
|
156
|
+
const preHits = findSecrets(rawText);
|
|
157
|
+
const preTotal = preHits.reduce((a, h) => a + h[1], 0);
|
|
158
|
+
const preList = preHits.map(h => h[0] + ':' + h[1]).join(', ');
|
|
124
159
|
|
|
125
160
|
const nodes = raw.map(serializeNode);
|
|
126
161
|
const catIds = [...new Set(nodes.map(n => n.category))].sort();
|
|
@@ -149,21 +184,25 @@ console.log('post-scrub leak hits (emails/keys, should be ~0):', leakHits);
|
|
|
149
184
|
console.log('redacted-path files skipped:', redactedPaths);
|
|
150
185
|
if (flags.length) console.log('flags:', flags.join('; '));
|
|
151
186
|
|
|
152
|
-
// --- fail-closed safe-share audit
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
187
|
+
// --- fail-closed safe-share audit.
|
|
188
|
+
// PRIMARY = pre-scrub detection (preTotal, computed above) -> the real boundary.
|
|
189
|
+
// SECONDARY = post-scrub residual on the emitted artifact -> flags scrub gaps (defense in depth). ---
|
|
190
|
+
const residualHits = findSecrets(blob);
|
|
191
|
+
const residualTotal = residualHits.reduce((a, h) => a + h[1], 0);
|
|
192
|
+
const residualList = residualHits.map(h => h[0] + ':' + h[1]).join(', ');
|
|
156
193
|
if (SHARE_SAFE) {
|
|
157
194
|
console.log('');
|
|
158
195
|
console.log('KnoSky safety report');
|
|
159
196
|
console.log('- Files scanned:', scanned);
|
|
160
197
|
console.log('- Files skipped (redact-path):', redactedPaths);
|
|
161
|
-
console.log('- Secrets found:',
|
|
198
|
+
console.log('- Secrets found (pre-scrub):', preTotal, preTotal ? '[' + preList + ']' : '');
|
|
199
|
+
console.log('- Residual after scrub (should be 0):', residualTotal, residualTotal ? '[' + residualList + ']' : '');
|
|
162
200
|
console.log('- Absolute path in output:', INCLUDE_ABS ? 'INCLUDED' : 'removed (basename only)');
|
|
163
|
-
console.log('- Risk level:',
|
|
201
|
+
console.log('- Risk level:', (preTotal || residualTotal) ? 'HIGH' : 'Low');
|
|
164
202
|
}
|
|
165
|
-
if (
|
|
166
|
-
|
|
203
|
+
if ((preTotal || residualTotal) && !ALLOW_LEAKS) {
|
|
204
|
+
const why = preTotal ? preList : residualList;
|
|
205
|
+
console.error('BLOCKED: ' + (preTotal || residualTotal) + ' secret-like value(s) detected [' + why + ']. Nothing written.');
|
|
167
206
|
console.error('Add the offending paths to .kcignore or pass --redact <term>, then rebuild. Override with --allow-leaks (not recommended).');
|
|
168
207
|
process.exit(1);
|
|
169
208
|
}
|
package/core/retrieve.mjs
CHANGED
|
@@ -51,3 +51,13 @@ export function getProvenance(ctx, id) {
|
|
|
51
51
|
if (!n) return null;
|
|
52
52
|
return { id, title: n.title, provenance: n.provenance, links: n.links || [] };
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
// related = file connections (D-155): out-edges (imports) + in-edges (imported-by) + churn signal. File-level only.
|
|
56
|
+
export function getRelated(ctx, id) {
|
|
57
|
+
const n = ctx.byId.get(id);
|
|
58
|
+
if (!n) return null;
|
|
59
|
+
const ref = (m) => ({ id: m.id, title: m.title, source: m.provenance && m.provenance.ref });
|
|
60
|
+
const imports = (n.links || []).map(tid => ctx.byId.get(tid)).filter(Boolean).map(ref);
|
|
61
|
+
const importedBy = ctx.city.nodes.filter(m => (m.links || []).includes(id)).map(ref);
|
|
62
|
+
return { id, title: n.title, churn: n.churn || null, imports, importedBy };
|
|
63
|
+
}
|
package/mcp/server.mjs
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { load, search, getNode, listCategories, getProvenance } from "../core/retrieve.mjs";
|
|
6
|
+
import { load, search, getNode, listCategories, getProvenance, getRelated } from "../core/retrieve.mjs";
|
|
7
7
|
|
|
8
8
|
const CITY = process.env.KC_CITY || process.argv[2];
|
|
9
9
|
if (!CITY) { console.error("Knowledge City MCP: set KC_CITY (or pass a path) to a city-data.v2.json"); process.exit(1); }
|
|
10
10
|
const ctx = load(CITY);
|
|
11
11
|
|
|
12
|
-
const server = new McpServer({ name: "knowledge-city", version: "0.
|
|
12
|
+
const server = new McpServer({ name: "knowledge-city", version: "0.4.1" });
|
|
13
13
|
|
|
14
14
|
server.registerTool("kc_search", {
|
|
15
15
|
title: "Search the Knowledge City",
|
|
@@ -54,5 +54,21 @@ server.registerTool("kc_get_provenance", {
|
|
|
54
54
|
return { content: [{ type: "text", text: p ? JSON.stringify(p, null, 2) : `No item with id ${id}` }], structuredContent: p || {} };
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
server.registerTool("kc_related", {
|
|
58
|
+
title: "Related files (connections)",
|
|
59
|
+
description: "How a file connects to others in this project: which files it imports (out), which files import it (in), and its recent-change (churn) signal. File-level structure with citations, not code analysis.",
|
|
60
|
+
inputSchema: { id: z.string().max(400).describe("node id, e.g. fs:src/auth.js") },
|
|
61
|
+
annotations: { readOnlyHint: true },
|
|
62
|
+
}, async ({ id }) => {
|
|
63
|
+
const r = getRelated(ctx, id);
|
|
64
|
+
if (!r) return { content: [{ type: "text", text: "No item with id " + id }], structuredContent: {} };
|
|
65
|
+
const NL = String.fromCharCode(10);
|
|
66
|
+
const lines = [r.title + " (" + id + ")"];
|
|
67
|
+
if (r.churn) lines.push("recent changes: " + r.churn.c + " commit(s), heat " + r.churn.b);
|
|
68
|
+
lines.push("imports (" + r.imports.length + "): " + (r.imports.map(x => x.source || x.id).join(", ") || "none"));
|
|
69
|
+
lines.push("imported by (" + r.importedBy.length + "): " + (r.importedBy.map(x => x.source || x.id).join(", ") || "none"));
|
|
70
|
+
return { content: [{ type: "text", text: lines.join(NL) }], structuredContent: r };
|
|
71
|
+
});
|
|
72
|
+
|
|
57
73
|
await server.connect(new StdioServerTransport());
|
|
58
74
|
console.error(`knowledge-city MCP ready — ${ctx.city.node_count} nodes, ${(ctx.city.categories||[]).length} categories`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knosky",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Turn any repo or docs folder into an explorable city, plus a local MCP that grounds your AI in your own source with citations. Local-first, free, $0 tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
function adapt(n){const versions=Array.isArray(n.versions)&&n.versions.length?n.versions:[{v:n.version||"1.0",date:n.fact_date||GD,note:n.status||""}];
|
|
185
185
|
const dc=n.category||n.district;const d=DCFG[dc]?dc:(DKEYS[0]||"general");
|
|
186
186
|
return {id:n.id,d,t:n.title||n.id,s:n.summary||"",kind:n.kind,status:n.status||"",
|
|
187
|
-
versions,prov:n.provenance||null,links:(n.links||[]),sensitive:!!n.sensitive};}
|
|
187
|
+
versions,prov:n.provenance||null,links:(n.links||[]),churn:n.churn||null,sensitive:!!n.sensitive};}
|
|
188
188
|
// NODES/byId are (re)built by ingest() from the postMessage payload.
|
|
189
189
|
let NODES=[];
|
|
190
190
|
let byId={};
|
|
@@ -501,6 +501,15 @@
|
|
|
501
501
|
ctx.fillStyle="rgba(255,233,166,.28)";ctx.beginPath();ctx.arc(B[0],B[1],7,0,7);ctx.fill();ctx.setLineDash([2,6]);});
|
|
502
502
|
ctx.setLineDash([]);ctx.fillStyle="#fff2c4";ctx.beginPath();ctx.arc(A[0],A[1],4.5,0,7);ctx.fill();ctx.restore();}
|
|
503
503
|
|
|
504
|
+
// ---------- churn heat: ground glow under recently-changed files (D-155, file-level metadata only) ----------
|
|
505
|
+
function drawChurn(){
|
|
506
|
+
for(const n of NODES){ const ch=n.churn; if(!ch||!ch.b||ch.b<2||filteredHidden.has(n.id)) continue;
|
|
507
|
+
const col = ch.b>=3 ? 'rgba(242,104,63,' : 'rgba(232,178,74,';
|
|
508
|
+
ctx.fillStyle=col+'0.20)'; diamondPath(n.bx,n.by,TW*0.62,TH*0.62); ctx.fill();
|
|
509
|
+
ctx.fillStyle=col+'0.32)'; diamondPath(n.bx,n.by,TW*0.40,TH*0.40); ctx.fill();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
504
513
|
// ---------- cars (more realistic iso car) ----------
|
|
505
514
|
const CARCOL=["#e85d54","#3f7fd0","#e8c44e","#43b07a","#d9dde4","#9a6cd0"];
|
|
506
515
|
// Sprite columns: 0=up-left(-x) 1=up-right(-y) 2=down-right(+x) 3=down-left(+y). Each car faces its travel dir.
|
|
@@ -569,7 +578,7 @@
|
|
|
569
578
|
function render(){NOW=performance.now()-T0;moveSky();ctx.setTransform(DPR,0,0,DPR,0,0);drawBackdrop();
|
|
570
579
|
ctx.save();ctx.translate(cam.x,cam.y);ctx.scale(cam.zoom,cam.zoom);
|
|
571
580
|
ctx.globalAlpha=Math.min(1,NOW/450);
|
|
572
|
-
drawIslandShadow();drawSlab();drawGround();drawCloudShadows();drawShadows();
|
|
581
|
+
drawIslandShadow();drawSlab();drawGround();drawCloudShadows();drawShadows();drawChurn();
|
|
573
582
|
const list=OBJ.slice();drawCars(list);
|
|
574
583
|
list.sort((a,b)=>(a.gx+a.gy)-(b.gx+b.gy)||a.gx-b.gx);
|
|
575
584
|
for(const o of list){
|
|
@@ -681,20 +690,25 @@
|
|
|
681
690
|
|
|
682
691
|
// ---------- host bridge over postMessage (embedded mode ONLY; OFF in the standalone gift) ----------
|
|
683
692
|
if(window.__KC_EMBED__){
|
|
684
|
-
var KC_OK=window.__KC_ALLOWED_ORIGINS
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
693
|
+
var KC_OK=(Array.isArray(window.__KC_ALLOWED_ORIGINS)?window.__KC_ALLOWED_ORIGINS:[]).filter(Boolean);
|
|
694
|
+
if(!KC_OK.length){
|
|
695
|
+
console.warn('KnoSky embed disabled: window.__KC_ALLOWED_ORIGINS is empty. Set at least one allowed origin to enable the postMessage bridge.');
|
|
696
|
+
}else{
|
|
697
|
+
window.addEventListener('message',function(ev){
|
|
698
|
+
if(KC_OK.indexOf(ev.origin)===-1)return; // origin allowlist (REQUIRED, fail-closed)
|
|
699
|
+
if(ev.source!==window.parent)return; // only accept from the embedding parent
|
|
700
|
+
var d=ev&&ev.data;if(!d||typeof d!=='object')return;
|
|
701
|
+
if(d.type==='kc:data'){ingest(d.payload);return;}
|
|
702
|
+
if(d.type==='kc:refit'){resize();if(typeof fitAll==='function'){fitAll();cam.x=cam.tx;cam.y=cam.ty;cam.zoom=cam.tz;}return;}
|
|
703
|
+
if(d.type==='kc:focus'&&d.id!=null){var id=String(d.id);if(byId[id]){selectBuilding(id);}}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
692
706
|
}
|
|
693
707
|
if(window.__KC_KENNEY__)loadKenney();
|
|
694
708
|
// Standalone (local product): render immediately from injected page data.
|
|
695
709
|
try{ if(window.__KC_DATA__&&typeof window.__KC_DATA__==='object'&&Array.isArray(window.__KC_DATA__.nodes)) ingest(window.__KC_DATA__); }catch(e){}
|
|
696
710
|
// Announce readiness to an authenticated parent (embedded mode only, no wildcard broadcast in standalone).
|
|
697
|
-
if(window.__KC_EMBED__){try{window.parent&&window.parent.postMessage({type:"kc:ready"},
|
|
711
|
+
if(window.__KC_EMBED__){var KC_RDY=(Array.isArray(window.__KC_ALLOWED_ORIGINS)?window.__KC_ALLOWED_ORIGINS:[]).filter(Boolean);if(KC_RDY.length){try{window.parent&&window.parent.postMessage({type:"kc:ready"},KC_RDY[0]);}catch(e){}}}
|
|
698
712
|
})();
|
|
699
713
|
</script>
|
|
700
714
|
</body>
|