@jkwd/inbase 0.1.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.
Files changed (44) hide show
  1. package/README.md +76 -0
  2. package/apps/explorer/index.html +12 -0
  3. package/apps/explorer/package.json +28 -0
  4. package/apps/explorer/scripts/js-source.mjs +188 -0
  5. package/apps/explorer/scripts/patch-lib.d.ts +115 -0
  6. package/apps/explorer/scripts/patch-lib.mjs +472 -0
  7. package/apps/explorer/scripts/scan-target.mjs +188 -0
  8. package/apps/explorer/scripts/session-store.d.ts +156 -0
  9. package/apps/explorer/scripts/session-store.mjs +809 -0
  10. package/apps/explorer/scripts/target-config.d.ts +8 -0
  11. package/apps/explorer/scripts/target-config.mjs +42 -0
  12. package/apps/explorer/src/App.tsx +941 -0
  13. package/apps/explorer/src/agentIntent.ts +182 -0
  14. package/apps/explorer/src/codebase.ts +15 -0
  15. package/apps/explorer/src/index.css +632 -0
  16. package/apps/explorer/src/layout.ts +508 -0
  17. package/apps/explorer/src/main.tsx +16 -0
  18. package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
  19. package/apps/explorer/src/scene/Bridge.tsx +290 -0
  20. package/apps/explorer/src/scene/FileBlock.tsx +256 -0
  21. package/apps/explorer/src/scene/FolderArea.tsx +96 -0
  22. package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
  23. package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
  24. package/apps/explorer/src/scene/MapView.tsx +247 -0
  25. package/apps/explorer/src/scene/Player.tsx +245 -0
  26. package/apps/explorer/src/scene/RelationLines.tsx +223 -0
  27. package/apps/explorer/src/scene/SelectionController.tsx +89 -0
  28. package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
  29. package/apps/explorer/src/scene/World.tsx +323 -0
  30. package/apps/explorer/src/theme.ts +111 -0
  31. package/apps/explorer/src/types.ts +245 -0
  32. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
  33. package/apps/explorer/src/ui/HUD.tsx +1090 -0
  34. package/apps/explorer/src/ui/NameInput.tsx +45 -0
  35. package/apps/explorer/src/userContext.ts +73 -0
  36. package/apps/explorer/src/userCreated.ts +354 -0
  37. package/apps/explorer/src/vite-env.d.ts +1 -0
  38. package/apps/explorer/tsconfig.json +21 -0
  39. package/apps/explorer/vite.config.ts +295 -0
  40. package/bin/inbase.mjs +170 -0
  41. package/bin/project.mjs +94 -0
  42. package/bin/session.mjs +241 -0
  43. package/package.json +63 -0
  44. package/skill/inbase/SKILL.md +167 -0
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Inbase
2
+
3
+ A first-person 3D map of a JavaScript or TypeScript codebase. Files become blocks, folders become walkable areas, and imports become lines in the air.
4
+
5
+ Install the package in a project, run `inbase init`, then `inbase run`. Cursor uses the installed skill so code changes go through the visual map.
6
+
7
+ The npm package is `@jkwd/inbase` (npm blocks the unscoped name `inbase`). The command is still `inbase`.
8
+
9
+ ## Use in a project
10
+
11
+ ```bash
12
+ npm install -D @jkwd/inbase
13
+ npx inbase init
14
+ npx inbase run
15
+ ```
16
+
17
+ Or install globally:
18
+
19
+ ```bash
20
+ npm install -g @jkwd/inbase
21
+ inbase init
22
+ inbase run
23
+ ```
24
+
25
+ Open the printed URL (http://localhost:5173 by default), click the scene, then walk with WASD.
26
+
27
+ `inbase run` maps the current directory. To map another folder:
28
+
29
+ ```bash
30
+ inbase run --target /path/to/your/project
31
+ ```
32
+
33
+ `inbase init` copies a Cursor skill into `.cursor/skills/inbase/` and gitignores `.inbase/`. After that, ask Cursor to change source files in this repo — it should follow the visual plan/patch workflow while `inbase run` is up.
34
+
35
+ ## Commands
36
+
37
+ | Command | What it does |
38
+ | --- | --- |
39
+ | `inbase init` | Install the Cursor skill in this repo |
40
+ | `inbase run` | Scan this repo and start the local map |
41
+ | `inbase run --port 5174` | Start on another port |
42
+
43
+ Session commands (`start-session`, `wait-for-blueprint`, `report-plan`, `wait-for-approval`, `propose-patch`) are used by the Cursor skill. You do not need to run them yourself.
44
+
45
+ ## Language support
46
+
47
+ Source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`) become blocks with **functions**, **classes**, and **variables**, plus import edges from `import` and `require()`. Styles and docs (`.css`, `.scss`, `.html`, `.json`, `.md`) show up as blocks without symbols. Only relative specifiers (`./`, `../`) become edges — package names like `react` or `@angular/core` do not.
48
+
49
+ ## How to read the map
50
+
51
+ - Each **file** is a block. Taller blocks have more lines of code.
52
+ - Small cubes on a block are **functions** (blue) and **variables** (tan).
53
+ - Each **folder** is a floor area with a center walkway.
54
+ - At the far end of an area, **bridges** lead into child folders. The folder name hangs above the bridge.
55
+ - Click a block to see **import relations**. Connected files stay lit and arcs draw to them.
56
+
57
+ ## Develop Inbase itself
58
+
59
+ This repository is a workspace. The bundled demo target is `apps/example-target`.
60
+
61
+ ```bash
62
+ npm install
63
+ npm run dev
64
+ ```
65
+
66
+ That still maps `apps/example-target` by default. To map a different project without the CLI:
67
+
68
+ ```bash
69
+ VISUAL_CODER_TARGET=/path/to/your/project npm run dev
70
+ ```
71
+
72
+ To run the example React app itself:
73
+
74
+ ```bash
75
+ npm run dev:target
76
+ ```
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Inbase</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@visual-coder/explorer",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "scan": "node scripts/scan-target.mjs",
8
+ "dev": "node scripts/scan-target.mjs && vite",
9
+ "build": "node scripts/scan-target.mjs && tsc --noEmit && vite build",
10
+ "test": "node --test scripts/*.test.mjs",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "@react-three/drei": "^9.117.3",
15
+ "@react-three/fiber": "^8.17.10",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1",
18
+ "three": "^0.170.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.3.12",
22
+ "@types/react-dom": "^18.3.1",
23
+ "@types/three": "^0.170.0",
24
+ "@vitejs/plugin-react": "^4.3.4",
25
+ "typescript": "^5.6.3",
26
+ "vite": "^6.0.3"
27
+ }
28
+ }
@@ -0,0 +1,188 @@
1
+ export const FILE_EXTENSIONS = new Set([
2
+ '.ts',
3
+ '.tsx',
4
+ '.js',
5
+ '.jsx',
6
+ '.mjs',
7
+ '.cjs',
8
+ '.css',
9
+ '.scss',
10
+ '.json',
11
+ '.html',
12
+ '.md',
13
+ ])
14
+
15
+ export const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'])
16
+
17
+ export const RESOLVE_EXTENSIONS = [
18
+ '.tsx',
19
+ '.ts',
20
+ '.jsx',
21
+ '.js',
22
+ '.mjs',
23
+ '.cjs',
24
+ '.css',
25
+ '.scss',
26
+ '.json',
27
+ ]
28
+
29
+ export function extractJsSymbols(source) {
30
+ const symbols = []
31
+ const seen = new Set()
32
+
33
+ const add = (name, kind) => {
34
+ if (!name || seen.has(name)) return
35
+ seen.add(name)
36
+ symbols.push({ name, kind })
37
+ }
38
+
39
+ for (const match of source.matchAll(/\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) {
40
+ add(match[1], 'function')
41
+ }
42
+
43
+ for (const match of source.matchAll(
44
+ /\b(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/g,
45
+ )) {
46
+ add(match[1], 'class')
47
+ }
48
+
49
+ for (const match of source.matchAll(
50
+ /(?:export\s+)?(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g,
51
+ )) {
52
+ add(match[1], 'function')
53
+ }
54
+
55
+ for (const match of source.matchAll(/(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)) {
56
+ add(match[1], 'variable')
57
+ }
58
+
59
+ for (const match of source.matchAll(
60
+ /(?:export\s+)?(?:const|let|var)\s+(\[[^\]]+\]|\{[^}]+\})/g,
61
+ )) {
62
+ for (const ident of destructuredNames(match[1])) add(ident, 'variable')
63
+ }
64
+
65
+ return symbols
66
+ }
67
+
68
+ export function collectImportSpecifiers(source) {
69
+ const specifiers = new Set()
70
+ for (const match of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
71
+ specifiers.add(match[1])
72
+ }
73
+ for (const match of source.matchAll(/import\s+['"]([^'"]+)['"]/g)) {
74
+ specifiers.add(match[1])
75
+ }
76
+ for (const match of source.matchAll(/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g)) {
77
+ specifiers.add(match[1])
78
+ }
79
+ return [...specifiers]
80
+ }
81
+
82
+ export function extractImportBindings(source) {
83
+ const bindings = []
84
+ const seen = new Set()
85
+
86
+ const add = (name, from) => {
87
+ const ident = name?.trim()
88
+ const specifier = from?.trim()
89
+ if (!ident || !specifier) return
90
+ const key = `${ident}\0${specifier}`
91
+ if (seen.has(key)) return
92
+ seen.add(key)
93
+ bindings.push({ name: ident, from: specifier })
94
+ }
95
+
96
+ for (const match of source.matchAll(/^[ \t]*import\s+['"]([^'"]+)['"]/gm)) {
97
+ add(match[1], match[1])
98
+ }
99
+
100
+ for (const match of source.matchAll(
101
+ /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm,
102
+ )) {
103
+ const clause = match[1].trim()
104
+ const from = match[2]
105
+ const star = clause.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)/)
106
+ if (star) {
107
+ add(star[1], from)
108
+ continue
109
+ }
110
+
111
+ const named = clause.match(/\{([^}]*)\}/)
112
+ const defaultPart = clause
113
+ .replace(/\{[^}]*\}/, '')
114
+ .replace(/,/g, '')
115
+ .replace(/^type\s+/, '')
116
+ .trim()
117
+ if (defaultPart && /^[A-Za-z_$][\w$]*$/.test(defaultPart)) {
118
+ add(defaultPart, from)
119
+ }
120
+ if (!named) continue
121
+ for (const ident of destructuredNames(`{${named[1]}}`, { importClause: true })) {
122
+ add(ident, from)
123
+ }
124
+ }
125
+
126
+ const boundRequires = new Set()
127
+
128
+ for (const match of source.matchAll(
129
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g,
130
+ )) {
131
+ add(match[1], match[2])
132
+ boundRequires.add(match[2])
133
+ }
134
+
135
+ for (const match of source.matchAll(
136
+ /\b(?:const|let|var)\s+(\{[^}]+\}|\[[^\]]+\])\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g,
137
+ )) {
138
+ boundRequires.add(match[2])
139
+ for (const ident of destructuredNames(match[1])) add(ident, match[2])
140
+ }
141
+
142
+ for (const match of source.matchAll(/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g)) {
143
+ if (boundRequires.has(match[1])) continue
144
+ add(match[1], match[1])
145
+ }
146
+
147
+ return bindings
148
+ }
149
+
150
+ export function resolveSpecifierAgainst(candidate, known) {
151
+ if (known.has(candidate)) return candidate
152
+ for (const ext of RESOLVE_EXTENSIONS) {
153
+ if (known.has(candidate + ext)) return candidate + ext
154
+ }
155
+ for (const ext of RESOLVE_EXTENSIONS) {
156
+ const indexFile = `${candidate}/index${ext}`
157
+ if (known.has(indexFile)) return indexFile
158
+ }
159
+ return null
160
+ }
161
+
162
+ function destructuredNames(pattern, { importClause = false } = {}) {
163
+ const names = []
164
+ const inner = pattern.slice(1, -1)
165
+ for (const part of inner.split(',')) {
166
+ let token = part.trim()
167
+ if (!token) continue
168
+ token = token.replace(/^\.\.\./, '')
169
+ if (importClause) {
170
+ const aliased = token.match(/^(?:type\s+)?[A-Za-z_$][\w$]*\s+as\s+([A-Za-z_$][\w$]*)$/)
171
+ if (aliased) {
172
+ names.push(aliased[1])
173
+ continue
174
+ }
175
+ const ident = token.replace(/^type\s+/, '').trim()
176
+ if (/^[A-Za-z_$][\w$]*$/.test(ident)) names.push(ident)
177
+ continue
178
+ }
179
+ const aliased = token.match(/^[A-Za-z_$][\w$]*\s*:\s*([A-Za-z_$][\w$]*)/)
180
+ if (aliased) {
181
+ names.push(aliased[1])
182
+ continue
183
+ }
184
+ const ident = token.split(/\s*=/)[0].trim()
185
+ if (/^[A-Za-z_$][\w$]*$/.test(ident)) names.push(ident)
186
+ }
187
+ return names
188
+ }
@@ -0,0 +1,115 @@
1
+ export type PatchImport = {
2
+ from: string
3
+ to: string
4
+ }
5
+
6
+ export type PatchSymbolAddition = {
7
+ name: string
8
+ file: string
9
+ }
10
+
11
+ export type PatchImportAddition = {
12
+ name: string
13
+ from: string
14
+ file: string
15
+ }
16
+
17
+ export function parseUnifiedPatch(patch: string): {
18
+ files: string[]
19
+ creates: string[]
20
+ deletes: string[]
21
+ createLines: Record<string, number>
22
+ entries: Array<{
23
+ id: string
24
+ kind: 'add' | 'modify' | 'delete'
25
+ addedLines: number
26
+ hunks: unknown[]
27
+ }>
28
+ }
29
+
30
+ export function extractPatchImports(
31
+ entries: Array<{
32
+ id: string
33
+ kind: 'add' | 'modify' | 'delete'
34
+ hunks: unknown[]
35
+ }>,
36
+ knownFileIds?: string[],
37
+ ): PatchImport[]
38
+
39
+ export function extractPatchAdditions(
40
+ entries: Array<{
41
+ id: string
42
+ kind: 'add' | 'modify' | 'delete'
43
+ hunks: unknown[]
44
+ }>,
45
+ ): {
46
+ addedFunctions: PatchSymbolAddition[]
47
+ addedVariables: PatchSymbolAddition[]
48
+ addedImports: PatchImportAddition[]
49
+ }
50
+
51
+ export function accumulatePatchAdditions(patches?: string[]): {
52
+ addedFunctions: PatchSymbolAddition[]
53
+ addedVariables: PatchSymbolAddition[]
54
+ addedImports: PatchImportAddition[]
55
+ }
56
+
57
+ export function applyUnifiedPatch(
58
+ patch: string,
59
+ targetRoot: string,
60
+ ): ReturnType<typeof parseUnifiedPatch>
61
+
62
+ export function folderOfFile(fileId: string): string
63
+ export function folderParent(folderPath: string): string | null
64
+ export function foldersFromFileIds(ids?: string[]): Set<string>
65
+ export function collectCreateFolders(
66
+ creates?: string[],
67
+ existingFolders?: Iterable<string>,
68
+ ): string[]
69
+
70
+ export const emptyIntent: {
71
+ updatedAt: null
72
+ showMap: boolean
73
+ status: string
74
+ feature: null
75
+ steps: unknown[]
76
+ step: null
77
+ files: string[]
78
+ creates: string[]
79
+ deletes: string[]
80
+ createFolders: string[]
81
+ createLines: Record<string, number>
82
+ imports: PatchImport[]
83
+ addedFunctions: PatchSymbolAddition[]
84
+ addedVariables: PatchSymbolAddition[]
85
+ addedImports: PatchImportAddition[]
86
+ reason: null
87
+ sessionId: null
88
+ diffId: null
89
+ parentDiffId: null
90
+ chainIndex: null
91
+ chain: unknown[]
92
+ isActiveDiff: boolean
93
+ preview: boolean
94
+ phase: null
95
+ working: boolean
96
+ creationMode: boolean
97
+ canEnterBlueprint: boolean
98
+ blueprintSessionId: string | null
99
+ userCreatedBlocks: unknown[]
100
+ userCreatedIslands: unknown[]
101
+ blueprintFunctions: unknown[]
102
+ blueprintVariables: unknown[]
103
+ blueprintImports: unknown[]
104
+ }
105
+
106
+ export function isLastStep(intent: {
107
+ step?: number | null
108
+ steps?: unknown[]
109
+ }): boolean
110
+
111
+ export function overlayPatch(
112
+ intent: Record<string, unknown>,
113
+ patchText: string,
114
+ knownFileIds?: string[],
115
+ ): Record<string, unknown>