contextgit 0.0.2 → 0.0.4

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.
Files changed (163) hide show
  1. package/dist/bootstrap.d.ts +10 -0
  2. package/dist/bootstrap.d.ts.map +1 -0
  3. package/dist/bootstrap.js +43 -0
  4. package/dist/bootstrap.js.map +1 -0
  5. package/dist/commands/branch.d.ts +13 -0
  6. package/dist/commands/branch.d.ts.map +1 -0
  7. package/dist/commands/branch.js +52 -0
  8. package/dist/commands/branch.js.map +1 -0
  9. package/dist/commands/claim.d.ts +15 -0
  10. package/dist/commands/claim.d.ts.map +1 -0
  11. package/dist/commands/claim.js +60 -0
  12. package/dist/commands/claim.js.map +1 -0
  13. package/dist/commands/commit.d.ts +17 -0
  14. package/dist/commands/commit.d.ts.map +1 -0
  15. package/dist/commands/commit.js +83 -0
  16. package/dist/commands/commit.js.map +1 -0
  17. package/dist/commands/context.d.ts +9 -0
  18. package/dist/commands/context.d.ts.map +1 -0
  19. package/dist/commands/context.js +38 -0
  20. package/dist/commands/context.js.map +1 -0
  21. package/dist/commands/doctor.d.ts +6 -0
  22. package/dist/commands/doctor.d.ts.map +1 -0
  23. package/dist/commands/doctor.js +84 -0
  24. package/dist/commands/doctor.js.map +1 -0
  25. package/dist/commands/init.d.ts +10 -0
  26. package/dist/commands/init.d.ts.map +1 -0
  27. package/dist/commands/init.js +184 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/keygen.d.ts +10 -0
  30. package/dist/commands/keygen.d.ts.map +1 -0
  31. package/dist/commands/keygen.js +57 -0
  32. package/dist/commands/keygen.js.map +1 -0
  33. package/dist/commands/log.d.ts +13 -0
  34. package/dist/commands/log.d.ts.map +1 -0
  35. package/dist/commands/log.js +91 -0
  36. package/dist/commands/log.js.map +1 -0
  37. package/dist/commands/merge.d.ts +12 -0
  38. package/dist/commands/merge.d.ts.map +1 -0
  39. package/dist/commands/merge.js +29 -0
  40. package/dist/commands/merge.js.map +1 -0
  41. package/dist/commands/pull.d.ts +10 -0
  42. package/dist/commands/pull.d.ts.map +1 -0
  43. package/dist/commands/pull.js +123 -0
  44. package/dist/commands/pull.js.map +1 -0
  45. package/dist/commands/push.d.ts +10 -0
  46. package/dist/commands/push.d.ts.map +1 -0
  47. package/dist/commands/push.js +141 -0
  48. package/dist/commands/push.js.map +1 -0
  49. package/dist/commands/remote-show.d.ts +6 -0
  50. package/dist/commands/remote-show.d.ts.map +1 -0
  51. package/dist/commands/remote-show.js +71 -0
  52. package/dist/commands/remote-show.js.map +1 -0
  53. package/dist/commands/search.d.ts +11 -0
  54. package/dist/commands/search.d.ts.map +1 -0
  55. package/dist/commands/search.js +47 -0
  56. package/dist/commands/search.js.map +1 -0
  57. package/dist/commands/serve.d.ts +9 -0
  58. package/dist/commands/serve.d.ts.map +1 -0
  59. package/dist/commands/serve.js +51 -0
  60. package/dist/commands/serve.js.map +1 -0
  61. package/dist/commands/set-remote.d.ts +9 -0
  62. package/dist/commands/set-remote.d.ts.map +1 -0
  63. package/dist/commands/set-remote.js +26 -0
  64. package/dist/commands/set-remote.js.map +1 -0
  65. package/dist/commands/status.d.ts +6 -0
  66. package/dist/commands/status.d.ts.map +1 -0
  67. package/dist/commands/status.js +54 -0
  68. package/dist/commands/status.js.map +1 -0
  69. package/dist/commands/unclaim.d.ts +9 -0
  70. package/dist/commands/unclaim.d.ts.map +1 -0
  71. package/dist/commands/unclaim.js +22 -0
  72. package/dist/commands/unclaim.js.map +1 -0
  73. package/dist/config.d.ts +19 -0
  74. package/dist/config.d.ts.map +1 -0
  75. package/dist/config.js +58 -0
  76. package/dist/config.js.map +1 -0
  77. package/dist/git-hooks.d.ts +6 -0
  78. package/dist/git-hooks.d.ts.map +1 -0
  79. package/dist/git-hooks.js +58 -0
  80. package/dist/git-hooks.js.map +1 -0
  81. package/package.json +24 -18
  82. package/.claude/settings.local.json +0 -41
  83. package/.contextgit/config.json +0 -10
  84. package/.contextgit/system-prompt.md +0 -4
  85. package/.github/workflows/contextgit-ci.yml +0 -40
  86. package/CLAUDE.md +0 -123
  87. package/CLAUDE.md.next +0 -65
  88. package/docs/ContextGit_ARCHITECTURE_v3.md +0 -1141
  89. package/docs/ContextGit_DELTA.md +0 -84
  90. package/docs/ContextGit_PHASE1_PLAN.md +0 -177
  91. package/docs/ContextGit_PHASE2_PLAN.md +0 -535
  92. package/docs/ContextGit_PRD_v4.md +0 -488
  93. package/docs/decisions.md +0 -370
  94. package/packages/api/package.json +0 -25
  95. package/packages/api/src/bootstrap.ts +0 -64
  96. package/packages/api/src/config.ts +0 -45
  97. package/packages/api/src/index.ts +0 -17
  98. package/packages/api/src/middleware/auth.test.ts +0 -83
  99. package/packages/api/src/middleware/auth.ts +0 -41
  100. package/packages/api/src/remote-store.test.ts +0 -301
  101. package/packages/api/src/router.ts +0 -121
  102. package/packages/api/src/server-config.ts +0 -34
  103. package/packages/api/src/server.ts +0 -38
  104. package/packages/api/src/store-router.ts +0 -241
  105. package/packages/api/tsconfig.json +0 -8
  106. package/packages/cli/package.json +0 -29
  107. package/packages/cli/src/bootstrap.ts +0 -68
  108. package/packages/cli/src/commands/branch.ts +0 -58
  109. package/packages/cli/src/commands/claim.ts +0 -58
  110. package/packages/cli/src/commands/commit.ts +0 -79
  111. package/packages/cli/src/commands/context.ts +0 -46
  112. package/packages/cli/src/commands/doctor.ts +0 -99
  113. package/packages/cli/src/commands/init.ts +0 -141
  114. package/packages/cli/src/commands/keygen.ts +0 -65
  115. package/packages/cli/src/commands/log.ts +0 -103
  116. package/packages/cli/src/commands/merge.ts +0 -36
  117. package/packages/cli/src/commands/pull.ts +0 -145
  118. package/packages/cli/src/commands/push.ts +0 -158
  119. package/packages/cli/src/commands/remote-show.ts +0 -87
  120. package/packages/cli/src/commands/search.ts +0 -54
  121. package/packages/cli/src/commands/serve.ts +0 -61
  122. package/packages/cli/src/commands/set-remote.ts +0 -30
  123. package/packages/cli/src/commands/status.ts +0 -62
  124. package/packages/cli/src/commands/unclaim.ts +0 -28
  125. package/packages/cli/src/config.ts +0 -64
  126. package/packages/cli/src/git-hooks.ts +0 -61
  127. package/packages/cli/tsconfig.json +0 -9
  128. package/packages/core/package.json +0 -28
  129. package/packages/core/src/embeddings.test.ts +0 -58
  130. package/packages/core/src/embeddings.ts +0 -75
  131. package/packages/core/src/engine.ts +0 -274
  132. package/packages/core/src/index.ts +0 -6
  133. package/packages/core/src/snapshot.ts +0 -82
  134. package/packages/core/src/summarizer.test.ts +0 -120
  135. package/packages/core/src/summarizer.ts +0 -113
  136. package/packages/core/src/threads.ts +0 -29
  137. package/packages/core/src/types.ts +0 -240
  138. package/packages/core/tsconfig.json +0 -9
  139. package/packages/mcp/package.json +0 -31
  140. package/packages/mcp/src/auto-snapshot.ts +0 -83
  141. package/packages/mcp/src/config.ts +0 -53
  142. package/packages/mcp/src/git-sync.ts +0 -94
  143. package/packages/mcp/src/index.ts +0 -19
  144. package/packages/mcp/src/server.ts +0 -377
  145. package/packages/mcp/tsconfig.json +0 -9
  146. package/packages/store/package.json +0 -30
  147. package/packages/store/src/branch-merge.test.ts +0 -127
  148. package/packages/store/src/engine-integration.test.ts +0 -93
  149. package/packages/store/src/index.ts +0 -3
  150. package/packages/store/src/interface.ts +0 -62
  151. package/packages/store/src/local/claims.test.ts +0 -190
  152. package/packages/store/src/local/index.ts +0 -380
  153. package/packages/store/src/local/local-store.test.ts +0 -164
  154. package/packages/store/src/local/migrations.ts +0 -99
  155. package/packages/store/src/local/queries.ts +0 -760
  156. package/packages/store/src/local/schema.ts +0 -157
  157. package/packages/store/src/remote/index.ts +0 -300
  158. package/packages/store/tsconfig.json +0 -9
  159. package/pnpm-workspace.yaml +0 -2
  160. package/scripts/build.sh +0 -28
  161. package/tsconfig.base.json +0 -14
  162. package/vitest.config.ts +0 -15
  163. /package/{packages/cli/bin → bin}/run.js +0 -0
