contextgit 0.0.2 → 0.0.3
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/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +43 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/commands/branch.d.ts +13 -0
- package/dist/commands/branch.d.ts.map +1 -0
- package/dist/commands/branch.js +52 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/claim.d.ts +13 -0
- package/dist/commands/claim.d.ts.map +1 -0
- package/dist/commands/claim.js +50 -0
- package/dist/commands/claim.js.map +1 -0
- package/dist/commands/commit.d.ts +14 -0
- package/dist/commands/commit.d.ts.map +1 -0
- package/dist/commands/commit.js +71 -0
- package/dist/commands/commit.js.map +1 -0
- package/dist/commands/context.d.ts +9 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +38 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +84 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +126 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/keygen.d.ts +10 -0
- package/dist/commands/keygen.d.ts.map +1 -0
- package/dist/commands/keygen.js +57 -0
- package/dist/commands/keygen.js.map +1 -0
- package/dist/commands/log.d.ts +13 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +91 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/merge.d.ts +12 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +29 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/pull.d.ts +10 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +123 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +10 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +141 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/remote-show.d.ts +6 -0
- package/dist/commands/remote-show.d.ts.map +1 -0
- package/dist/commands/remote-show.js +71 -0
- package/dist/commands/remote-show.js.map +1 -0
- package/dist/commands/search.d.ts +11 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +47 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +51 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/set-remote.d.ts +9 -0
- package/dist/commands/set-remote.d.ts.map +1 -0
- package/dist/commands/set-remote.js +26 -0
- package/dist/commands/set-remote.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +54 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unclaim.d.ts +9 -0
- package/dist/commands/unclaim.d.ts.map +1 -0
- package/dist/commands/unclaim.js +22 -0
- package/dist/commands/unclaim.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/git-hooks.d.ts +6 -0
- package/dist/git-hooks.d.ts.map +1 -0
- package/dist/git-hooks.js +58 -0
- package/dist/git-hooks.js.map +1 -0
- package/package.json +22 -19
- package/.claude/settings.local.json +0 -41
- package/.contextgit/config.json +0 -10
- package/.contextgit/system-prompt.md +0 -4
- package/.github/workflows/contextgit-ci.yml +0 -40
- package/CLAUDE.md +0 -123
- package/CLAUDE.md.next +0 -65
- package/docs/ContextGit_ARCHITECTURE_v3.md +0 -1141
- package/docs/ContextGit_DELTA.md +0 -84
- package/docs/ContextGit_PHASE1_PLAN.md +0 -177
- package/docs/ContextGit_PHASE2_PLAN.md +0 -535
- package/docs/ContextGit_PRD_v4.md +0 -488
- package/docs/decisions.md +0 -370
- package/packages/api/package.json +0 -25
- package/packages/api/src/bootstrap.ts +0 -64
- package/packages/api/src/config.ts +0 -45
- package/packages/api/src/index.ts +0 -17
- package/packages/api/src/middleware/auth.test.ts +0 -83
- package/packages/api/src/middleware/auth.ts +0 -41
- package/packages/api/src/remote-store.test.ts +0 -301
- package/packages/api/src/router.ts +0 -121
- package/packages/api/src/server-config.ts +0 -34
- package/packages/api/src/server.ts +0 -38
- package/packages/api/src/store-router.ts +0 -241
- package/packages/api/tsconfig.json +0 -8
- package/packages/cli/package.json +0 -29
- package/packages/cli/src/bootstrap.ts +0 -68
- package/packages/cli/src/commands/branch.ts +0 -58
- package/packages/cli/src/commands/claim.ts +0 -58
- package/packages/cli/src/commands/commit.ts +0 -79
- package/packages/cli/src/commands/context.ts +0 -46
- package/packages/cli/src/commands/doctor.ts +0 -99
- package/packages/cli/src/commands/init.ts +0 -141
- package/packages/cli/src/commands/keygen.ts +0 -65
- package/packages/cli/src/commands/log.ts +0 -103
- package/packages/cli/src/commands/merge.ts +0 -36
- package/packages/cli/src/commands/pull.ts +0 -145
- package/packages/cli/src/commands/push.ts +0 -158
- package/packages/cli/src/commands/remote-show.ts +0 -87
- package/packages/cli/src/commands/search.ts +0 -54
- package/packages/cli/src/commands/serve.ts +0 -61
- package/packages/cli/src/commands/set-remote.ts +0 -30
- package/packages/cli/src/commands/status.ts +0 -62
- package/packages/cli/src/commands/unclaim.ts +0 -28
- package/packages/cli/src/config.ts +0 -64
- package/packages/cli/src/git-hooks.ts +0 -61
- package/packages/cli/tsconfig.json +0 -9
- package/packages/core/package.json +0 -28
- package/packages/core/src/embeddings.test.ts +0 -58
- package/packages/core/src/embeddings.ts +0 -75
- package/packages/core/src/engine.ts +0 -274
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/snapshot.ts +0 -82
- package/packages/core/src/summarizer.test.ts +0 -120
- package/packages/core/src/summarizer.ts +0 -113
- package/packages/core/src/threads.ts +0 -29
- package/packages/core/src/types.ts +0 -240
- package/packages/core/tsconfig.json +0 -9
- package/packages/mcp/package.json +0 -31
- package/packages/mcp/src/auto-snapshot.ts +0 -83
- package/packages/mcp/src/config.ts +0 -53
- package/packages/mcp/src/git-sync.ts +0 -94
- package/packages/mcp/src/index.ts +0 -19
- package/packages/mcp/src/server.ts +0 -377
- package/packages/mcp/tsconfig.json +0 -9
- package/packages/store/package.json +0 -30
- package/packages/store/src/branch-merge.test.ts +0 -127
- package/packages/store/src/engine-integration.test.ts +0 -93
- package/packages/store/src/index.ts +0 -3
- package/packages/store/src/interface.ts +0 -62
- package/packages/store/src/local/claims.test.ts +0 -190
- package/packages/store/src/local/index.ts +0 -380
- package/packages/store/src/local/local-store.test.ts +0 -164
- package/packages/store/src/local/migrations.ts +0 -99
- package/packages/store/src/local/queries.ts +0 -760
- package/packages/store/src/local/schema.ts +0 -157
- package/packages/store/src/remote/index.ts +0 -300
- package/packages/store/tsconfig.json +0 -9
- package/pnpm-workspace.yaml +0 -2
- package/scripts/build.sh +0 -28
- package/tsconfig.base.json +0 -14
- package/vitest.config.ts +0 -15
- /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
|
-
}
|