@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.
@@ -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 clearing diff sessions')
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 !== 'set_step_by_step'
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> [--feature "name"]
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> <file.patch|->
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 targetRoot = resolveOptionalPath(target, cwd)
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