@jkwd/inbase 0.1.4 → 0.1.5

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
@@ -1,4 +1,6 @@
1
- # Inbase
1
+ <p align="center">
2
+ <img src="docs/inbase-logo.png" alt="InBase — Dive into your codebase" width="520" />
3
+ </p>
2
4
 
3
5
  A first-person 3D map of a JavaScript or TypeScript codebase. Files become blocks, folders become walkable areas, and imports become lines in the air.
4
6
 
@@ -45,10 +45,30 @@ export type DiffManifest = {
45
45
  export function assertSessionId(value: unknown): string
46
46
  export function readActiveSession(dataDir: string): string | null
47
47
  export function writeActiveSession(dataDir: string, sessionId: string | null): void
48
+ export function touchSessionConnection(dataDir: string, sessionId: string): void
49
+ export function isSessionConnected(
50
+ dataDir: string,
51
+ sessionId: string,
52
+ waiterIds?: Set<string>,
53
+ ): boolean
54
+ export function listStoredSessionIds(dataDir: string): string[]
55
+ export function listOpenSessionIds(dataDir: string): string[]
56
+ export function discardInactiveDiffSessions(
57
+ dataDir: string,
58
+ targetRoot?: string | null,
59
+ waiterIds?: Iterable<string>,
60
+ ): string[]
61
+ export function listSessionIntents(
62
+ dataDir: string,
63
+ knownFileIds?: string[],
64
+ ): Array<Record<string, unknown>>
48
65
  export function readBlueprintSession(dataDir: string): string | null
49
66
  export function writeBlueprintSession(dataDir: string, sessionId: string | null): void
50
67
  export function readManifest(dataDir: string, sessionId: string): DiffManifest | null
51
68
  export function writeManifest(dataDir: string, manifest: DiffManifest): void
69
+ export function isSessionStopped(dataDir: string, sessionId: string): boolean
70
+ export function isWorkflowStopped(dataDir: string, sessionId: string): boolean
71
+ export function sessionStoppedError(sessionId: string): Error
52
72
  export function startSession(
53
73
  dataDir: string,
54
74
  input: {
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
4
5
  import {
5
6
  accumulatePatchAdditions,
6
7
  applyUnifiedPatch,
@@ -11,6 +12,7 @@ import {
11
12
  } from './patch-lib.mjs'
12
13
 
13
14
  const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
15
+ const CONNECTED_TTL_MS = 15_000
14
16
 
15
17
  export function assertSessionId(value) {
16
18
  if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
@@ -46,9 +48,48 @@ export function sessionPaths(dataDir, sessionId) {
46
48
  blueprint: path.join(root, 'blueprint.json'),
47
49
  baseline: path.join(root, 'baseline.json'),
48
50
  baselineFiles: path.join(root, 'baseline'),
51
+ stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
49
52
  }
50
53
  }
51
54
 
55
+ export function sessionStoppedError(sessionId) {
56
+ return new Error(
57
+ `VISUAL_CODER_STOPPED Session ${assertSessionId(sessionId)} was stopped. Do not modify project files.`,
58
+ )
59
+ }
60
+
61
+ export function isSessionStopped(dataDir, sessionId) {
62
+ return fs.existsSync(sessionPaths(dataDir, sessionId).stopped)
63
+ }
64
+
65
+ export function isWorkflowStopped(dataDir, sessionId) {
66
+ const safeId = assertSessionId(sessionId)
67
+ const manifest = readManifest(dataDir, safeId)
68
+ if (manifest?.phase === 'stopped') return true
69
+ return !manifest && isSessionStopped(dataDir, safeId)
70
+ }
71
+
72
+ function writeStoppedMarker(dataDir, sessionId) {
73
+ const { stopped } = sessionPaths(dataDir, sessionId)
74
+ atomicWrite(
75
+ stopped,
76
+ `${JSON.stringify({ sessionId: assertSessionId(sessionId), stoppedAt: new Date().toISOString() }, null, 2)}\n`,
77
+ )
78
+ }
79
+
80
+ function clearStoppedMarker(dataDir, sessionId) {
81
+ const { stopped } = sessionPaths(dataDir, sessionId)
82
+ if (fs.existsSync(stopped)) fs.unlinkSync(stopped)
83
+ }
84
+
85
+ function requireManifest(dataDir, sessionId, missingMessage) {
86
+ const safeId = assertSessionId(sessionId)
87
+ const manifest = readManifest(dataDir, safeId)
88
+ if (manifest) return manifest
89
+ if (isSessionStopped(dataDir, safeId)) throw sessionStoppedError(safeId)
90
+ throw new Error(missingMessage ?? `Unknown session ${safeId}`)
91
+ }
92
+
52
93
  export function resolveTargetFile(targetRoot, fileId) {
53
94
  if (typeof fileId !== 'string' || fileId.trim() === '') {
54
95
  throw new Error('fileId is required')
@@ -78,6 +119,138 @@ export function writeActiveSession(dataDir, sessionId) {
78
119
  )
79
120
  }
80
121
 
122
+ function connectionFile(dataDir, sessionId) {
123
+ return path.join(sessionPaths(dataDir, sessionId).root, 'connected.json')
124
+ }
125
+
126
+ function isFreshTimestamp(value, now = Date.now()) {
127
+ if (typeof value !== 'string') return false
128
+ const at = Date.parse(value)
129
+ return Number.isFinite(at) && now - at >= 0 && now - at < CONNECTED_TTL_MS
130
+ }
131
+
132
+ export function touchSessionConnection(dataDir, sessionId) {
133
+ const safeId = assertSessionId(sessionId)
134
+ if (isSessionStopped(dataDir, safeId)) return
135
+ atomicWrite(
136
+ connectionFile(dataDir, safeId),
137
+ `${JSON.stringify({ sessionId: safeId, connectedAt: new Date().toISOString() }, null, 2)}\n`,
138
+ )
139
+ }
140
+
141
+ function waiterSessionIds() {
142
+ try {
143
+ const result = spawnSync('ps', ['-ax', '-o', 'command='], {
144
+ encoding: 'utf8',
145
+ })
146
+ if (result.status !== 0 || !result.stdout) return new Set()
147
+ const ids = new Set()
148
+ for (const line of result.stdout.split('\n')) {
149
+ if (
150
+ !line.includes('wait-for-blueprint') &&
151
+ !line.includes('wait-for-approval')
152
+ ) {
153
+ continue
154
+ }
155
+ const match = line.match(/--session\s+(\S+)/)
156
+ if (!match) continue
157
+ try {
158
+ ids.add(assertSessionId(match[1]))
159
+ } catch {
160
+ // Ignore process command lines with invalid session ids.
161
+ }
162
+ }
163
+ return ids
164
+ } catch {
165
+ return new Set()
166
+ }
167
+ }
168
+
169
+ function isGeneratingPhase(phase) {
170
+ return phase === 'preparing' || phase === 'working' || phase === 'replanning'
171
+ }
172
+
173
+ export function isSessionConnected(
174
+ dataDir,
175
+ sessionId,
176
+ waiterIds = waiterSessionIds(),
177
+ ) {
178
+ const safeId = assertSessionId(sessionId)
179
+ const manifest = readManifest(dataDir, safeId)
180
+ if (!manifest) return false
181
+ if (
182
+ manifest.phase === 'finished' ||
183
+ manifest.phase === 'stopped' ||
184
+ manifest.status === 'finished' ||
185
+ manifest.status === 'rejected'
186
+ ) {
187
+ return false
188
+ }
189
+ if (isGeneratingPhase(manifest.phase)) return true
190
+ if (waiterIds.has(safeId)) return true
191
+ const connected = readJson(connectionFile(dataDir, safeId), null)
192
+ if (isFreshTimestamp(connected?.connectedAt)) return true
193
+ return isFreshTimestamp(manifest.updatedAt) || isFreshTimestamp(manifest.createdAt)
194
+ }
195
+
196
+ function diffSessionsRoot(dataDir) {
197
+ return path.join(dataDir, 'diff-sessions')
198
+ }
199
+
200
+ export function listStoredSessionIds(dataDir) {
201
+ const root = diffSessionsRoot(dataDir)
202
+ if (!fs.existsSync(root)) return []
203
+ const ids = new Set()
204
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
205
+ if (entry.name === '.gitkeep') continue
206
+ const name =
207
+ entry.isFile() && entry.name.endsWith('.stopped')
208
+ ? entry.name.slice(0, -'.stopped'.length)
209
+ : entry.name
210
+ try {
211
+ ids.add(assertSessionId(name))
212
+ } catch {
213
+ // Skip files that are not valid session ids.
214
+ }
215
+ }
216
+ return [...ids]
217
+ }
218
+
219
+ export function listOpenSessionIds(dataDir) {
220
+ const root = diffSessionsRoot(dataDir)
221
+ if (!fs.existsSync(root)) return []
222
+ const waiters = waiterSessionIds()
223
+ const sessions = []
224
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
225
+ if (!entry.isDirectory()) continue
226
+ try {
227
+ const sessionId = assertSessionId(entry.name)
228
+ if (!isSessionConnected(dataDir, sessionId, waiters)) continue
229
+ const manifest = readManifest(dataDir, sessionId)
230
+ if (!manifest) continue
231
+ sessions.push({
232
+ sessionId,
233
+ createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : '',
234
+ })
235
+ } catch {
236
+ // Skip folders that are not valid session ids.
237
+ }
238
+ }
239
+ sessions.sort((left, right) => {
240
+ if (left.createdAt !== right.createdAt) {
241
+ return left.createdAt.localeCompare(right.createdAt)
242
+ }
243
+ return left.sessionId.localeCompare(right.sessionId)
244
+ })
245
+ return sessions.map((item) => item.sessionId)
246
+ }
247
+
248
+ export function listSessionIntents(dataDir, knownFileIds = []) {
249
+ return listOpenSessionIds(dataDir)
250
+ .map((sessionId) => sessionIntent(dataDir, sessionId, knownFileIds))
251
+ .filter(Boolean)
252
+ }
253
+
81
254
  export function readBlueprintSession(dataDir) {
82
255
  const value = readJson(path.join(dataDir, 'blueprint-session.json'), null)
83
256
  return value?.sessionId ? assertSessionId(value.sessionId) : null
@@ -400,8 +573,51 @@ function replayPatches(dataDir, sessionId, targetRoot, entries) {
400
573
  }
401
574
  }
402
575
 
403
- function acceptedEntries(manifest) {
404
- return manifest.diffs.filter((entry) => entry.status === 'applied')
576
+ function gitTopLevel(fromDir) {
577
+ try {
578
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
579
+ cwd: fromDir,
580
+ encoding: 'utf8',
581
+ })
582
+ if (result.status !== 0) return null
583
+ const root = result.stdout.trim()
584
+ return root ? fs.realpathSync(root) : null
585
+ } catch {
586
+ return null
587
+ }
588
+ }
589
+
590
+ function repoRelativePath(root, absolutePath) {
591
+ const resolved = path.resolve(absolutePath)
592
+ let candidate = resolved
593
+ try {
594
+ if (fs.existsSync(resolved)) candidate = fs.realpathSync(resolved)
595
+ else if (fs.existsSync(path.dirname(resolved))) {
596
+ candidate = path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved))
597
+ }
598
+ } catch {
599
+ candidate = resolved
600
+ }
601
+ const relative = path.relative(root, candidate)
602
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null
603
+ return relative
604
+ }
605
+
606
+ function unstagePaths(fromDir, absolutePaths) {
607
+ if (!absolutePaths.length) return
608
+ const root = gitTopLevel(fromDir)
609
+ if (!root) return
610
+ const relative = [...new Set(absolutePaths)]
611
+ .map((item) => repoRelativePath(root, item))
612
+ .filter((item) => Boolean(item))
613
+ if (!relative.length) return
614
+ for (const item of relative) {
615
+ spawnSync('git', ['restore', '--staged', '--', item], {
616
+ cwd: root,
617
+ encoding: 'utf8',
618
+ stdio: 'ignore',
619
+ })
620
+ }
405
621
  }
