@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
@@ -0,0 +1,45 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ type NameInputProps = {
4
+ placeholder: string
5
+ onCommit: (name: string) => void
6
+ onCancel: () => void
7
+ }
8
+
9
+ export function NameInput({ placeholder, onCommit, onCancel }: NameInputProps) {
10
+ const input = useRef<HTMLInputElement>(null)
11
+
12
+ useEffect(() => {
13
+ const timer = window.setTimeout(() => input.current?.focus(), 40)
14
+ return () => window.clearTimeout(timer)
15
+ }, [])
16
+
17
+ return (
18
+ <form
19
+ className="block-name-form"
20
+ onPointerDown={(event) => event.stopPropagation()}
21
+ onSubmit={(event) => {
22
+ event.preventDefault()
23
+ const value = input.current?.value ?? ''
24
+ if (!value.trim()) return
25
+ onCommit(value)
26
+ }}
27
+ >
28
+ <input
29
+ ref={input}
30
+ className="block-name-input"
31
+ placeholder={placeholder}
32
+ aria-label={placeholder}
33
+ autoComplete="off"
34
+ spellCheck={false}
35
+ onKeyDown={(event) => {
36
+ event.stopPropagation()
37
+ if (event.code === 'Escape') {
38
+ event.preventDefault()
39
+ onCancel()
40
+ }
41
+ }}
42
+ />
43
+ </form>
44
+ )
45
+ }
@@ -0,0 +1,73 @@
1
+ import type {
2
+ CodebaseGraph,
3
+ FileNode,
4
+ UserContext,
5
+ UserFileRef,
6
+ } from './types'
7
+
8
+ export function toFileRef(file: FileNode): UserFileRef {
9
+ return {
10
+ id: file.id,
11
+ name: file.name,
12
+ path: file.path,
13
+ folder: file.folder,
14
+ }
15
+ }
16
+
17
+ let timer: number | null = null
18
+ let pending: UserContext | null = null
19
+ let lastWritten = ''
20
+
21
+ export async function fetchUserContext(): Promise<UserContext | null> {
22
+ try {
23
+ const response = await fetch('/api/user-context')
24
+ if (!response.ok) return null
25
+ return (await response.json()) as UserContext
26
+ } catch {
27
+ return null
28
+ }
29
+ }
30
+
31
+ export function persistFollowLook(followLook: boolean) {
32
+ fetch('/api/user-context', {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: `${JSON.stringify({ followLook })}\n`,
36
+ }).catch(() => {
37
+ lastWritten = ''
38
+ })
39
+ }
40
+
41
+ export function persistUserContext(context: UserContext) {
42
+ pending = context
43
+ if (timer !== null) return
44
+ timer = window.setTimeout(flushUserContext, 400)
45
+ }
46
+
47
+ function flushUserContext() {
48
+ timer = null
49
+ const context = pending
50
+ pending = null
51
+ if (!context) return
52
+ const {
53
+ followLook: _followLook,
54
+ userCreatedBlocks: _userCreatedBlocks,
55
+ userCreatedIslands: _userCreatedIslands,
56
+ ...gaze
57
+ } = context
58
+ const body = JSON.stringify(gaze, null, 2)
59
+ if (body === lastWritten) return
60
+ lastWritten = body
61
+ fetch('/api/user-context', {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: `${body}\n`,
65
+ }).catch(() => {
66
+ lastWritten = ''
67
+ })
68
+ }
69
+
70
+ export function fileById(graph: CodebaseGraph, id: string | null) {
71
+ if (!id) return null
72
+ return graph.files.find((file) => file.id === id) ?? null
73
+ }
@@ -0,0 +1,354 @@
1
+ import { CONFIG, fileHeight } from './theme'
2
+ import { folderOfFile, folderParent } from './layout'
3
+ import type {
4
+ CodebaseGraph,
5
+ FileNode,
6
+ PatchImportAddition,
7
+ PatchSymbolAddition,
8
+ UserCreatedBlock,
9
+ UserCreatedIsland,
10
+ WorldLayout,
11
+ } from './types'
12
+
13
+ export function languageOfName(name: string) {
14
+ const ext = name.split('.').pop()?.toLowerCase()
15
+ return ext && ext !== name ? ext : 'txt'
16
+ }
17
+
18
+ export function toCreatedFile(block: UserCreatedBlock): FileNode {
19
+ const name = block.naming && !block.name ? 'New file' : block.name
20
+ return {
21
+ id: block.id,
22
+ name,
23
+ path: block.path || block.id,
24
+ folder: block.folder,
25
+ lines: 12,
26
+ language: languageOfName(name),
27
+ symbols: [],
28
+ imports: [],
29
+ userCreated: true,
30
+ }
31
+ }
32
+
33
+ export function namedCreatedBlocks(blocks: UserCreatedBlock[]) {
34
+ return blocks.filter((block) => !block.naming && Boolean(block.name))
35
+ }
36
+
37
+ export function namedCreatedIslands(islands: UserCreatedIsland[]) {
38
+ return islands.filter((island) => !island.naming && Boolean(island.name))
39
+ }
40
+
41
+ export function parseUserCreatedBlocks(value: unknown): UserCreatedBlock[] {
42
+ if (!Array.isArray(value)) return []
43
+ return value.flatMap((item) => {
44
+ if (!item || typeof item !== 'object') return []
45
+ const block = item as Partial<UserCreatedBlock>
46
+ if (
47
+ typeof block.id !== 'string' ||
48
+ typeof block.name !== 'string' ||
49
+ typeof block.path !== 'string' ||
50
+ typeof block.folder !== 'string' ||
51
+ typeof block.x !== 'number' ||
52
+ typeof block.z !== 'number'
53
+ ) {
54
+ return []
55
+ }
56
+ return [
57
+ {
58
+ id: block.id,
59
+ name: block.name,
60
+ path: block.path,
61
+ folder: block.folder,
62
+ x: block.x,
63
+ z: block.z,
64
+ },
65
+ ]
66
+ })
67
+ }
68
+
69
+ export function parseUserCreatedIslands(value: unknown): UserCreatedIsland[] {
70
+ if (!Array.isArray(value)) return []
71
+ return value.flatMap((item) => {
72
+ if (!item || typeof item !== 'object') return []
73
+ const island = item as Partial<UserCreatedIsland>
74
+ if (
75
+ typeof island.id !== 'string' ||
76
+ typeof island.name !== 'string' ||
77
+ typeof island.path !== 'string' ||
78
+ typeof island.parent !== 'string'
79
+ ) {
80
+ return []
81
+ }
82
+ return [
83
+ {
84
+ id: island.id,
85
+ name: island.name,
86
+ path: island.path,
87
+ parent: island.parent,
88
+ },
89
+ ]
90
+ })
91
+ }
92
+
93
+ export function resolveCreatedFile(rawName: string, folder: string) {
94
+ const trimmed = rawName.trim().replaceAll('\\', '/').replace(/^\.\//, '')
95
+ if (!trimmed) return null
96
+ const path = trimmed.includes('/')
97
+ ? trimmed.replace(/^\/+/, '').replace(/\/+/g, '/')
98
+ : folder === '.'
99
+ ? trimmed
100
+ : `${folder}/${trimmed}`
101
+ const name = path.split('/').pop() ?? path
102
+ if (!name) return null
103
+ return {
104
+ id: path,
105
+ name,
106
+ path,
107
+ folder: folderOfFile(path),
108
+ }
109
+ }
110
+
111
+ export function resolveCreatedIsland(rawName: string, parent: string) {
112
+ const trimmed = rawName
113
+ .trim()
114
+ .replaceAll('\\', '/')
115
+ .replace(/^\.\//, '')
116
+ .replace(/\/+$/, '')
117
+ if (!trimmed) return null
118
+ const path = trimmed.includes('/')
119
+ ? trimmed.replace(/^\/+/, '').replace(/\/+/g, '/')
120
+ : parent === '.'
121
+ ? trimmed
122
+ : `${parent}/${trimmed}`
123
+ const name = path.split('/').pop() ?? path
124
+ if (!name) return null
125
+ return {
126
+ id: path,
127
+ name,
128
+ path,
129
+ parent: folderParent(path) ?? '.',
130
+ }
131
+ }
132
+
133
+ function islandKey(island: UserCreatedIsland) {
134
+ return island.path || island.id
135
+ }
136
+
137
+ function islandWidth() {
138
+ const fileOffset = CONFIG.aisleWidth / 2 + CONFIG.fileWidth / 2 + 0.7
139
+ const fileOuter = fileOffset + CONFIG.fileWidth / 2
140
+ return (fileOuter + 1.1) * 2
141
+ }
142
+
143
+ function islandDepth() {
144
+ return CONFIG.areaPadding * 2 + CONFIG.fileSpacing
145
+ }
146
+
147
+ export function withUserCreatedGraph(
148
+ graph: CodebaseGraph,
149
+ blocks: UserCreatedBlock[],
150
+ islands: UserCreatedIsland[] = [],
151
+ ): CodebaseGraph {
152
+ if (blocks.length === 0 && islands.length === 0) return graph
153
+ const files = new Map(graph.files.map((file) => [file.id, file]))
154
+ const folders = new Map(
155
+ graph.folders.map((folder) => [
156
+ folder.path,
157
+ { ...folder, files: [...folder.files], children: [...folder.children] },
158
+ ]),
159
+ )
160
+
161
+ for (const island of islands) {
162
+ const path = islandKey(island)
163
+ if (folders.has(path)) continue
164
+ const parent = island.parent || folderParent(path)
165
+ folders.set(path, {
166
+ path,
167
+ name: island.naming && !island.name ? 'New folder' : island.name,
168
+ parent,
169
+ files: [],
170
+ children: [],
171
+ userCreated: true,
172
+ })
173
+ if (parent) {
174
+ const parentFolder = folders.get(parent)
175
+ if (parentFolder && !parentFolder.children.includes(path)) {
176
+ parentFolder.children.push(path)
177
+ }
178
+ }
179
+ }
180
+
181
+ for (const block of blocks) {
182
+ if (files.has(block.id)) continue
183
+ const file = toCreatedFile(block)
184
+ files.set(file.id, file)
185
+ const folder = folders.get(file.folder)
186
+ if (folder && !folder.files.includes(file.id)) folder.files.push(file.id)
187
+ }
188
+
189
+ return {
190
+ ...graph,
191
+ files: [...files.values()],
192
+ folders: [...folders.values()],
193
+ }
194
+ }
195
+
196
+ function overlayIslands(
197
+ layout: WorldLayout,
198
+ islands: UserCreatedIsland[],
199
+ ): WorldLayout {
200
+ if (islands.length === 0) return layout
201
+ const folders = { ...layout.folders }
202
+ const bridges = [...layout.bridges]
203
+ const width = islandWidth()
204
+ const depth = islandDepth()
205
+
206
+ for (const island of islands) {
207
+ const id = islandKey(island)
208
+ if (layout.folders[id]) {
209
+ folders[id] = { ...folders[id], added: true, name: island.name || folders[id].name }
210
+ continue
211
+ }
212
+ const parentPath = island.parent
213
+ const parent = folders[parentPath]
214
+ if (!parent) continue
215
+ const siblings = Object.values(folders).filter((folder) => {
216
+ if (folder.path === id) return false
217
+ const placedIsland = islands.find((item) => islandKey(item) === folder.path)
218
+ const folderParentPath = placedIsland?.parent ?? folderParent(folder.path)
219
+ return folderParentPath === parentPath
220
+ })
221
+ const x =
222
+ siblings.length === 0
223
+ ? parent.x
224
+ : Math.max(...siblings.map((folder) => folder.x + folder.width / 2)) +
225
+ CONFIG.siblingGap +
226
+ width / 2
227
+ const z = parent.z + parent.depth + CONFIG.bridgeLength
228
+ folders[id] = {
229
+ path: id,
230
+ name: island.naming && !island.name ? 'New folder' : island.name,
231
+ x,
232
+ z,
233
+ width,
234
+ depth,
235
+ added: true,
236
+ }
237
+ bridges.push({
238
+ id: `${parentPath}→${id}`,
239
+ label: folders[id].name,
240
+ fromLabel: parent.name,
241
+ points: [
242
+ [x, parent.z + parent.depth - CONFIG.bridgeOverlap],
243
+ [x, z + CONFIG.bridgeOverlap],
244
+ ],
245
+ })
246
+ }
247
+
248
+ return { ...layout, folders, bridges }
249
+ }
250
+
251
+ export function withUserCreatedLayout(
252
+ layout: WorldLayout,
253
+ blocks: UserCreatedBlock[],
254
+ islands: UserCreatedIsland[] = [],
255
+ ): WorldLayout {
256
+ const withIslands = overlayIslands(layout, islands)
257
+ if (blocks.length === 0) return withIslands
258
+ const files = { ...withIslands.files }
259
+ const height = fileHeight(12)
260
+ for (const block of blocks) {
261
+ const folder = withIslands.folders[block.folder]
262
+ files[block.id] = {
263
+ id: block.id,
264
+ position: [block.x, height / 2, block.z],
265
+ size: [CONFIG.fileWidth, height, CONFIG.fileDepth],
266
+ aisleFace: folder && block.x >= folder.x ? -1 : 1,
267
+ }
268
+ }
269
+ return { ...withIslands, files }
270
+ }
271
+
272
+ export function defaultBlockSpot(
273
+ layout: WorldLayout,
274
+ folderPath: string,
275
+ fileIndex: number,
276
+ ): { x: number; z: number; folder: string } | null {
277
+ const folder = layout.folders[folderPath]
278
+ if (!folder) return null
279
+ const side: 1 | -1 = fileIndex % 2 === 0 ? -1 : 1
280
+ const row = Math.floor(fileIndex / 2)
281
+ return {
282
+ x: folder.x + side * (CONFIG.aisleWidth / 2 + CONFIG.fileWidth / 2 + 0.7),
283
+ z: folder.z + CONFIG.areaPadding + row * CONFIG.fileSpacing,
284
+ folder: folderPath,
285
+ }
286
+ }
287
+
288
+ export function isBlueprintSymbolName(value: string) {
289
+ return /^[A-Za-z_$][\w$]*$/.test(value.trim())
290
+ }
291
+
292
+ export function parseBlueprintImport(
293
+ raw: string,
294
+ file: string,
295
+ knownFileIds: Iterable<string> = [],
296
+ ): PatchImportAddition | null {
297
+ const trimmed = raw.trim().replaceAll('\\', '/')
298
+ if (!trimmed || !file) return null
299
+ const match = trimmed.match(/^(.+?)\s+from\s+(.+)$/i)
300
+ const name = (match?.[1] ?? trimmed).trim()
301
+ const specifier = (match?.[2] ?? trimmed).trim()
302
+ if (!name || !specifier) return null
303
+ const known = new Set(knownFileIds)
304
+ const from =
305
+ known.has(specifier)
306
+ ? specifier
307
+ : ([...known].find(
308
+ (id) => id === specifier || id.endsWith(`/${specifier}`),
309
+ ) ?? specifier)
310
+ return { name, from, file }
311
+ }
312
+
313
+ export function withBlueprintIntent(
314
+ graph: CodebaseGraph,
315
+ functions: PatchSymbolAddition[] = [],
316
+ variables: PatchSymbolAddition[] = [],
317
+ imports: PatchImportAddition[] = [],
318
+ ): CodebaseGraph {
319
+ if (functions.length === 0 && variables.length === 0 && imports.length === 0) {
320
+ return graph
321
+ }
322
+ const files = graph.files.map((file) => ({
323
+ ...file,
324
+ symbols: [...file.symbols],
325
+ imports: [...file.imports],
326
+ }))
327
+ const byId = new Map(files.map((file) => [file.id, file]))
328
+ const known = new Set(byId.keys())
329
+
330
+ for (const item of functions) {
331
+ const file = byId.get(item.file)
332
+ if (!file) continue
333
+ if (file.symbols.some((symbol) => symbol.kind === 'function' && symbol.name === item.name)) {
334
+ continue
335
+ }
336
+ file.symbols.push({ name: item.name, kind: 'function', intended: true })
337
+ }
338
+ for (const item of variables) {
339
+ const file = byId.get(item.file)
340
+ if (!file) continue
341
+ if (file.symbols.some((symbol) => symbol.kind === 'variable' && symbol.name === item.name)) {
342
+ continue
343
+ }
344
+ file.symbols.push({ name: item.name, kind: 'variable', intended: true })
345
+ }
346
+ for (const item of imports) {
347
+ const file = byId.get(item.file)
348
+ if (!file || !known.has(item.from) || file.imports.includes(item.from)) continue
349
+ file.imports.push(item.from)
350
+ }
351
+
352
+ return { ...graph, files }
353
+ }
354
+
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "strict": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["src"]
21
+ }