@@ -1,41 +0,0 @@
1
- // auth.ts — Bearer token authentication middleware
2
- //
3
- // Accepts a pre-computed SHA-256 hex digest of the API key (keyHashHex).
4
- // Every request must include an `Authorization: Bearer <key>` header whose
5
- // SHA-256 hash matches the stored digest.
6
- // When keyHashHex is absent the server runs in dev mode (all requests pass).
7
-
8
- import { createHash, timingSafeEqual } from 'crypto'
9
- import type { Request, Response, NextFunction } from 'express'
10
-
11
- function sha256(input: string): Buffer {
12
- return createHash('sha256').update(input, 'utf8').digest()
13
- }
14
-
15
- export function createAuthMiddleware(keyHashHex: string | undefined) {
16
- if (!keyHashHex) {
17
- // Dev mode — no key configured, pass all requests
18
- return (_req: Request, _res: Response, next: NextFunction): void => next()
19
- }
20
-
21
- const expectedHash = Buffer.from(keyHashHex, 'hex')
22
-
23
- return (req: Request, res: Response, next: NextFunction): void => {
24
- const authHeader = req.headers['authorization']
25
-
26
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
27
- res.status(401).json({ error: 'Authorization required' })
28
- return
29
- }
30
-
31
- const token = authHeader.slice(7)
32
- const tokenHash = sha256(token)
33
-
34
- if (tokenHash.length !== expectedHash.length || !timingSafeEqual(tokenHash, expectedHash)) {
35
- res.status(401).json({ error: 'Invalid API key' })
36
- return
37
- }
38
-
39
- next()
40
- }
41
- }
@@ -1,301 +0,0 @@
1
- // Integration test: RemoteStore ↔ LocalStore
2
- //
3
- // Scenario: spin up an Express server with the store router backed by an
4
- // in-memory LocalStore, then run CRUD through a RemoteStore client and verify
5
- // the results match what LocalStore returns directly.
6
-
7
- import { describe, it, expect, beforeAll, afterAll } from 'vitest'
8
- import express from 'express'
9
- import http from 'http'
10
- import type { AddressInfo } from 'net'
11
- import { LocalStore, RemoteStore } from '@contextgit/store'
12
- import { createStoreRouter } from './store-router.js'
13
-
14
- describe('RemoteStore ↔ LocalStore integration', () => {
15
- let server: http.Server
16
- let localStore: LocalStore
17
- let remote: RemoteStore
18
-
19
- beforeAll(async () => {
20
- localStore = new LocalStore(':memory:')
21
-
22
- const app = express()
23
- app.use(express.json())
24
- app.use('/v1/store', createStoreRouter(localStore))
25
-
26
- server = http.createServer(app)
27
- const port = await new Promise<number>(resolve =>
28
- server.listen(0, () => resolve((server.address() as AddressInfo).port)),
29
- )
30
-
31
- remote = new RemoteStore(`http://localhost:${port}`)
32
- })
33
-
34
- afterAll(() => {
35
- return new Promise<void>(resolve => {
36
- server.close(() => {
37
- localStore.close()
38
- resolve()
39
- })
40
- })
41
- })
42
-
43
- it('creates and retrieves a project via RemoteStore', async () => {
44
- const project = await remote.createProject({ name: 'remote-test', description: 'HTTP project' })
45
- expect(project.id).toBeTruthy()
46
- expect(project.name).toBe('remote-test')
47
- expect(project.createdAt).toBeInstanceOf(Date)
48
-
49
- // Directly confirm it's in LocalStore
50
- const local = await localStore.getProject(project.id)
51
- expect(local?.name).toBe('remote-test')
52
- })
53
-
54
- it('creates a branch and retrieves by git name', async () => {
55
- const project = await remote.createProject({ name: 'branch-proj' })
56
- const branch = await remote.createBranch({
57
- projectId: project.id,
58
- name: 'main',
59
- gitBranch: 'main',
60
- })
61
- expect(branch.id).toBeTruthy()
62
- expect(branch.gitBranch).toBe('main')
63
- expect(branch.createdAt).toBeInstanceOf(Date)
64
-
65
- const byGit = await remote.getBranchByGitName(project.id, 'main')
66
- expect(byGit?.id).toBe(branch.id)
67
-
68
- const notFound = await remote.getBranchByGitName(project.id, 'nonexistent')
69
- expect(notFound).toBeNull()
70
- })
71
-
72
- it('creates a commit and retrieves it', async () => {
73
- const project = await remote.createProject({ name: 'commit-proj' })
74
- const branch = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
75
-
76
- await remote.upsertAgent({
77
- id: 'agent-remote',
78
- projectId: project.id,
79
- role: 'dev',
80
- tool: 'test',
81
- workflowType: 'interactive',
82
- })
83
-
84
- const commit = await remote.createCommit({
85
- branchId: branch.id,
86
- agentId: 'agent-remote',
87
- agentRole: 'dev',
88
- tool: 'test',
89
- workflowType: 'interactive',
90
- message: 'Remote commit',
91
- content: 'Content via HTTP',
92
- summary: 'Summary',
93
- commitType: 'manual',
94
- })
95
- expect(commit.id).toBeTruthy()
96
- expect(commit.message).toBe('Remote commit')
97
- expect(commit.createdAt).toBeInstanceOf(Date)
98
-
99
- const fetched = await remote.getCommit(commit.id)
100
- expect(fetched?.message).toBe('Remote commit')
101
- })
102
-
103
- it('getFormattedSnapshot via RemoteStore matches LocalStore output', async () => {
104
- const project = await remote.createProject({ name: 'snap-proj' })
105
- const branch = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
106
-
107
- await remote.upsertAgent({
108
- id: 'snap-agent',
109
- projectId: project.id,
110
- role: 'dev',
111
- tool: 'test',
112
- workflowType: 'interactive',
113
- })
114
-
115
- await remote.createCommit({
116
- branchId: branch.id,
117
- agentId: 'snap-agent',
118
- agentRole: 'dev',
119
- tool: 'test',
120
- workflowType: 'interactive',
121
- message: 'First commit',
122
- content: 'Setting things up',
123
- summary: 'Project scaffolded',
124
- commitType: 'manual',
125
- })
126
-
127
- const remoteSnapshot = await remote.getFormattedSnapshot(project.id, branch.id, 'text')
128
- const localSnapshot = await localStore.getFormattedSnapshot(project.id, branch.id, 'text')
129
-
130
- // Both should contain the expected sections
131
- expect(remoteSnapshot).toContain('=== PROJECT STATE ===')
132
- expect(remoteSnapshot).toContain('Project scaffolded')
133
-
134
- // RemoteStore and LocalStore return the same formatted text
135
- expect(remoteSnapshot).toBe(localSnapshot)
136
- })
137
-
138
- it('open threads round-trip via RemoteStore', async () => {
139
- const project = await remote.createProject({ name: 'thread-proj' })
140
- const branch = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
141
-
142
- await remote.upsertAgent({
143
- id: 'thread-agent',
144
- projectId: project.id,
145
- role: 'dev',
146
- tool: 'test',
147
- workflowType: 'interactive',
148
- })
149
-
150
- await remote.createCommit({
151
- branchId: branch.id,
152
- agentId: 'thread-agent',
153
- agentRole: 'dev',
154
- tool: 'test',
155
- workflowType: 'interactive',
156
- message: 'Open a thread',
157
- content: 'Some work',
158
- summary: 'Work in progress',
159
- commitType: 'manual',
160
- threads: { open: ['Which auth approach to use?'] },
161
- })
162
-
163
- const threads = await remote.listOpenThreads(project.id)
164
- expect(threads).toHaveLength(1)
165
- expect(threads[0].description).toBe('Which auth approach to use?')
166
- expect(threads[0].createdAt).toBeInstanceOf(Date)
167
- })
168
-
169
- it('fullTextSearch returns results via RemoteStore', async () => {
170
- const project = await remote.createProject({ name: 'search-proj' })
171
- const branch = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
172
-
173
- await remote.upsertAgent({
174
- id: 'search-agent',
175
- projectId: project.id,
176
- role: 'dev',
177
- tool: 'test',
178
- workflowType: 'interactive',
179
- })
180
-
181
- await remote.createCommit({
182
- branchId: branch.id,
183
- agentId: 'search-agent',
184
- agentRole: 'dev',
185
- tool: 'test',
186
- workflowType: 'interactive',
187
- message: 'Add authentication',
188
- content: 'Implemented JWT RS256 for authentication',
189
- summary: 'JWT auth done',
190
- commitType: 'manual',
191
- })
192
-
193
- const results = await remote.fullTextSearch('JWT', project.id)
194
- expect(results.length).toBeGreaterThan(0)
195
- expect(results[0].commit.createdAt).toBeInstanceOf(Date)
196
- expect(results[0].matchType).toBe('fulltext')
197
- })
198
-
199
- it('mergeBranch carries threads via RemoteStore', async () => {
200
- const project = await remote.createProject({ name: 'merge-proj' })
201
- const main = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
202
- const feat = await remote.createBranch({
203
- projectId: project.id,
204
- name: 'feat',
205
- gitBranch: 'feat',
206
- parentBranchId: main.id,
207
- })
208
-
209
- await remote.upsertAgent({
210
- id: 'merge-agent',
211
- projectId: project.id,
212
- role: 'dev',
213
- tool: 'test',
214
- workflowType: 'interactive',
215
- })
216
-
217
- await remote.createCommit({
218
- branchId: main.id,
219
- agentId: 'merge-agent',
220
- agentRole: 'dev',
221
- tool: 'test',
222
- workflowType: 'interactive',
223
- message: 'main init',
224
- content: 'c',
225
- summary: 's',
226
- commitType: 'branch-init',
227
- })
228
-
229
- await remote.createCommit({
230
- branchId: feat.id,
231
- agentId: 'merge-agent',
232
- agentRole: 'dev',
233
- tool: 'test',
234
- workflowType: 'interactive',
235
- message: 'feat work',
236
- content: 'c',
237
- summary: 's',
238
- commitType: 'manual',
239
- threads: { open: ['Thread from feat'] },
240
- })
241
-
242
- const mergeCommit = await remote.mergeBranch(feat.id, main.id, 'Merged feat into main')
243
- expect(mergeCommit.commitType).toBe('merge')
244
- expect(mergeCommit.createdAt).toBeInstanceOf(Date)
245
-
246
- // Source branch is merged
247
- const updatedFeat = await remote.getBranch(feat.id)
248
- expect(updatedFeat?.status).toBe('merged')
249
-
250
- // Thread carried to main branch
251
- const mainThreads = await remote.listOpenThreadsByBranch(main.id)
252
- expect(mainThreads).toHaveLength(1)
253
- expect(mainThreads[0].description).toBe('Thread from feat')
254
- })
255
-
256
- it('syncThread is idempotent and round-trips via RemoteStore', async () => {
257
- const project = await remote.createProject({ name: 'sync-thread-proj' })
258
- const branch = await remote.createBranch({ projectId: project.id, name: 'main', gitBranch: 'main' })
259
-
260
- await remote.upsertAgent({
261
- id: 'sync-agent',
262
- projectId: project.id,
263
- role: 'dev',
264
- tool: 'test',
265
- workflowType: 'interactive',
266
- })
267
-
268
- // Create a commit so we have a valid openedInCommit reference
269
- const commit = await remote.createCommit({
270
- branchId: branch.id,
271
- agentId: 'sync-agent',
272
- agentRole: 'dev',
273
- tool: 'test',
274
- workflowType: 'interactive',
275
- message: 'Seed commit',
276
- content: 'c',
277
- summary: 's',
278
- commitType: 'manual',
279
- })
280
-
281
- // Construct a Thread as if it came from another store (e.g. during push/pull)
282
- const thread = await remote.syncThread({
283
- id: 'sync-thread-id-1',
284
- projectId: project.id,
285
- branchId: branch.id,
286
- description: 'Synced open question',
287
- status: 'open',
288
- openedInCommit: commit.id,
289
- createdAt: new Date(),
290
- })
291
- expect(thread.id).toBe('sync-thread-id-1')
292
- expect(thread.description).toBe('Synced open question')
293
-
294
- // Idempotent: calling again should not throw
295
- await expect(remote.syncThread(thread)).resolves.toBeDefined()
296
-
297
- // Thread visible via listOpenThreads
298
- const open = await remote.listOpenThreads(project.id)
299
- expect(open.some(t => t.id === 'sync-thread-id-1')).toBe(true)
300
- })
301
- })
@@ -1,121 +0,0 @@
1
- // router.ts — Express router for ContextGit REST API
2
- //
3
- // Routes:
4
- // POST /commits — engine.commit() — persist a context commit
5
- // GET /snapshot — store.getFormattedSnapshot() — formatted project snapshot
6
- // GET /search — store.fullTextSearch() — full-text search over commits
7
-
8
- import { Router, Request, Response } from 'express'
9
- import type { ApiContext } from './bootstrap.js'
10
- import type { SnapshotFormat } from '@contextgit/core'
11
-
12
- export function createRouter(ctx: ApiContext): Router {
13
- const router = Router()
14
-
15
- // ── POST /commits ──────────────────────────────────────────────────────────
16
- // Body: { message, content, open_threads?, close_thread_ids? }
17
- router.post('/commits', async (req: Request, res: Response) => {
18
- try {
19
- const { message, content, open_threads, close_thread_ids } = req.body as {
20
- message?: unknown
21
- content?: unknown
22
- open_threads?: unknown
23
- close_thread_ids?: unknown
24
- }
25
-
26
- if (typeof message !== 'string' || message.trim() === '') {
27
- res.status(400).json({ error: "'message' is required and must be a non-empty string" })
28
- return
29
- }
30
- if (typeof content !== 'string' || content.trim() === '') {
31
- res.status(400).json({ error: "'content' is required and must be a non-empty string" })
32
- return
33
- }
34
-
35
- const threads: { open?: string[]; close?: Array<{ id: string; note: string }> } = {}
36
-
37
- if (Array.isArray(open_threads) && open_threads.length > 0) {
38
- threads.open = open_threads.map(String)
39
- }
40
- if (Array.isArray(close_thread_ids) && close_thread_ids.length > 0) {
41
- threads.close = close_thread_ids.map((id: unknown) => ({
42
- id: String(id),
43
- note: 'Closed via API',
44
- }))
45
- }
46
-
47
- const commit = await ctx.engine.commit({
48
- message,
49
- content,
50
- ...(Object.keys(threads).length > 0 ? { threads } : {}),
51
- })
52
-
53
- res.status(201).json({ id: commit.id, message: commit.message, createdAt: commit.createdAt })
54
- } catch (err) {
55
- const message = err instanceof Error ? err.message : String(err)
56
- res.status(500).json({ error: message })
57
- }
58
- })
59
-
60
- // ── GET /snapshot ──────────────────────────────────────────────────────────
61
- // Query: ?format=agents-md|json|text (default: agents-md)
62
- router.get('/snapshot', async (req: Request, res: Response) => {
63
- try {
64
- const format = (req.query['format'] ?? 'agents-md') as SnapshotFormat
65
- const validFormats: SnapshotFormat[] = ['agents-md', 'json', 'text']
66
- if (!validFormats.includes(format)) {
67
- res.status(400).json({ error: `'format' must be one of: ${validFormats.join(', ')}` })
68
- return
69
- }
70
-
71
- const snapshot = await ctx.store.getFormattedSnapshot(ctx.projectId, ctx.branchId, format)
72
-
73
- if (format === 'json') {
74
- res.setHeader('Content-Type', 'application/json')
75
- res.send(snapshot)
76
- } else {
77
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
78
- res.send(snapshot)
79
- }
80
- } catch (err) {
81
- const message = err instanceof Error ? err.message : String(err)
82
- res.status(500).json({ error: message })
83
- }
84
- })
85
-
86
- // ── GET /search ────────────────────────────────────────────────────────────
87
- // Query: ?q=<query>&limit=<n> (limit default: 5, max: 20)
88
- router.get('/search', async (req: Request, res: Response) => {
89
- try {
90
- const q = req.query['q']
91
- if (typeof q !== 'string' || q.trim() === '') {
92
- res.status(400).json({ error: "'q' query parameter is required" })
93
- return
94
- }
95
-
96
- const limitRaw = parseInt(String(req.query['limit'] ?? '5'), 10)
97
- const limit = Number.isNaN(limitRaw) ? 5 : Math.min(Math.max(limitRaw, 1), 20)
98
-
99
- const results = await ctx.store.fullTextSearch(q, ctx.projectId)
100
- const trimmed = results.slice(0, limit)
101
-
102
- res.json({
103
- query: q,
104
- total: trimmed.length,
105
- results: trimmed.map(r => ({
106
- id: r.commit.id,
107
- message: r.commit.message,
108
- content: r.commit.content,
109
- score: r.score,
110
- matchType: r.matchType,
111
- createdAt: r.commit.createdAt,
112
- })),
113
- })
114
- } catch (err) {
115
- const message = err instanceof Error ? err.message : String(err)
116
- res.status(500).json({ error: message })
117
- }
118
- })
119
-
120
- return router
121
- }
@@ -1,34 +0,0 @@
1
- // server-config.ts — global server configuration at ~/.contextgit/server.json
2
- //
3
- // Stores the SHA-256 hash of the API key so the plaintext key never rests on disk.
4
- // The server reads the hash on startup; keygen --save writes it.
5
-
6
- import os from 'os'
7
- import { join } from 'path'
8
- import { mkdirSync, readFileSync, writeFileSync } from 'fs'
9
- import { createHash } from 'crypto'
10
-
11
- const CONTEXTGIT_DIR = join(os.homedir(), '.contextgit')
12
- const SERVER_CONFIG_PATH = join(CONTEXTGIT_DIR, 'server.json')
13
-
14
- export interface ServerConfig {
15
- keyHash?: string // SHA-256 hex digest of the API key
16
- }
17
-
18
- export function sha256hex(input: string): string {
19
- return createHash('sha256').update(input, 'utf8').digest('hex')
20
- }
21
-
22
- export function readServerConfig(): ServerConfig {
23
- try {
24
- const raw = readFileSync(SERVER_CONFIG_PATH, 'utf-8')
25
- return JSON.parse(raw) as ServerConfig
26
- } catch {
27
- return {}
28
- }
29
- }
30
-
31
- export function writeServerConfig(config: ServerConfig): void {
32
- mkdirSync(CONTEXTGIT_DIR, { recursive: true })
33
- writeFileSync(SERVER_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
34
- }
@@ -1,38 +0,0 @@
1
- // server.ts — ContextGit REST API server
2
- //
3
- // Creates and configures the Express app. Separated from index.ts so tests
4
- // can import createApp() without binding to a port.
5
-
6
- import express from 'express'
7
- import type { Express } from 'express'
8
- import { bootstrap } from './bootstrap.js'
9
- import { createRouter } from './router.js'
10
- import { createStoreRouter } from './store-router.js'
11
- import { createAuthMiddleware } from './middleware/auth.js'
12
- import { readServerConfig, sha256hex } from './server-config.js'
13
-
14
- export async function createApp(): Promise<Express> {
15
- const ctx = await bootstrap()
16
-
17
- // Resolve key hash: prefer ~/.contextgit/server.json, fall back to env var
18
- const serverCfg = readServerConfig()
19
- let keyHashHex = serverCfg.keyHash
20
- if (!keyHashHex) {
21
- const envKey = process.env['CONTEXTGIT_API_KEY']
22
- if (envKey) keyHashHex = sha256hex(envKey)
23
- }
24
-
25
- const app = express()
26
- app.use(express.json())
27
- app.use(createAuthMiddleware(keyHashHex))
28
-
29
- app.use('/v1/store', createStoreRouter(ctx.store))
30
- app.use('/', createRouter(ctx))
31
-
32
- // 404 catch-all
33
- app.use((_req, res) => {
34
- res.status(404).json({ error: 'Not found' })
35
- })
36
-
37
- return app
38
- }