406
622
 
407
623
  function liveEntries(manifest, diffId) {
@@ -409,8 +625,7 @@ function liveEntries(manifest, diffId) {
409
625
  }
410
626
 
411
627
  export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
412
- const manifest = readManifest(dataDir, assertSessionId(sessionId))
413
- if (!manifest) throw new Error(`Unknown session ${sessionId}`)
628
+ const manifest = requireManifest(dataDir, sessionId)
414
629
  const through = diffId || manifest.activeDiffId
415
630
  if (!through) return manifest
416
631
  restoreBaseline(dataDir, sessionId, targetRoot)
@@ -473,6 +688,7 @@ function featureName(value) {
473
688
 
474
689
  export function startSession(dataDir, input) {
475
690
  const sessionId = assertSessionId(input.sessionId)
691
+ clearStoppedMarker(dataDir, sessionId)
476
692
  const existing = readManifest(dataDir, sessionId)
477
693
  if (existing) {
478
694
  focusSession(dataDir, sessionId)
@@ -502,8 +718,7 @@ export function startSession(dataDir, input) {
502
718
  }
503
719
 
504
720
  export function answerBlueprint(dataDir, sessionId, enabled) {
505
- const manifest = readManifest(dataDir, assertSessionId(sessionId))
506
- if (!manifest) throw new Error(`Unknown session ${sessionId}`)
721
+ const manifest = requireManifest(dataDir, sessionId)
507
722
  if (manifest.phase !== 'blueprint_ask') {
508
723
  throw new Error(`Session ${sessionId} is not asking for a blueprint`)
509
724
  }
@@ -522,8 +737,7 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
522
737
 
523
738
  export function updateBlueprint(dataDir, sessionId, input = {}) {
524
739
  const safeId = assertSessionId(sessionId)
525
- const manifest = readManifest(dataDir, safeId)
526
- if (!manifest) throw new Error(`Unknown session ${safeId}`)
740
+ const manifest = requireManifest(dataDir, safeId)
527
741
  if (manifest.phase !== 'blueprint') {
528
742
  throw new Error(`Session ${safeId} is not in blueprint mode`)
529
743
  }
@@ -544,8 +758,7 @@ export function updateBlueprint(dataDir, sessionId, input = {}) {
544
758
 
545
759
  export function sendBlueprint(dataDir, sessionId, input = {}) {
546
760
  const safeId = assertSessionId(sessionId)
547
- const manifest = readManifest(dataDir, safeId)
548
- if (!manifest) throw new Error(`Unknown session ${safeId}`)
761
+ const manifest = requireManifest(dataDir, safeId)
549
762
  if (manifest.phase !== 'blueprint') {
550
763
  throw new Error(`Session ${safeId} is not in blueprint mode`)
551
764
  }
@@ -570,6 +783,9 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
570
783
  export function reportPlan(dataDir, input) {
571
784
  const sessionId = assertSessionId(input.sessionId)
572
785
  const existing = readManifest(dataDir, sessionId)
786
+ if (!existing && isSessionStopped(dataDir, sessionId)) {
787
+ throw sessionStoppedError(sessionId)
788
+ }
573
789
  const now = new Date().toISOString()
574
790
 
575
791
  if (existing?.phase === 'blueprint_ask' || existing?.phase === 'blueprint') {
@@ -623,8 +839,7 @@ export function reportPlan(dataDir, input) {
623
839
  }
624
840
 
625
841
  export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
626
- const manifest = readManifest(dataDir, assertSessionId(sessionId))
627
- if (!manifest) throw new Error(`Unknown session ${sessionId}`)
842
+ const manifest = requireManifest(dataDir, sessionId)
628
843
 
629
844
  if (manifest.phase === 'review') {
630
845
  if (!targetRoot) throw new Error('A target root is required to apply the current step')
@@ -665,8 +880,11 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
665
880
 
666
881
  export function appendDiff(dataDir, targetRoot, input) {
667
882
  const sessionId = assertSessionId(input.sessionId)
668
- const manifest = readManifest(dataDir, sessionId)
669
- if (!manifest) throw new Error(`Report a plan for session ${sessionId} first`)
883
+ const manifest = requireManifest(
884
+ dataDir,
885
+ sessionId,
886
+ `Report a plan for session ${sessionId} first`,
887
+ )
670
888
  if (manifest.phase !== 'working') {
671
889
  throw new Error(`Step ${manifest.currentStep} has not been invoked`)
672
890
  }
@@ -748,8 +966,7 @@ function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
748
966
  }
749
967
 
750
968
  export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
751
- const manifest = readManifest(dataDir, assertSessionId(sessionId))
752
- if (!manifest) throw new Error(`Unknown session ${sessionId}`)
969
+ const manifest = requireManifest(dataDir, sessionId)
753
970
  const active = pendingActive(manifest, diffId)
754
971
  applyUnresolved(dataDir, targetRoot, manifest, diffId)
755
972
 
@@ -769,8 +986,7 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
769
986
  }
770
987
 
771
988
  export function requestReplan(dataDir, sessionId, diffId, instruction) {
772
- const manifest = readManifest(dataDir, assertSessionId(sessionId))
773
- if (!manifest) throw new Error(`Unknown session ${sessionId}`)
989
+ const manifest = requireManifest(dataDir, sessionId)
774
990
  const active = pendingActive(manifest, diffId)
775
991
  const guidance = typeof instruction === 'string' ? instruction.trim() : ''
776
992
  if (!guidance) throw new Error('An alternative instruction is required')
@@ -784,14 +1000,91 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
784
1000
  return manifest
785
1001
  }
786
1002
 
787
- export function stopSession(dataDir, sessionId, targetRoot = null) {
1003
+ function unstageDiffSessionArtifacts(dataDir, targetRoot, extraPaths = []) {
1004
+ if (!targetRoot) return
1005
+ unstagePaths(targetRoot, [
1006
+ ...extraPaths,
1007
+ diffSessionsRoot(dataDir),
1008
+ path.join(dataDir, 'active-session.json'),
1009
+ path.join(dataDir, 'blueprint-session.json'),
1010
+ ])
1011
+ }
1012
+
1013
+ function discardStoredSession(
1014
+ dataDir,
1015
+ sessionId,
1016
+ targetRoot = null,
1017
+ { restore = true, keepStoppedMarker = false } = {},
1018
+ ) {
788
1019
  const safeId = assertSessionId(sessionId)
789
- const manifest = readManifest(dataDir, safeId)
790
- if (manifest && targetRoot) {
791
- restoreBaseline(dataDir, safeId, targetRoot)
792
- replayPatches(dataDir, safeId, targetRoot, acceptedEntries(manifest))
1020
+ const paths = sessionPaths(dataDir, safeId)
1021
+ const fileIds = Object.keys(readBaseline(dataDir, safeId).files)
1022
+ if (targetRoot && restore) {
1023
+ try {
1024
+ restoreBaseline(dataDir, safeId, targetRoot)
1025
+ } catch {
1026
+ // Incomplete session artifacts should still be deleted.
1027
+ }
1028
+ }
1029
+ if (targetRoot) {
1030
+ unstageDiffSessionArtifacts(
1031
+ dataDir,
1032
+ targetRoot,
1033
+ fileIds.flatMap((id) => {
1034
+ try {
1035
+ return [resolveTargetFile(targetRoot, id).absolute]
1036
+ } catch {
1037
+ return []
1038
+ }
1039
+ }),
1040
+ )
1041
+ }
1042
+ releaseBlueprintSession(dataDir, safeId)
1043
+ if (fs.existsSync(paths.root)) {
1044
+ fs.rmSync(paths.root, { recursive: true, force: true })
1045
+ }
1046
+ if (!keepStoppedMarker) clearStoppedMarker(dataDir, safeId)
1047
+ const active = readActiveSession(dataDir)
1048
+ if (active === safeId) writeActiveSession(dataDir, null)
1049
+ }
1050
+
1051
+ export function discardInactiveDiffSessions(
1052
+ dataDir,
1053
+ targetRoot = null,
1054
+ waiterIds = waiterSessionIds(),
1055
+ ) {
1056
+ const keep = new Set()
1057
+ for (const value of waiterIds) {
1058
+ try {
1059
+ keep.add(assertSessionId(value))
1060
+ } catch {
1061
+ // Ignore process command lines with invalid session ids.
1062
+ }
1063
+ }
1064
+
1065
+ for (const sessionId of listStoredSessionIds(dataDir)) {
1066
+ const live = keep.has(sessionId) && Boolean(readManifest(dataDir, sessionId))
1067
+ const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
1068
+ if (live || stopping) continue
1069
+ discardStoredSession(dataDir, sessionId, targetRoot)
793
1070
  }
794
- finalizeFinishedSession(dataDir, safeId)
1071
+
1072
+ const liveIds = [...keep].filter((id) => readManifest(dataDir, id))
1073
+ const active = readActiveSession(dataDir)
1074
+ if (active && !liveIds.includes(active)) writeActiveSession(dataDir, null)
1075
+ const locked = readBlueprintSession(dataDir)
1076
+ if (locked && !liveIds.includes(locked)) writeBlueprintSession(dataDir, null)
1077
+ unstageDiffSessionArtifacts(dataDir, targetRoot)
1078
+ return liveIds
1079
+ }
1080
+
1081
+ export function stopSession(dataDir, sessionId, targetRoot = null) {
1082
+ const safeId = assertSessionId(sessionId)
1083
+ writeStoppedMarker(dataDir, safeId)
1084
+ discardStoredSession(dataDir, safeId, targetRoot, { keepStoppedMarker: true })
1085
+ const waiters = waiterSessionIds()
1086
+ waiters.add(safeId)
1087
+ discardInactiveDiffSessions(dataDir, targetRoot, waiters)
795
1088
  return null
796
1089
  }
797
1090
 
@@ -819,14 +1112,7 @@ export function closeSession(dataDir, sessionId) {
819
1112
  }
820
1113
 
821
1114
  export function finalizeFinishedSession(dataDir, sessionId) {
822
- const safeId = assertSessionId(sessionId)
823
- const paths = sessionPaths(dataDir, safeId)
824
- releaseBlueprintSession(dataDir, safeId)
825
- if (fs.existsSync(paths.root)) {
826
- fs.rmSync(paths.root, { recursive: true, force: true })
827
- }
828
- const active = readActiveSession(dataDir)
829
- if (active === safeId) writeActiveSession(dataDir, null)
1115
+ discardStoredSession(dataDir, sessionId, null, { restore: false })
830
1116
  }
831
1117
 
832
1118
  export function emptyBlueprint() {