@jkwd/inbase 0.1.15 → 0.1.17

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 CHANGED
@@ -49,11 +49,11 @@ inbase run --target /path/to/your/project
49
49
 
50
50
  Inbase does not call a model itself. The visual coding loop currently supports **Cursor**. The installed skill makes the agent work through the map: it reports a plan, waits on the **HUD** (heads-up display — the overlay panel on the 3D map), edits live files for each invoked step, and records that step as a patch. Those stored patches are the session record and are applied on every step update.
51
51
 
52
- Sessions start in the map: click **Setup LLM session**, type an **initial instruction**, optionally place a **blueprint** (`Space` for files, `B` for folders), then open a Cursor chat and run **`/inbase`**. That command connects the chat and starts the work. The layout is the source of truth for the chat. You can still place files and islands on later steps; they are stored on that chat’s blueprint. When the session finishes, placement stops and the session is discarded. Restarting the visualizer also starts with no LLM session; leftover session files are not restored.
52
+ Sessions start in the map: click **Setup LLM session**, type an **initial instruction**, optionally place a **blueprint** (`Space` for files, `B` for folders), then open a Cursor chat and run **`/inbase`**. That command connects the chat and starts the work. The layout is the source of truth for the chat. The blueprint is shared across sessions and stays on the map even after those files and folders exist; use **Hide/Show blueprint**, **Clear blueprint**, or **Cleanup blueprint** (drops planned items that already exist). When a session finishes it is discarded; the shared blueprint remains. Restarting the visualizer also starts with no LLM session; leftover session files are not restored.
53
53
 
54
54
  A normal chat request does not open a session. Use `/inbase` after Setup LLM session, or `/skipinbase [request]` to work outside the map. `/inbase` starts the session immediately; `wait-for-blueprint` only reads the optional blueprint.
55
55
 
56
- If several sessions are open, `/inbase` attaches the newest session that is still waiting. Sessions that already have an LLM are skipped, and the map window does not need to be focused.
56
+ If several sessions are open, `/inbase` attaches the oldest session that is still waiting. Sessions that already have an LLM are skipped, and the map window does not need to be focused.
57
57
 
58
58
  Turn on **Make LLM look where I look** if the agent should prefer the island you are standing on and the blocks you are facing. With **Step by step** on, click **Create proposal** to start a step, then **Accept proposal** when the patch is ready. With it off, the LLM implements the full plan; you can still walk Previous/Next over the diffs, then **Accept proposal**. Send an alternative instruction from the HUD to revise the remaining plan, or **Stop** to end the session.
59
59
 
@@ -8,23 +8,10 @@ import {
8
8
  foldersFromFileIds,
9
9
  parseUnifiedPatch,
10
10
  } from './patch-lib.mjs'
11
+ import { shouldIgnoreRelativePath, toPosix } from './scan-ignore.mjs'
11
12
 
12
- const SKIP_DIRS = new Set([
13
- 'node_modules',
14
- 'dist',
15
- 'build',
16
- 'out',
17
- 'coverage',
18
- '.git',
19
- '.inbase',
20
- ])
21
- const SKIP_FILES = new Set(['package-lock.json'])
22
13
  const BINARY_PROBE_BYTES = 8000
23
14
 
24
- function toPosix(filePath) {
25
- return filePath.split(path.sep).join('/')
26
- }
27
-
28
15
  function unique(ids) {
29
16
  return [...new Set(ids)]
30
17
  }
@@ -64,11 +51,7 @@ function isBinaryFile(filePath) {
64
51
  }
65
52
 
66
53
  function shouldSkipPath(fileId) {
67
- const parts = fileId.split('/').filter(Boolean)
68
- if (parts.some((part) => part.startsWith('.') || SKIP_DIRS.has(part))) {
69
- return true
70
- }
71
- return SKIP_FILES.has(parts.at(-1) ?? '')
54
+ return shouldIgnoreRelativePath(fileId)
72
55
  }
73
56
 
