@jkwd/inbase 0.1.10 → 0.1.11
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/LICENSE +21 -0
- package/README.md +22 -9
- package/apps/explorer/package.json +1 -0
- package/apps/explorer/scripts/branch-changes.d.ts +24 -0
- package/apps/explorer/scripts/branch-changes.mjs +237 -0
- package/apps/explorer/scripts/js-source.mjs +12 -15
- package/apps/explorer/scripts/patch-lib.d.ts +5 -0
- package/apps/explorer/scripts/patch-lib.mjs +5 -0
- package/apps/explorer/scripts/scan-target.mjs +46 -13
- package/apps/explorer/scripts/session-store.d.ts +58 -1
- package/apps/explorer/scripts/session-store.mjs +276 -52
- package/apps/explorer/scripts/tree-diff.mjs +121 -0
- package/apps/explorer/src/App.tsx +132 -27
- package/apps/explorer/src/agentIntent.ts +63 -0
- package/apps/explorer/src/branchChanges.ts +54 -0
- package/apps/explorer/src/index.css +124 -0
- package/apps/explorer/src/scene/FileBlock.tsx +55 -5
- package/apps/explorer/src/types.ts +44 -0
- package/apps/explorer/src/ui/HUD.tsx +576 -92
- package/apps/explorer/src/userContext.ts +11 -0
- package/apps/explorer/vite.config.ts +61 -7
- package/bin/inbase.mjs +23 -4
- package/bin/project.mjs +62 -4
- package/bin/session.mjs +215 -125
- package/package.json +20 -2
- package/skill/commands/inbase.md +17 -0
- package/skill/commands/skipinbase.md +9 -0
- package/skill/inbase/SKILL.md +141 -112
|
@@ -38,6 +38,16 @@ export function persistFollowLook(followLook: boolean) {
|
|
|
38
38
|
})
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function persistShowBranchChanges(showBranchChanges: boolean) {
|
|
42
|
+
fetch('/api/user-context', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: `${JSON.stringify({ showBranchChanges })}\n`,
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
lastWritten = ''
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export function persistUserContext(context: UserContext) {
|
|
42
52
|
pending = context
|
|
43
53
|
if (timer !== null) return
|
|
@@ -51,6 +61,7 @@ function flushUserContext() {
|
|
|
51
61
|
if (!context) return
|
|
52
62
|
const {
|
|
53
63
|
followLook: _followLook,
|
|
64
|
+
showBranchChanges: _showBranchChanges,
|
|
54
65
|
userCreatedBlocks: _userCreatedBlocks,
|
|
55
66
|
userCreatedIslands: _userCreatedIslands,
|
|
56
67
|
...gaze
|
|
@@ -6,11 +6,13 @@ import path from 'node:path'
|
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
8
|
import { emptyIntent } from './scripts/patch-lib.mjs'
|
|
9
|
+
import { readBranchChanges } from './scripts/branch-changes.mjs'
|
|
10
|
+
import { writeRunningInstance } from '../../bin/project.mjs'
|
|
9
11
|
import { dataDir, targetRoot } from './scripts/target-config.mjs'
|
|
10
12
|
import { editorFileUri, openInEditor } from './scripts/open-editor.mjs'
|
|
11
13
|
import {
|
|
12
14
|
answerBlueprint,
|
|
13
|
-
|
|
15
|
+
clearDiffSessions,
|
|
14
16
|
continueDiff,
|
|
15
17
|
inspectTargetFile,
|
|
16
18
|
invokeStep,
|
|
@@ -19,7 +21,9 @@ import {
|
|
|
19
21
|
requestReplan,
|
|
20
22
|
sendBlueprint,
|
|
21
23
|
sessionIntent,
|
|
24
|
+
setInitialInstruction,
|
|
22
25
|
setStepByStep,
|
|
26
|
+
setupSession,
|
|
23
27
|
focusSession,
|
|
24
28
|
stopSession,
|
|
25
29
|
updateBlueprint,
|
|
@@ -85,8 +89,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
85
89
|
return {
|
|
86
90
|
name: 'visual-coder-json-files',
|
|
87
91
|
configureServer(server) {
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
writeRunningInstance({
|
|
93
|
+
dataDir,
|
|
94
|
+
targetRoot,
|
|
95
|
+
port: server.config.server.port ?? 5173,
|
|
96
|
+
})
|
|
97
|
+
// Always boot with no LLM session. Leftover diffs and pointers are not restored.
|
|
98
|
+
clearDiffSessions(dataDir, targetRoot)
|
|
99
|
+
rescanTarget('after discarding leftover LLM sessions')
|
|
90
100
|
server.middlewares.use('/api/user-context', (req, res, next) => {
|
|
91
101
|
if (req.method === 'GET') {
|
|
92
102
|
sendJson(res, 200, readUserContext())
|
|
@@ -145,6 +155,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
145
155
|
next()
|
|
146
156
|
})
|
|
147
157
|
|
|
158
|
+
server.middlewares.use('/api/branch-changes', (req, res, next) => {
|
|
159
|
+
if (req.method === 'GET') {
|
|
160
|
+
sendJson(res, 200, readBranchChanges(targetRoot, knownFileIds()))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
next()
|
|
164
|
+
})
|
|
165
|
+
|
|
148
166
|
server.middlewares.use('/api/inspect-file', (req, res, next) => {
|
|
149
167
|
if (req.method === 'POST') {
|
|
150
168
|
void inspectFile(req, res)
|
|
@@ -191,6 +209,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
191
209
|
sessionId?: string
|
|
192
210
|
diffId?: string
|
|
193
211
|
instruction?: string
|
|
212
|
+
name?: string
|
|
194
213
|
step?: number
|
|
195
214
|
stepByStep?: boolean
|
|
196
215
|
userCreatedBlocks?: unknown[]
|
|
@@ -210,12 +229,14 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
210
229
|
action !== 'blueprint_send' &&
|
|
211
230
|
action !== 'blueprint_update' &&
|
|
212
231
|
action !== 'focus' &&
|
|
213
|
-
action !== 'set_step_by_step'
|
|
232
|
+
action !== 'set_step_by_step' &&
|
|
233
|
+
action !== 'set_initial_instruction' &&
|
|
234
|
+
action !== 'setup_session'
|
|
214
235
|
) {
|
|
215
236
|
sendJson(res, 400, { error: 'invalid workflow action' })
|
|
216
237
|
return
|
|
217
238
|
}
|
|
218
|
-
if (!body.sessionId) {
|
|
239
|
+
if (action !== 'setup_session' && !body.sessionId) {
|
|
219
240
|
sendJson(res, 400, { error: 'sessionId is required' })
|
|
220
241
|
return
|
|
221
242
|
}
|
|
@@ -227,13 +248,20 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
227
248
|
return
|
|
228
249
|
}
|
|
229
250
|
|
|
251
|
+
if (
|
|
252
|
+
body.name !== undefined &&
|
|
253
|
+
(typeof body.name !== 'string' || body.name.length > 200)
|
|
254
|
+
) {
|
|
255
|
+
sendJson(res, 400, { error: 'name must be a string up to 200 characters' })
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
if (action === 'invoke') {
|
|
231
260
|
if (!Number.isInteger(body.step)) {
|
|
232
261
|
sendJson(res, 400, { error: 'step is required for invoke' })
|
|
233
262
|
return
|
|
234
263
|
}
|
|
235
264
|
invokeStep(dataDir, body.sessionId, body.step as number, targetRoot)
|
|
236
|
-
rescanTarget('after invoking step')
|
|
237
265
|
} else if (action === 'continue') {
|
|
238
266
|
if (!body.diffId) {
|
|
239
267
|
sendJson(res, 400, { error: 'diffId is required for continue' })
|
|
@@ -274,6 +302,16 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
274
302
|
addedVariables: body.addedVariables,
|
|
275
303
|
addedImports: body.addedImports,
|
|
276
304
|
})
|
|
305
|
+
} else if (action === 'setup_session') {
|
|
306
|
+
const manifest = setupSession(dataDir, {
|
|
307
|
+
sessionId: body.sessionId,
|
|
308
|
+
name: body.name,
|
|
309
|
+
})
|
|
310
|
+
const next = sessionIntent(dataDir, manifest.sessionId, knownFileIds())
|
|
311
|
+
sendJson(res, 200, next ?? { ...emptyIntent })
|
|
312
|
+
return
|
|
313
|
+
} else if (action === 'set_initial_instruction') {
|
|
314
|
+
setInitialInstruction(dataDir, body.sessionId, body.instruction ?? '')
|
|
277
315
|
} else if (action === 'focus') {
|
|
278
316
|
focusSession(dataDir, body.sessionId)
|
|
279
317
|
} else if (action === 'set_step_by_step') {
|
|
@@ -330,9 +368,10 @@ function readUserContext() {
|
|
|
330
368
|
return {
|
|
331
369
|
...parsed,
|
|
332
370
|
followLook: Boolean(parsed.followLook),
|
|
371
|
+
showBranchChanges: Boolean(parsed.showBranchChanges),
|
|
333
372
|
}
|
|
334
373
|
} catch {
|
|
335
|
-
return { followLook: false }
|
|
374
|
+
return { followLook: false, showBranchChanges: false }
|
|
336
375
|
}
|
|
337
376
|
}
|
|
338
377
|
|
|
@@ -347,6 +386,10 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
347
386
|
typeof incoming.followLook === 'boolean'
|
|
348
387
|
? incoming.followLook
|
|
349
388
|
: Boolean(existing.followLook),
|
|
389
|
+
showBranchChanges:
|
|
390
|
+
typeof incoming.showBranchChanges === 'boolean'
|
|
391
|
+
? incoming.showBranchChanges
|
|
392
|
+
: Boolean(existing.showBranchChanges),
|
|
350
393
|
}
|
|
351
394
|
delete next.userCreatedBlocks
|
|
352
395
|
delete next.userCreatedIslands
|
|
@@ -360,10 +403,21 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
360
403
|
}
|
|
361
404
|
}
|
|
362
405
|
|
|
406
|
+
function isDataDirPath(filePath: string) {
|
|
407
|
+
const file = path.resolve(filePath)
|
|
408
|
+
const root = path.resolve(dataDir)
|
|
409
|
+
return file === root || file.startsWith(root + path.sep)
|
|
410
|
+
}
|
|
411
|
+
|
|
363
412
|
export default defineConfig({
|
|
364
413
|
plugins: [react(), jsonFilePlugin()],
|
|
365
414
|
server: {
|
|
366
415
|
port: 5173,
|
|
416
|
+
watch: {
|
|
417
|
+
// Session snapshots copy target source into the data dir. If Vite
|
|
418
|
+
// watches those writes, Create proposal full-reloads the visualizer.
|
|
419
|
+
ignored: ['**/src/data/**', isDataDirPath],
|
|
420
|
+
},
|
|
367
421
|
fs: {
|
|
368
422
|
allow: [here, dataDir],
|
|
369
423
|
},
|
package/bin/inbase.mjs
CHANGED
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
ensureGitignoreEntry,
|
|
11
11
|
explorerRoot,
|
|
12
12
|
skillTemplateDir,
|
|
13
|
+
commandTemplateDir,
|
|
13
14
|
takeFlagValue,
|
|
14
15
|
} from './project.mjs'
|
|
15
16
|
import {
|
|
16
17
|
proposePatch,
|
|
17
18
|
reportPlan,
|
|
18
19
|
startSession,
|
|
20
|
+
attachSession,
|
|
19
21
|
waitForApproval,
|
|
20
22
|
waitForBlueprint,
|
|
21
23
|
} from './session.mjs'
|
|
@@ -28,11 +30,12 @@ Usage:
|
|
|
28
30
|
inbase help Show this help
|
|
29
31
|
|
|
30
32
|
Agent commands (used by the Cursor skill):
|
|
31
|
-
inbase start-session --session <id>
|
|
33
|
+
inbase start-session --session <id> --name "short name"
|
|
34
|
+
inbase attach [--session <id>]
|
|
32
35
|
inbase wait-for-blueprint --session <id>
|
|
33
36
|
inbase report-plan --session <id> --feature "name" --steps "one" [--steps "two"]
|
|
34
37
|
inbase wait-for-approval --session <id>
|
|
35
|
-
inbase propose-patch --session <id>
|
|
38
|
+
inbase propose-patch --session <id> [file.patch|-]
|
|
36
39
|
inbase propose-patch --session <id> --clear
|
|
37
40
|
|
|
38
41
|
Options for run:
|
|
@@ -50,6 +53,10 @@ export function initProject(projectRoot = process.cwd()) {
|
|
|
50
53
|
}
|
|
51
54
|
const skillDir = path.join(projectRoot, '.cursor/skills/inbase')
|
|
52
55
|
copyDir(skillTemplateDir, skillDir)
|
|
56
|
+
const commandDir = path.join(projectRoot, '.cursor/commands')
|
|
57
|
+
if (fs.existsSync(commandTemplateDir)) {
|
|
58
|
+
copyDir(commandTemplateDir, commandDir)
|
|
59
|
+
}
|
|
53
60
|
const { dataDir } = applyHostEnv({
|
|
54
61
|
cwd: projectRoot,
|
|
55
62
|
target: projectRoot,
|
|
@@ -57,7 +64,7 @@ export function initProject(projectRoot = process.cwd()) {
|
|
|
57
64
|
})
|
|
58
65
|
ensureDataDir(dataDir)
|
|
59
66
|
const gitignoreAdded = ensureGitignoreEntry(projectRoot)
|
|
60
|
-
return { skillDir, dataDir, gitignoreAdded }
|
|
67
|
+
return { skillDir, commandDir, dataDir, gitignoreAdded }
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function explorerHref(relative) {
|
|
@@ -122,6 +129,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
122
129
|
if (command === 'init') {
|
|
123
130
|
const result = initProject()
|
|
124
131
|
console.log(`Installed Cursor skill at ${result.skillDir}`)
|
|
132
|
+
if (fs.existsSync(path.join(result.commandDir, 'inbase.md'))) {
|
|
133
|
+
console.log(`Installed /inbase command at ${result.commandDir}`)
|
|
134
|
+
}
|
|
125
135
|
if (result.gitignoreAdded) console.log('Added .inbase/ to .gitignore')
|
|
126
136
|
console.log('Next: run `inbase run`, then ask Cursor to change source files.')
|
|
127
137
|
return
|
|
@@ -132,13 +142,22 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
132
142
|
return
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
applyHostEnv()
|
|
145
|
+
const host = applyHostEnv()
|
|
136
146
|
ensureDataDir(process.env.INBASE_DATA_DIR)
|
|
147
|
+
if (host.instance) {
|
|
148
|
+
console.log(
|
|
149
|
+
`INBASE_ATTACHED Using the running visualizer (${host.instance.dataDir}). Run wait-for-blueprint to read the optional blueprint; it does not wait.`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
137
152
|
|
|
138
153
|
if (command === 'start-session') {
|
|
139
154
|
await startSession(args)
|
|
140
155
|
return
|
|
141
156
|
}
|
|
157
|
+
if (command === 'attach') {
|
|
158
|
+
await attachSession(args)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
142
161
|
if (command === 'wait-for-blueprint') {
|
|
143
162
|
await waitForBlueprint(args)
|
|
144
163
|
return
|
package/bin/project.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const here = path.dirname(fileURLToPath(import.meta.url))
|
|
|
6
6
|
export const packageRoot = path.resolve(here, '..')
|
|
7
7
|
export const explorerRoot = path.join(packageRoot, 'apps/explorer')
|
|
8
8
|
export const skillTemplateDir = path.join(packageRoot, 'skill/inbase')
|
|
9
|
+
export const commandTemplateDir = path.join(packageRoot, 'skill/commands')
|
|
9
10
|
|
|
10
11
|
export function resolveOptionalPath(value, fallback) {
|
|
11
12
|
const raw = value?.trim()
|
|
@@ -13,19 +14,76 @@ export function resolveOptionalPath(value, fallback) {
|
|
|
13
14
|
return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(process.cwd(), raw)
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export const INSTANCE_FILE = 'instance.json'
|
|
18
|
+
|
|
19
|
+
function isPidAlive(pid) {
|
|
20
|
+
if (!Number.isInteger(pid)) return true
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function instanceFile(dataDir) {
|
|
30
|
+
return path.join(dataDir, INSTANCE_FILE)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeRunningInstance({ dataDir, targetRoot, port = null }) {
|
|
34
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
35
|
+
const instance = {
|
|
36
|
+
dataDir: path.resolve(dataDir),
|
|
37
|
+
targetRoot: path.resolve(targetRoot),
|
|
38
|
+
port: port ?? null,
|
|
39
|
+
pid: process.pid,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(instanceFile(dataDir), `${JSON.stringify(instance, null, 2)}\n`)
|
|
43
|
+
return instance
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readInstanceFile(file) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
49
|
+
if (!parsed?.dataDir || !parsed?.targetRoot) return null
|
|
50
|
+
if (!isPidAlive(parsed.pid)) return null
|
|
51
|
+
return parsed
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readRunningInstance(cwd = process.cwd()) {
|
|
58
|
+
const files = [
|
|
59
|
+
path.join(cwd, '.inbase', INSTANCE_FILE),
|
|
60
|
+
path.join(explorerRoot, 'src/data', INSTANCE_FILE),
|
|
61
|
+
]
|
|
62
|
+
const seen = new Set()
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const resolved = path.resolve(file)
|
|
65
|
+
if (seen.has(resolved) || !fs.existsSync(resolved)) continue
|
|
66
|
+
seen.add(resolved)
|
|
67
|
+
const instance = readInstanceFile(resolved)
|
|
68
|
+
if (instance) return instance
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
16
73
|
export function applyHostEnv({
|
|
17
74
|
cwd = process.cwd(),
|
|
18
75
|
target = process.env.VISUAL_CODER_TARGET,
|
|
19
76
|
dataDir = process.env.INBASE_DATA_DIR,
|
|
20
77
|
} = {}) {
|
|
21
|
-
const
|
|
78
|
+
const running = !target && !dataDir ? readRunningInstance(cwd) : null
|
|
79
|
+
const targetRoot = resolveOptionalPath(target, running?.targetRoot ?? cwd)
|
|
22
80
|
const resolvedDataDir = resolveOptionalPath(
|
|
23
81
|
dataDir,
|
|
24
|
-
path.join(targetRoot, '.inbase'),
|
|
82
|
+
running?.dataDir ?? path.join(targetRoot, '.inbase'),
|
|
25
83
|
)
|
|
26
84
|
process.env.VISUAL_CODER_TARGET = targetRoot
|
|
27
85
|
process.env.INBASE_DATA_DIR = resolvedDataDir
|
|
28
|
-
return { cwd, targetRoot, dataDir: resolvedDataDir }
|
|
86
|
+
return { cwd, targetRoot, dataDir: resolvedDataDir, instance: running }
|
|
29
87
|
}
|
|
30
88
|
|
|
31
89
|
export function ensureDataDir(dataDir) {
|
|
@@ -34,7 +92,7 @@ export function ensureDataDir(dataDir) {
|
|
|
34
92
|
if (!fs.existsSync(userContextFile)) {
|
|
35
93
|
fs.writeFileSync(
|
|
36
94
|
userContextFile,
|
|
37
|
-
`${JSON.stringify({ followLook: false }, null, 2)}\n`,
|
|
95
|
+
`${JSON.stringify({ followLook: false, showBranchChanges: false }, null, 2)}\n`,
|
|
38
96
|
)
|
|
39
97
|
}
|
|
40
98
|
return dataDir
|