@jkwd/inbase 0.1.9 → 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 +23 -7
- 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 +6 -0
- package/apps/explorer/scripts/patch-lib.mjs +6 -0
- package/apps/explorer/scripts/scan-target.mjs +46 -13
- package/apps/explorer/scripts/session-store.d.ts +64 -1
- package/apps/explorer/scripts/session-store.mjs +369 -88
- package/apps/explorer/scripts/tree-diff.mjs +121 -0
- package/apps/explorer/src/App.tsx +215 -86
- package/apps/explorer/src/agentIntent.ts +78 -0
- package/apps/explorer/src/branchChanges.ts +54 -0
- package/apps/explorer/src/index.css +152 -10
- package/apps/explorer/src/scene/FileBlock.tsx +55 -5
- package/apps/explorer/src/types.ts +46 -0
- package/apps/explorer/src/ui/HUD.tsx +670 -158
- package/apps/explorer/src/userContext.ts +11 -0
- package/apps/explorer/vite.config.ts +65 -5
- 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 +142 -89
|
@@ -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,6 +6,8 @@ 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 {
|
|
@@ -19,7 +21,10 @@ import {
|
|
|
19
21
|
requestReplan,
|
|
20
22
|
sendBlueprint,
|
|
21
23
|
sessionIntent,
|
|
24
|
+
setInitialInstruction,
|
|
22
25
|
setStepByStep,
|
|
26
|
+
setupSession,
|
|
27
|
+
focusSession,
|
|
23
28
|
stopSession,
|
|
24
29
|
updateBlueprint,
|
|
25
30
|
} from './scripts/session-store.mjs'
|
|
@@ -84,8 +89,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
84
89
|
return {
|
|
85
90
|
name: 'visual-coder-json-files',
|
|
86
91
|
configureServer(server) {
|
|
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.
|
|
87
98
|
clearDiffSessions(dataDir, targetRoot)
|
|
88
|
-
rescanTarget('after
|
|
99
|
+
rescanTarget('after discarding leftover LLM sessions')
|
|
89
100
|
server.middlewares.use('/api/user-context', (req, res, next) => {
|
|
90
101
|
if (req.method === 'GET') {
|
|
91
102
|
sendJson(res, 200, readUserContext())
|
|
@@ -144,6 +155,14 @@ function jsonFilePlugin(): Plugin {
|
|
|
144
155
|
next()
|
|
145
156
|
})
|
|
146
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
|
+
|
|
147
166
|
server.middlewares.use('/api/inspect-file', (req, res, next) => {
|
|
148
167
|
if (req.method === 'POST') {
|
|
149
168
|
void inspectFile(req, res)
|
|
@@ -190,6 +209,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
190
209
|
sessionId?: string
|
|
191
210
|
diffId?: string
|
|
192
211
|
instruction?: string
|
|
212
|
+
name?: string
|
|
193
213
|
step?: number
|
|
194
214
|
stepByStep?: boolean
|
|
195
215
|
userCreatedBlocks?: unknown[]
|
|
@@ -208,12 +228,15 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
208
228
|
action !== 'blueprint_no' &&
|
|
209
229
|
action !== 'blueprint_send' &&
|
|
210
230
|
action !== 'blueprint_update' &&
|
|
211
|
-
action !== '
|
|
231
|
+
action !== 'focus' &&
|
|
232
|
+
action !== 'set_step_by_step' &&
|
|
233
|
+
action !== 'set_initial_instruction' &&
|
|
234
|
+
action !== 'setup_session'
|
|
212
235
|
) {
|
|
213
236
|
sendJson(res, 400, { error: 'invalid workflow action' })
|
|
214
237
|
return
|
|
215
238
|
}
|
|
216
|
-
if (!body.sessionId) {
|
|
239
|
+
if (action !== 'setup_session' && !body.sessionId) {
|
|
217
240
|
sendJson(res, 400, { error: 'sessionId is required' })
|
|
218
241
|
return
|
|
219
242
|
}
|
|
@@ -225,13 +248,20 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
225
248
|
return
|
|
226
249
|
}
|
|
227
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
|
+
|
|
228
259
|
if (action === 'invoke') {
|
|
229
260
|
if (!Number.isInteger(body.step)) {
|
|
230
261
|
sendJson(res, 400, { error: 'step is required for invoke' })
|
|
231
262
|
return
|
|
232
263
|
}
|
|
233
264
|
invokeStep(dataDir, body.sessionId, body.step as number, targetRoot)
|
|
234
|
-
rescanTarget('after invoking step')
|
|
235
265
|
} else if (action === 'continue') {
|
|
236
266
|
if (!body.diffId) {
|
|
237
267
|
sendJson(res, 400, { error: 'diffId is required for continue' })
|
|
@@ -249,7 +279,9 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
249
279
|
body.sessionId,
|
|
250
280
|
body.diffId,
|
|
251
281
|
body.instruction ?? '',
|
|
282
|
+
targetRoot,
|
|
252
283
|
)
|
|
284
|
+
rescanTarget('after withdrawing a patch')
|
|
253
285
|
} else if (action === 'blueprint_yes') {
|
|
254
286
|
answerBlueprint(dataDir, body.sessionId, true)
|
|
255
287
|
} else if (action === 'blueprint_no') {
|
|
@@ -270,6 +302,18 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
270
302
|
addedVariables: body.addedVariables,
|
|
271
303
|
addedImports: body.addedImports,
|
|
272
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 ?? '')
|
|
315
|
+
} else if (action === 'focus') {
|
|
316
|
+
focusSession(dataDir, body.sessionId)
|
|
273
317
|
} else if (action === 'set_step_by_step') {
|
|
274
318
|
if (typeof body.stepByStep !== 'boolean') {
|
|
275
319
|
sendJson(res, 400, { error: 'stepByStep is required' })
|
|
@@ -324,9 +368,10 @@ function readUserContext() {
|
|
|
324
368
|
return {
|
|
325
369
|
...parsed,
|
|
326
370
|
followLook: Boolean(parsed.followLook),
|
|
371
|
+
showBranchChanges: Boolean(parsed.showBranchChanges),
|
|
327
372
|
}
|
|
328
373
|
} catch {
|
|
329
|
-
return { followLook: false }
|
|
374
|
+
return { followLook: false, showBranchChanges: false }
|
|
330
375
|
}
|
|
331
376
|
}
|
|
332
377
|
|
|
@@ -341,6 +386,10 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
341
386
|
typeof incoming.followLook === 'boolean'
|
|
342
387
|
? incoming.followLook
|
|
343
388
|
: Boolean(existing.followLook),
|
|
389
|
+
showBranchChanges:
|
|
390
|
+
typeof incoming.showBranchChanges === 'boolean'
|
|
391
|
+
? incoming.showBranchChanges
|
|
392
|
+
: Boolean(existing.showBranchChanges),
|
|
344
393
|
}
|
|
345
394
|
delete next.userCreatedBlocks
|
|
346
395
|
delete next.userCreatedIslands
|
|
@@ -354,10 +403,21 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
|
|
|
354
403
|
}
|
|
355
404
|
}
|
|
356
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
|
+
|
|
357
412
|
export default defineConfig({
|
|
358
413
|
plugins: [react(), jsonFilePlugin()],
|
|
359
414
|
server: {
|
|
360
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
|
+
},
|
|
361
421
|
fs: {
|
|
362
422
|
allow: [here, dataDir],
|
|
363
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
|