74
57
  function currentBranch(cwd) {
@@ -114,12 +114,15 @@ export const emptyIntent: {
114
114
  initialInstruction: string | null
115
115
  creationMode: boolean
116
116
  canEnterBlueprint: boolean
117
+ blueprintHidden?: boolean
118
+ blueprintRevision?: number
117
119
  blueprintSessionId: string | null
118
120
  userCreatedBlocks: unknown[]
119
121
  userCreatedIslands: unknown[]
120
122
  blueprintFunctions: unknown[]
121
123
  blueprintVariables: unknown[]
122
124
  blueprintImports: unknown[]
125
+ blueprintNotes: unknown[]
123
126
  }
124
127
 
125
128
  export function isLastStep(intent: {
@@ -541,12 +541,15 @@ export const emptyIntent = {
541
541
  initialInstruction: null,
542
542
  creationMode: false,
543
543
  canEnterBlueprint: false,
544
+ blueprintHidden: false,
545
+ blueprintRevision: 0,
544
546
  blueprintSessionId: null,
545
547
  userCreatedBlocks: [],
546
548
  userCreatedIslands: [],
547
549
  blueprintFunctions: [],
548
550
  blueprintVariables: [],
549
551
  blueprintImports: [],
552
+ blueprintNotes: [],
550
553
  }
551
554
 
552
555
  export function isLastStep(intent) {
@@ -0,0 +1,156 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const IGNORE_DIR_NAMES = new Set([
5
+ 'node_modules',
6
+ 'dist',
7
+ 'build',
8
+ 'out',
9
+ 'coverage',
10
+ '.git',
11
+ '.inbase',
12
+ ])
13
+
14
+ export const IGNORE_FILE_NAMES = new Set(['package-lock.json'])
15
+
16
+ export function toPosix(filePath) {
17
+ return filePath.split(path.sep).join('/')
18
+ }
19
+
20
+ /** True when any path segment is ignored, e.g. apps/web/node_modules/pkg/index.js. */
21
+ export function shouldIgnoreRelativePath(relative) {
22
+ const parts = toPosix(relative).split('/').filter(Boolean)
23
+ if (
24
+ parts.some(
25
+ (part) =>
26
+ IGNORE_DIR_NAMES.has(part) ||
27
+ (part.startsWith('.') && part !== '.' && part !== '..'),
28
+ )
29
+ ) {
30
+ return true
31
+ }
32
+ return IGNORE_FILE_NAMES.has(parts.at(-1) ?? '')
33
+ }
34
+
35
+ export function parseGitignore(text) {
36
+ const rules = []
37
+ for (const raw of text.split(/\r?\n/)) {
38
+ const trimmed = raw.trim()
39
+ if (!trimmed || trimmed.startsWith('#')) continue
40
+ let pattern = trimmed
41
+ const negated = pattern.startsWith('!')
42
+ if (negated) pattern = pattern.slice(1)
43
+ const dirOnly = pattern.endsWith('/')
44
+ if (dirOnly) pattern = pattern.slice(0, -1)
45
+ const fromRoot = pattern.startsWith('/')
46
+ if (fromRoot) pattern = pattern.slice(1)
47
+ const anchored = fromRoot || pattern.includes('/')
48
+ rules.push({ negated, dirOnly, anchored, pattern })
49
+ }
50
+ return rules
51
+ }
52
+
53
+ function globToRegExp(pattern) {
54
+ let source = '^'
55
+ for (let index = 0; index < pattern.length; index += 1) {
56
+ const char = pattern[index]
57
+ if (char === '*' && pattern[index + 1] === '*') {
58
+ source += '.*'
59
+ index += 1
60
+ if (pattern[index + 1] === '/') index += 1
61
+ continue
62
+ }
63
+ if (char === '*') {
64
+ source += '[^/]*'
65
+ continue
66
+ }
67
+ if (char === '?') {
68
+ source += '[^/]'
69
+ continue
70
+ }
71
+ if ('\\.^$+{}()|[]'.includes(char)) source += `\\${char}`
72
+ else source += char
73
+ }
74
+ source += '(/.*)?$'
75
+ return new RegExp(source)
76
+ }
77
+
78
+ function ruleMatches(relative, rule) {
79
+ if (!rule.pattern) return false
80
+ if (rule.anchored || rule.pattern.includes('*') || rule.pattern.includes('?')) {
81
+ const source = rule.anchored ? rule.pattern : `**/${rule.pattern}`
82
+ return globToRegExp(source).test(relative) || globToRegExp(rule.pattern).test(relative)
83
+ }
84
+ const parts = relative.split('/').filter(Boolean)
85
+ if (rule.dirOnly) {
86
+ return parts.slice(0, -1).includes(rule.pattern) || relative === rule.pattern
87
+ }
88
+ return parts.includes(rule.pattern)
89
+ }
90
+
91
+ export function matchesGitignoreRules(relative, rules) {
92
+ const rel = toPosix(relative).replace(/^\/+/, '')
93
+ if (!rel) return false
94
+ let ignored = false
95
+ for (const rule of rules) {
96
+ if (ruleMatches(rel, rule)) ignored = !rule.negated
97
+ }
98
+ return ignored
99
+ }
100
+
101
+ export function readGitignoreRules(dir) {
102
+ try {
103
+ return parseGitignore(fs.readFileSync(path.join(dir, '.gitignore'), 'utf8'))
104
+ } catch {
105
+ return []
106
+ }
107
+ }
108
+
109
+ export function collectGitignoreSets(root) {
110
+ const resolved = path.resolve(root)
111
+ if (isIgnoredByEnclosingGitignore(resolved)) {
112
+ const rules = readGitignoreRules(resolved)
113
+ return rules.length ? [{ base: resolved, rules }] : []
114
+ }
115
+ const chain = [resolved]
116
+ let dir = resolved
117
+ while (!fs.existsSync(path.join(dir, '.git'))) {
118
+ const parent = path.dirname(dir)
119
+ if (parent === dir) {
120
+ const rules = readGitignoreRules(resolved)
121
+ return rules.length ? [{ base: resolved, rules }] : []
122
+ }
123
+ dir = parent
124
+ chain.push(dir)
125
+ }
126
+ const sets = []
127
+ for (const base of chain.reverse()) {
128
+ const rules = readGitignoreRules(base)
129
+ if (rules.length) sets.push({ base, rules })
130
+ }
131
+ return sets
132
+ }
133
+
134
+ function isIgnoredByEnclosingGitignore(resolved) {
135
+ let dir = path.dirname(resolved)
136
+ for (;;) {
137
+ const rules = readGitignoreRules(dir)
138
+ if (rules.length) {
139
+ const relative = toPosix(path.relative(dir, resolved))
140
+ if (relative && matchesGitignoreRules(relative, rules)) return true
141
+ }
142
+ if (fs.existsSync(path.join(dir, '.git'))) return false
143
+ const parent = path.dirname(dir)
144
+ if (parent === dir) return false
145
+ dir = parent
146
+ }
147
+ }
148
+
149
+ export function isIgnoredByGitignore(absolutePath, ignoreSets) {
150
+ for (const { base, rules } of ignoreSets) {
151
+ const relative = toPosix(path.relative(base, absolutePath))
152
+ if (!relative || relative.startsWith('..')) continue
153
+ if (matchesGitignoreRules(relative, rules)) return true
154
+ }
155
+ return false
156
+ }
@@ -7,6 +7,13 @@ import {
7
7
  collectImportSpecifiers,
8
8
  extractJsSymbols,
9
9
  } from './js-source.mjs'
10
+ import {
11
+ collectGitignoreSets,
12
+ isIgnoredByGitignore,
13
+ readGitignoreRules,
14
+ shouldIgnoreRelativePath,
15
+ toPosix,
16
+ } from './scan-ignore.mjs'
10
17
  import {
11
18
  dataDir as defaultDataDir,
12
19
  targetName as defaultTargetName,
@@ -14,49 +21,78 @@ import {
14
21
  } from './target-config.mjs'
15
22
 
16
23
  const defaultOutPath = path.join(defaultDataDir, 'codebase.json')
17
- const IGNORE_DIRS = new Set([
18
- 'node_modules',
19
- 'dist',
20
- 'build',
21
- 'out',
22
- 'coverage',
23
- '.git',
24
- '.inbase',
25
- ])
26
- const IGNORE_FILES = new Set(['package-lock.json'])
27
24
  const BINARY_PROBE_BYTES = 8000
28
25
 
29
- function toPosix(filePath) {
30
- return filePath.split(path.sep).join('/')
26
+ function isBinaryFile(filePath) {
27
+ try {
28
+ const fd = fs.openSync(filePath, 'r')
29
+ try {
30
+ const buf = Buffer.alloc(BINARY_PROBE_BYTES)
31
+ const bytes = fs.readSync(fd, buf, 0, buf.length, 0)
32
+ return buf.subarray(0, bytes).includes(0)
33
+ } finally {
34
+ fs.closeSync(fd)
35
+ }
36
+ } catch {
37
+ return true
38
+ }
31
39
  }
32
40
 
33
- function isBinaryFile(filePath) {
34
- const fd = fs.openSync(filePath, 'r')
41
+ function resolvesThroughIgnored(absolutePath, root) {
35
42
  try {
36
- const buf = Buffer.alloc(BINARY_PROBE_BYTES)
37
- const bytes = fs.readSync(fd, buf, 0, buf.length, 0)
38
- return buf.subarray(0, bytes).includes(0)
39
- } finally {
40
- fs.closeSync(fd)
43
+ const real = fs.realpathSync(absolutePath)
44
+ const realRoot = fs.realpathSync(root)
45
+ if (!(real === realRoot || real.startsWith(realRoot + path.sep))) return true
46
+ return shouldIgnoreRelativePath(path.relative(realRoot, real))
47
+ } catch {
48
+ return true
41
49
  }
42
50
  }
43
51
 
44
- function walk(dir, acc = []) {
45
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
46
- if (entry.name.startsWith('.')) continue
47
- if (entry.isDirectory()) {
48
- if (IGNORE_DIRS.has(entry.name)) continue
49
- walk(path.join(dir, entry.name), acc)
52
+ function walk(dir, root, ignoreSets, acc = []) {
53
+ const localRules = readGitignoreRules(dir)
54
+ const nextSets = localRules.length
55
+ ? [...ignoreSets, { base: dir, rules: localRules }]
56
+ : ignoreSets
57
+ let entries
58
+ try {
59
+ entries = fs.readdirSync(dir, { withFileTypes: true })
60
+ } catch {
61
+ return acc
62
+ }
63
+ for (const entry of entries) {
64
+ const absolutePath = path.join(dir, entry.name)
65
+ const relative = toPosix(path.relative(root, absolutePath))
66
+ if (
67
+ shouldIgnoreRelativePath(relative) ||
68
+ resolvesThroughIgnored(absolutePath, root) ||
69
+ isIgnoredByGitignore(absolutePath, nextSets)
70
+ ) {
50
71
  continue
51
72
  }
52
- if (IGNORE_FILES.has(entry.name)) continue
53
- const absolutePath = path.join(dir, entry.name)
73
+ let stat = entry
74
+ if (entry.isSymbolicLink()) {
75
+ try {
76
+ stat = fs.statSync(absolutePath)
77
+ } catch {
78
+ continue
79
+ }
80
+ }
81
+ if (stat.isDirectory()) {
82
+ walk(absolutePath, root, nextSets, acc)
83
+ continue
84
+ }
85
+ if (!stat.isFile()) continue
54
86
  if (isBinaryFile(absolutePath)) continue
55
87
  acc.push(absolutePath)
56
88
  }
57
89
  return acc
58
90
  }
59
91
 
92
+ function listSourceAbsolutes(root) {
93
+ return walk(root, root, collectGitignoreSets(root))
94
+ }
95
+
60
96
  function languageOf(filePath) {
61
97
  const ext = path.extname(filePath).slice(1).toLowerCase()
62
98
  return ext || 'txt'
@@ -140,7 +176,9 @@ function ensureFolder(folders, folderPath, rootName) {
140
176
 
141
177
  export function listSourceFiles(root) {
142
178
  if (!root || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) return []
143
- return walk(root).map((absolutePath) => toPosix(path.relative(root, absolutePath)))
179
+ return listSourceAbsolutes(root).map((absolutePath) =>
180
+ toPosix(path.relative(root, absolutePath)),
181
+ )
144
182
  }
145
183
 
146
184
  export function scanTarget({
@@ -154,7 +192,7 @@ export function scanTarget({
154
192
  )
155
193
  }
156
194
 
157
- const absoluteFiles = walk(root)
195
+ const absoluteFiles = listSourceAbsolutes(root)
158
196
  const folders = new Map()
159
197
  ensureFolder(folders, '.', name)
160
198
 
@@ -133,6 +133,8 @@ export function readAttachedSession(dataDir: string): string | null
133
133
  export function listAttachQueue(dataDir: string): string[]
134
134
  export function nextAttachSessionId(dataDir: string): string | null
135
135
  export type SessionBlueprint = {
136
+ hidden: boolean
137
+ revision: number
136
138
  enabled: boolean
137
139
  sent: boolean
138
140
  userCreatedBlocks: unknown[]
@@ -140,14 +142,31 @@ export type SessionBlueprint = {
140
142
  addedFunctions: unknown[]
141
143
  addedVariables: unknown[]
142
144
  addedImports: unknown[]
145
+ notes: unknown[]
143
146
  }
144
147
  export function emptyBlueprint(): SessionBlueprint
145
- export function readBlueprint(dataDir: string, sessionId: string): SessionBlueprint
148
+ export function readBlueprint(dataDir: string, sessionId?: string): SessionBlueprint
149
+ export function writeBlueprint(
150
+ dataDir: string,
151
+ blueprint: SessionBlueprint,
152
+ ): SessionBlueprint
146
153
  export function writeBlueprint(
147
154
  dataDir: string,
148
155
  sessionId: string,
149
156
  blueprint: SessionBlueprint,
150
- ): void
157
+ ): SessionBlueprint
158
+ export function setBlueprintHidden(dataDir: string, hidden: boolean): SessionBlueprint
159
+ export function clearBlueprint(dataDir: string): SessionBlueprint
160
+ export function cleanupBlueprint(
161
+ dataDir: string,
162
+ knownFileIds?: string[],
163
+ knownFolderPaths?: string[],
164
+ ): SessionBlueprint
165
+ export function markBlueprintSeen(
166
+ dataDir: string,
167
+ sessionId: string,
168
+ revision: number,
169
+ ): DiffManifest | null
151
170
  export function answerBlueprint(
152
171
  dataDir: string,
153
172
  sessionId: string,
@@ -155,13 +174,14 @@ export function answerBlueprint(
155
174
  ): DiffManifest
156
175
  export function updateBlueprint(
157
176
  dataDir: string,
158
- sessionId: string,
177
+ sessionId?: string | null,
159
178
  input?: {
160
179
  userCreatedBlocks?: unknown[]
161
180
  userCreatedIslands?: unknown[]
162
181
  addedFunctions?: unknown[]
163
182
  addedVariables?: unknown[]
164
183
  addedImports?: unknown[]
184
+ notes?: unknown[]
165
185
  },
166
186
  ): SessionBlueprint
167
187
  export function sendBlueprint(
@@ -173,6 +193,7 @@ export function sendBlueprint(
173
193
  addedFunctions?: unknown[]
174
194
  addedVariables?: unknown[]
175
195
  addedImports?: unknown[]
196
+ notes?: unknown[]
176
197
  },
177
198
  ): DiffManifest
178
199
  export function maybeStartVisualizerHandshake(