@retrace-dev/cli 0.1.0
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 +202 -0
- package/README.md +13 -0
- package/dist/doctor.d.ts +16 -0
- package/dist/doctor.js +161 -0
- package/dist/export-cli.d.ts +2 -0
- package/dist/export-cli.js +107 -0
- package/dist/gdrive-cli.d.ts +2 -0
- package/dist/gdrive-cli.js +97 -0
- package/dist/git-hook.d.ts +61 -0
- package/dist/git-hook.js +365 -0
- package/dist/github-cli.d.ts +2 -0
- package/dist/github-cli.js +111 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +384 -0
- package/dist/is-main.d.ts +2 -0
- package/dist/is-main.js +13 -0
- package/dist/keys.d.ts +9 -0
- package/dist/keys.js +28 -0
- package/dist/remote-store.d.ts +211 -0
- package/dist/remote-store.js +75 -0
- package/dist/serve.d.ts +11 -0
- package/dist/serve.js +53 -0
- package/dist/sqlite-store.d.ts +246 -0
- package/dist/sqlite-store.js +121 -0
- package/package.json +36 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Consistent headers for CLI-originated requests, including runtimes that require an explicit user agent. */
|
|
2
|
+
export function retraceHeaders(token) {
|
|
3
|
+
return {
|
|
4
|
+
accept: "application/json",
|
|
5
|
+
"content-type": "application/json",
|
|
6
|
+
"user-agent": "@retrace-dev/cli/0.1.0",
|
|
7
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export class RemoteStore {
|
|
11
|
+
baseUrl;
|
|
12
|
+
token;
|
|
13
|
+
constructor(baseUrl, token) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
this.token = token;
|
|
16
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
17
|
+
}
|
|
18
|
+
async req(method, path, body) {
|
|
19
|
+
const res = await fetch(this.baseUrl + path, {
|
|
20
|
+
method,
|
|
21
|
+
headers: retraceHeaders(this.token),
|
|
22
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`Retrace API ${method} ${path} → ${res.status}: ${await res.text()}`);
|
|
26
|
+
return (await res.json());
|
|
27
|
+
}
|
|
28
|
+
/** Remote appends server-side (chain sealing must happen where the head lives). */
|
|
29
|
+
async append(input) {
|
|
30
|
+
return this.req("POST", "/events", input);
|
|
31
|
+
}
|
|
32
|
+
async verify(project) {
|
|
33
|
+
return this.req("GET", `/projects/${encodeURIComponent(project)}/verify`);
|
|
34
|
+
}
|
|
35
|
+
async status(project) {
|
|
36
|
+
return this.req("GET", `/projects/${encodeURIComponent(project)}/status`);
|
|
37
|
+
}
|
|
38
|
+
async createShare() { throw new Error("use share()"); }
|
|
39
|
+
async getShare(id) { return this.req("GET", `/s/${encodeURIComponent(id)}/meta`); }
|
|
40
|
+
/** Server-side share creation; returns share + url. */
|
|
41
|
+
async share(body) {
|
|
42
|
+
return this.req("POST", `/projects/${encodeURIComponent(body.project)}/share`, body);
|
|
43
|
+
}
|
|
44
|
+
async export(scope) {
|
|
45
|
+
const p = new URLSearchParams();
|
|
46
|
+
if (scope.artifact_id)
|
|
47
|
+
p.set("artifact_id", scope.artifact_id);
|
|
48
|
+
return this.req("GET", `/projects/${encodeURIComponent(scope.project)}/export?${p}`);
|
|
49
|
+
}
|
|
50
|
+
async head(project) {
|
|
51
|
+
return this.req("GET", `/projects/${encodeURIComponent(project)}/head`);
|
|
52
|
+
}
|
|
53
|
+
async insert() {
|
|
54
|
+
throw new Error("RemoteStore.insert is not supported; use append()");
|
|
55
|
+
}
|
|
56
|
+
async byIdempotencyKey() {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async get(id) {
|
|
60
|
+
return this.req("GET", `/events/${encodeURIComponent(id)}`);
|
|
61
|
+
}
|
|
62
|
+
async all(project) {
|
|
63
|
+
return this.req("GET", `/projects/${encodeURIComponent(project)}/events?limit=100000`);
|
|
64
|
+
}
|
|
65
|
+
async projects() {
|
|
66
|
+
return this.req("GET", `/projects`);
|
|
67
|
+
}
|
|
68
|
+
async history(q) {
|
|
69
|
+
const p = new URLSearchParams();
|
|
70
|
+
for (const [k, v] of Object.entries(q))
|
|
71
|
+
if (v !== undefined && k !== "project")
|
|
72
|
+
p.set(k, String(v));
|
|
73
|
+
return this.req("GET", `/projects/${encodeURIComponent(q.project)}/events?${p}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local Retrace server: serves the timeline UI + REST API from the same SQLite file the MCP server writes to.
|
|
4
|
+
* retrace-serve → http://localhost:7777
|
|
5
|
+
* Env: RETRACE_DB, RETRACE_PORT (7777), RETRACE_TOKEN (optional bearer/query token), RETRACE_CREDENTIALS (per-actor tokens, JSON),
|
|
6
|
+
* RETRACE_SIGNING_KEY (JWK; else ~/.retrace/signing-key.json, auto-created), RETRACE_ISSUER, RETRACE_PUBLIC_URL,
|
|
7
|
+
* RETRACE_GITHUB_SECRET (enables POST /hooks/github), RETRACE_GITHUB_PUSH=1 (also log push commits),
|
|
8
|
+
* RETRACE_OWNER (who holds RETRACE_TOKEN; DELETE /projects/:p audit events are attributed to this human)
|
|
9
|
+
*/
|
|
10
|
+
import { IncomingMessage } from "node:http";
|
|
11
|
+
export declare function startServer(port?: number): import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local Retrace server: serves the timeline UI + REST API from the same SQLite file the MCP server writes to.
|
|
4
|
+
* retrace-serve → http://localhost:7777
|
|
5
|
+
* Env: RETRACE_DB, RETRACE_PORT (7777), RETRACE_TOKEN (optional bearer/query token), RETRACE_CREDENTIALS (per-actor tokens, JSON),
|
|
6
|
+
* RETRACE_SIGNING_KEY (JWK; else ~/.retrace/signing-key.json, auto-created), RETRACE_ISSUER, RETRACE_PUBLIC_URL,
|
|
7
|
+
* RETRACE_GITHUB_SECRET (enables POST /hooks/github), RETRACE_GITHUB_PUSH=1 (also log push commits),
|
|
8
|
+
* RETRACE_OWNER (who holds RETRACE_TOKEN; DELETE /projects/:p audit events are attributed to this human)
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { Readable } from "node:stream";
|
|
12
|
+
import { createHandler, parseCredentials, parseSigningKey } from "@retrace-dev/core";
|
|
13
|
+
import { loadSigningKey } from "./keys.js";
|
|
14
|
+
import { makeStore } from "./index.js";
|
|
15
|
+
import { isMainModule } from "./is-main.js";
|
|
16
|
+
function toRequest(req) {
|
|
17
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
for (const [k, v] of Object.entries(req.headers))
|
|
20
|
+
if (typeof v === "string")
|
|
21
|
+
headers.set(k, v);
|
|
22
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
23
|
+
return new Request(url, { method: req.method, headers, body: hasBody ? Readable.toWeb(req) : undefined, ...(hasBody ? { duplex: "half" } : {}) });
|
|
24
|
+
}
|
|
25
|
+
export function startServer(port = Number(process.env.RETRACE_PORT ?? 7777)) {
|
|
26
|
+
const handle = createHandler(makeStore(), {
|
|
27
|
+
token: process.env.RETRACE_TOKEN,
|
|
28
|
+
credentials: parseCredentials(process.env.RETRACE_CREDENTIALS),
|
|
29
|
+
signingKey: parseSigningKey(process.env.RETRACE_SIGNING_KEY) ?? loadSigningKey(),
|
|
30
|
+
issuerName: process.env.RETRACE_ISSUER,
|
|
31
|
+
publicUrl: process.env.RETRACE_PUBLIC_URL,
|
|
32
|
+
githubSecret: process.env.RETRACE_GITHUB_SECRET,
|
|
33
|
+
githubIncludePush: process.env.RETRACE_GITHUB_PUSH === "1",
|
|
34
|
+
ownerActor: process.env.RETRACE_OWNER ? { type: "human", id: process.env.RETRACE_OWNER } : undefined,
|
|
35
|
+
});
|
|
36
|
+
const server = createServer(async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const out = await handle(toRequest(req));
|
|
39
|
+
const hdrs = {};
|
|
40
|
+
out.headers.forEach((v, k) => (hdrs[k] = v));
|
|
41
|
+
res.writeHead(out.status, hdrs);
|
|
42
|
+
res.end(Buffer.from(await out.arrayBuffer()));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
46
|
+
res.end(JSON.stringify({ error: String(e?.message ?? e) }));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
server.listen(port, () => console.error(`Retrace UI + API → http://localhost:${port}`));
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
if (isMainModule(import.meta.url))
|
|
53
|
+
startServer();
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { ChainHead, Event, EventStore, HistoryQuery, Share } from "@retrace-dev/core";
|
|
2
|
+
export declare class SqliteStore implements EventStore {
|
|
3
|
+
private db;
|
|
4
|
+
constructor(path: string);
|
|
5
|
+
private headSync;
|
|
6
|
+
head(project: string): Promise<ChainHead | null>;
|
|
7
|
+
/** Raw row writes for one event — caller owns the transaction. */
|
|
8
|
+
private insertRows;
|
|
9
|
+
insert(e: Event): Promise<void>;
|
|
10
|
+
/** Deletes + audit insert in one transaction (B3); the local server's DELETE /projects/:p needs this. The head
|
|
11
|
+
* check runs inside the same transaction, so the audit can only ever commit against the head it describes. */
|
|
12
|
+
deleteProject(project: string, audit: Event, expectedHead: ChainHead): Promise<{
|
|
13
|
+
[k: string]: number;
|
|
14
|
+
}>;
|
|
15
|
+
createShare(s: Share): Promise<void>;
|
|
16
|
+
getShare(id: string): Promise<Share | null>;
|
|
17
|
+
byIdempotencyKey(project: string, key: string): Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
project: string;
|
|
20
|
+
actor: {
|
|
21
|
+
type: "human" | "agent" | "system";
|
|
22
|
+
id: string;
|
|
23
|
+
display_name?: string | undefined;
|
|
24
|
+
model?: string | undefined;
|
|
25
|
+
version?: string | undefined;
|
|
26
|
+
on_behalf_of?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
action: "received" | "created" | "edited" | "deleted" | "read" | "executed" | "approved" | "rejected" | "sent" | "moved" | "renamed" | "instructed" | "committed" | "merged" | "other";
|
|
29
|
+
artifacts: {
|
|
30
|
+
id: string;
|
|
31
|
+
kind?: string | undefined;
|
|
32
|
+
label?: string | undefined;
|
|
33
|
+
derived_from?: string[] | undefined;
|
|
34
|
+
role?: "used" | "generated" | "both" | undefined;
|
|
35
|
+
}[];
|
|
36
|
+
timestamp: string;
|
|
37
|
+
seq: number;
|
|
38
|
+
prev_hash: string;
|
|
39
|
+
hash: string;
|
|
40
|
+
received_at: string;
|
|
41
|
+
action_detail?: string | undefined;
|
|
42
|
+
change?: {
|
|
43
|
+
before_hash?: string | undefined;
|
|
44
|
+
after_hash?: string | undefined;
|
|
45
|
+
diff?: string | undefined;
|
|
46
|
+
summary?: string | undefined;
|
|
47
|
+
} | undefined;
|
|
48
|
+
duration_ms?: number | undefined;
|
|
49
|
+
location?: {
|
|
50
|
+
system?: string | undefined;
|
|
51
|
+
path?: string | undefined;
|
|
52
|
+
url?: string | undefined;
|
|
53
|
+
environment?: string | undefined;
|
|
54
|
+
device?: string | undefined;
|
|
55
|
+
session?: string | undefined;
|
|
56
|
+
client?: string | undefined;
|
|
57
|
+
ide?: string | undefined;
|
|
58
|
+
workspace?: string | undefined;
|
|
59
|
+
surface?: "agent" | "tty" | undefined;
|
|
60
|
+
} | undefined;
|
|
61
|
+
intent?: string | undefined;
|
|
62
|
+
caused_by?: string | undefined;
|
|
63
|
+
method?: {
|
|
64
|
+
params?: Record<string, unknown> | undefined;
|
|
65
|
+
tool?: string | undefined;
|
|
66
|
+
instruction?: string | undefined;
|
|
67
|
+
automated?: boolean | undefined;
|
|
68
|
+
tokens?: number | undefined;
|
|
69
|
+
cost_usd?: number | undefined;
|
|
70
|
+
} | undefined;
|
|
71
|
+
idempotency_key?: string | undefined;
|
|
72
|
+
tags?: string[] | undefined;
|
|
73
|
+
} | null>;
|
|
74
|
+
get(id: string): Promise<{
|
|
75
|
+
id: string;
|
|
76
|
+
project: string;
|
|
77
|
+
actor: {
|
|
78
|
+
type: "human" | "agent" | "system";
|
|
79
|
+
id: string;
|
|
80
|
+
display_name?: string | undefined;
|
|
81
|
+
model?: string | undefined;
|
|
82
|
+
version?: string | undefined;
|
|
83
|
+
on_behalf_of?: string | undefined;
|
|
84
|
+
};
|
|
85
|
+
action: "received" | "created" | "edited" | "deleted" | "read" | "executed" | "approved" | "rejected" | "sent" | "moved" | "renamed" | "instructed" | "committed" | "merged" | "other";
|
|
86
|
+
artifacts: {
|
|
87
|
+
id: string;
|
|
88
|
+
kind?: string | undefined;
|
|
89
|
+
label?: string | undefined;
|
|
90
|
+
derived_from?: string[] | undefined;
|
|
91
|
+
role?: "used" | "generated" | "both" | undefined;
|
|
92
|
+
}[];
|
|
93
|
+
timestamp: string;
|
|
94
|
+
seq: number;
|
|
95
|
+
prev_hash: string;
|
|
96
|
+
hash: string;
|
|
97
|
+
received_at: string;
|
|
98
|
+
action_detail?: string | undefined;
|
|
99
|
+
change?: {
|
|
100
|
+
before_hash?: string | undefined;
|
|
101
|
+
after_hash?: string | undefined;
|
|
102
|
+
diff?: string | undefined;
|
|
103
|
+
summary?: string | undefined;
|
|
104
|
+
} | undefined;
|
|
105
|
+
duration_ms?: number | undefined;
|
|
106
|
+
location?: {
|
|
107
|
+
system?: string | undefined;
|
|
108
|
+
path?: string | undefined;
|
|
109
|
+
url?: string | undefined;
|
|
110
|
+
environment?: string | undefined;
|
|
111
|
+
device?: string | undefined;
|
|
112
|
+
session?: string | undefined;
|
|
113
|
+
client?: string | undefined;
|
|
114
|
+
ide?: string | undefined;
|
|
115
|
+
workspace?: string | undefined;
|
|
116
|
+
surface?: "agent" | "tty" | undefined;
|
|
117
|
+
} | undefined;
|
|
118
|
+
intent?: string | undefined;
|
|
119
|
+
caused_by?: string | undefined;
|
|
120
|
+
method?: {
|
|
121
|
+
params?: Record<string, unknown> | undefined;
|
|
122
|
+
tool?: string | undefined;
|
|
123
|
+
instruction?: string | undefined;
|
|
124
|
+
automated?: boolean | undefined;
|
|
125
|
+
tokens?: number | undefined;
|
|
126
|
+
cost_usd?: number | undefined;
|
|
127
|
+
} | undefined;
|
|
128
|
+
idempotency_key?: string | undefined;
|
|
129
|
+
tags?: string[] | undefined;
|
|
130
|
+
} | null>;
|
|
131
|
+
all(project: string): Promise<{
|
|
132
|
+
id: string;
|
|
133
|
+
project: string;
|
|
134
|
+
actor: {
|
|
135
|
+
type: "human" | "agent" | "system";
|
|
136
|
+
id: string;
|
|
137
|
+
display_name?: string | undefined;
|
|
138
|
+
model?: string | undefined;
|
|
139
|
+
version?: string | undefined;
|
|
140
|
+
on_behalf_of?: string | undefined;
|
|
141
|
+
};
|
|
142
|
+
action: "received" | "created" | "edited" | "deleted" | "read" | "executed" | "approved" | "rejected" | "sent" | "moved" | "renamed" | "instructed" | "committed" | "merged" | "other";
|
|
143
|
+
artifacts: {
|
|
144
|
+
id: string;
|
|
145
|
+
kind?: string | undefined;
|
|
146
|
+
label?: string | undefined;
|
|
147
|
+
derived_from?: string[] | undefined;
|
|
148
|
+
role?: "used" | "generated" | "both" | undefined;
|
|
149
|
+
}[];
|
|
150
|
+
timestamp: string;
|
|
151
|
+
seq: number;
|
|
152
|
+
prev_hash: string;
|
|
153
|
+
hash: string;
|
|
154
|
+
received_at: string;
|
|
155
|
+
action_detail?: string | undefined;
|
|
156
|
+
change?: {
|
|
157
|
+
before_hash?: string | undefined;
|
|
158
|
+
after_hash?: string | undefined;
|
|
159
|
+
diff?: string | undefined;
|
|
160
|
+
summary?: string | undefined;
|
|
161
|
+
} | undefined;
|
|
162
|
+
duration_ms?: number | undefined;
|
|
163
|
+
location?: {
|
|
164
|
+
system?: string | undefined;
|
|
165
|
+
path?: string | undefined;
|
|
166
|
+
url?: string | undefined;
|
|
167
|
+
environment?: string | undefined;
|
|
168
|
+
device?: string | undefined;
|
|
169
|
+
session?: string | undefined;
|
|
170
|
+
client?: string | undefined;
|
|
171
|
+
ide?: string | undefined;
|
|
172
|
+
workspace?: string | undefined;
|
|
173
|
+
surface?: "agent" | "tty" | undefined;
|
|
174
|
+
} | undefined;
|
|
175
|
+
intent?: string | undefined;
|
|
176
|
+
caused_by?: string | undefined;
|
|
177
|
+
method?: {
|
|
178
|
+
params?: Record<string, unknown> | undefined;
|
|
179
|
+
tool?: string | undefined;
|
|
180
|
+
instruction?: string | undefined;
|
|
181
|
+
automated?: boolean | undefined;
|
|
182
|
+
tokens?: number | undefined;
|
|
183
|
+
cost_usd?: number | undefined;
|
|
184
|
+
} | undefined;
|
|
185
|
+
idempotency_key?: string | undefined;
|
|
186
|
+
tags?: string[] | undefined;
|
|
187
|
+
}[]>;
|
|
188
|
+
projects(): Promise<string[]>;
|
|
189
|
+
history(q: HistoryQuery): Promise<{
|
|
190
|
+
id: string;
|
|
191
|
+
project: string;
|
|
192
|
+
actor: {
|
|
193
|
+
type: "human" | "agent" | "system";
|
|
194
|
+
id: string;
|
|
195
|
+
display_name?: string | undefined;
|
|
196
|
+
model?: string | undefined;
|
|
197
|
+
version?: string | undefined;
|
|
198
|
+
on_behalf_of?: string | undefined;
|
|
199
|
+
};
|
|
200
|
+
action: "received" | "created" | "edited" | "deleted" | "read" | "executed" | "approved" | "rejected" | "sent" | "moved" | "renamed" | "instructed" | "committed" | "merged" | "other";
|
|
201
|
+
artifacts: {
|
|
202
|
+
id: string;
|
|
203
|
+
kind?: string | undefined;
|
|
204
|
+
label?: string | undefined;
|
|
205
|
+
derived_from?: string[] | undefined;
|
|
206
|
+
role?: "used" | "generated" | "both" | undefined;
|
|
207
|
+
}[];
|
|
208
|
+
timestamp: string;
|
|
209
|
+
seq: number;
|
|
210
|
+
prev_hash: string;
|
|
211
|
+
hash: string;
|
|
212
|
+
received_at: string;
|
|
213
|
+
action_detail?: string | undefined;
|
|
214
|
+
change?: {
|
|
215
|
+
before_hash?: string | undefined;
|
|
216
|
+
after_hash?: string | undefined;
|
|
217
|
+
diff?: string | undefined;
|
|
218
|
+
summary?: string | undefined;
|
|
219
|
+
} | undefined;
|
|
220
|
+
duration_ms?: number | undefined;
|
|
221
|
+
location?: {
|
|
222
|
+
system?: string | undefined;
|
|
223
|
+
path?: string | undefined;
|
|
224
|
+
url?: string | undefined;
|
|
225
|
+
environment?: string | undefined;
|
|
226
|
+
device?: string | undefined;
|
|
227
|
+
session?: string | undefined;
|
|
228
|
+
client?: string | undefined;
|
|
229
|
+
ide?: string | undefined;
|
|
230
|
+
workspace?: string | undefined;
|
|
231
|
+
surface?: "agent" | "tty" | undefined;
|
|
232
|
+
} | undefined;
|
|
233
|
+
intent?: string | undefined;
|
|
234
|
+
caused_by?: string | undefined;
|
|
235
|
+
method?: {
|
|
236
|
+
params?: Record<string, unknown> | undefined;
|
|
237
|
+
tool?: string | undefined;
|
|
238
|
+
instruction?: string | undefined;
|
|
239
|
+
automated?: boolean | undefined;
|
|
240
|
+
tokens?: number | undefined;
|
|
241
|
+
cost_usd?: number | undefined;
|
|
242
|
+
} | undefined;
|
|
243
|
+
idempotency_key?: string | undefined;
|
|
244
|
+
tags?: string[] | undefined;
|
|
245
|
+
}[]>;
|
|
246
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Local SQLite store using Node's built-in node:sqlite (Node >= 22.13). No native deps. */
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { HeadMovedError, SCHEMA_SQL } from "@retrace-dev/core";
|
|
4
|
+
export class SqliteStore {
|
|
5
|
+
db;
|
|
6
|
+
constructor(path) {
|
|
7
|
+
this.db = new DatabaseSync(path);
|
|
8
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
9
|
+
this.db.exec(SCHEMA_SQL);
|
|
10
|
+
}
|
|
11
|
+
headSync(project) {
|
|
12
|
+
const row = this.db.prepare("SELECT seq, hash FROM events WHERE project = ? ORDER BY seq DESC LIMIT 1").get(project);
|
|
13
|
+
return row ?? null;
|
|
14
|
+
}
|
|
15
|
+
async head(project) {
|
|
16
|
+
return this.headSync(project);
|
|
17
|
+
}
|
|
18
|
+
/** Raw row writes for one event — caller owns the transaction. */
|
|
19
|
+
insertRows(e) {
|
|
20
|
+
const ins = this.db.prepare(`INSERT INTO events (id, project, seq, timestamp, received_at, actor_type, actor_id, action, caused_by, idempotency_key, prev_hash, hash, body)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
22
|
+
const insArt = this.db.prepare("INSERT OR IGNORE INTO event_artifacts (event_id, project, artifact_id) VALUES (?, ?, ?)");
|
|
23
|
+
ins.run(e.id, e.project, e.seq, e.timestamp, e.received_at, e.actor.type, e.actor.id, e.action, e.caused_by ?? null, e.idempotency_key ?? null, e.prev_hash, e.hash, JSON.stringify(e));
|
|
24
|
+
for (const a of e.artifacts)
|
|
25
|
+
insArt.run(e.id, e.project, a.id);
|
|
26
|
+
}
|
|
27
|
+
async insert(e) {
|
|
28
|
+
this.db.exec("BEGIN");
|
|
29
|
+
try {
|
|
30
|
+
this.insertRows(e);
|
|
31
|
+
this.db.exec("COMMIT");
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
this.db.exec("ROLLBACK");
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Deletes + audit insert in one transaction (B3); the local server's DELETE /projects/:p needs this. The head
|
|
39
|
+
* check runs inside the same transaction, so the audit can only ever commit against the head it describes. */
|
|
40
|
+
async deleteProject(project, audit, expectedHead) {
|
|
41
|
+
const tables = ["events", "event_artifacts", "shares"];
|
|
42
|
+
this.db.exec("BEGIN");
|
|
43
|
+
try {
|
|
44
|
+
const head = this.headSync(project); // synchronous: the transaction never yields between check and deletes
|
|
45
|
+
if (!head || head.seq !== expectedHead.seq || head.hash !== expectedHead.hash)
|
|
46
|
+
throw new HeadMovedError(project, expectedHead);
|
|
47
|
+
const counts = Object.fromEntries(tables.map((t) => [t, Number(this.db.prepare(`DELETE FROM ${t} WHERE project = ?`).run(project).changes)]));
|
|
48
|
+
this.insertRows(audit);
|
|
49
|
+
this.db.exec("COMMIT");
|
|
50
|
+
return counts;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
this.db.exec("ROLLBACK");
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async createShare(s) {
|
|
58
|
+
this.db.prepare("INSERT INTO shares (id, project, artifact_id, label, created_at, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
59
|
+
.run(s.id, s.project, s.artifact_id ?? null, s.label ?? null, s.created_at, s.expires_at ?? null, s.created_by ?? null);
|
|
60
|
+
}
|
|
61
|
+
async getShare(id) {
|
|
62
|
+
const r = this.db.prepare("SELECT * FROM shares WHERE id = ?").get(id);
|
|
63
|
+
if (!r)
|
|
64
|
+
return null;
|
|
65
|
+
return { id: r.id, project: r.project, artifact_id: r.artifact_id ?? undefined, label: r.label ?? undefined, created_at: r.created_at, expires_at: r.expires_at ?? undefined, created_by: r.created_by ?? undefined };
|
|
66
|
+
}
|
|
67
|
+
async byIdempotencyKey(project, key) {
|
|
68
|
+
const row = this.db.prepare("SELECT body FROM events WHERE project = ? AND idempotency_key = ? LIMIT 1").get(project, key);
|
|
69
|
+
return row ? JSON.parse(row.body) : null;
|
|
70
|
+
}
|
|
71
|
+
async get(id) {
|
|
72
|
+
const row = this.db.prepare("SELECT body FROM events WHERE id = ?").get(id);
|
|
73
|
+
return row ? JSON.parse(row.body) : null;
|
|
74
|
+
}
|
|
75
|
+
async all(project) {
|
|
76
|
+
const rows = this.db.prepare("SELECT body FROM events WHERE project = ? ORDER BY seq ASC").all(project);
|
|
77
|
+
return rows.map((r) => JSON.parse(r.body));
|
|
78
|
+
}
|
|
79
|
+
async projects() {
|
|
80
|
+
const rows = this.db.prepare("SELECT DISTINCT project FROM events ORDER BY project").all();
|
|
81
|
+
return rows.map((r) => r.project);
|
|
82
|
+
}
|
|
83
|
+
async history(q) {
|
|
84
|
+
const where = ["e.project = ?"];
|
|
85
|
+
const params = [q.project];
|
|
86
|
+
let join = "";
|
|
87
|
+
if (q.artifact_id) {
|
|
88
|
+
join = "JOIN event_artifacts ea ON ea.event_id = e.id";
|
|
89
|
+
where.push("ea.artifact_id = ?");
|
|
90
|
+
params.push(q.artifact_id);
|
|
91
|
+
}
|
|
92
|
+
if (q.actor_id) {
|
|
93
|
+
where.push("e.actor_id = ?");
|
|
94
|
+
params.push(q.actor_id);
|
|
95
|
+
}
|
|
96
|
+
if (q.actor_type) {
|
|
97
|
+
where.push("e.actor_type = ?");
|
|
98
|
+
params.push(q.actor_type);
|
|
99
|
+
}
|
|
100
|
+
if (q.action) {
|
|
101
|
+
where.push("e.action = ?");
|
|
102
|
+
params.push(q.action);
|
|
103
|
+
}
|
|
104
|
+
if (q.since) {
|
|
105
|
+
where.push("e.timestamp >= ?");
|
|
106
|
+
params.push(q.since);
|
|
107
|
+
}
|
|
108
|
+
if (q.until) {
|
|
109
|
+
where.push("e.timestamp <= ?");
|
|
110
|
+
params.push(q.until);
|
|
111
|
+
}
|
|
112
|
+
if (q.text) {
|
|
113
|
+
where.push("e.body LIKE ?");
|
|
114
|
+
params.push(`%${q.text}%`);
|
|
115
|
+
}
|
|
116
|
+
const limit = Math.min(q.limit ?? 100, 1000);
|
|
117
|
+
const sql = `SELECT DISTINCT e.body, e.seq FROM events e ${join} WHERE ${where.join(" AND ")} ORDER BY e.seq ASC LIMIT ${limit}`;
|
|
118
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
119
|
+
return rows.map((r) => JSON.parse(r.body));
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retrace-dev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI, MCP server, Git adapter, and local UI for verifiable AI-assisted development provenance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"retrace-mcp": "dist/index.js",
|
|
8
|
+
"retrace-serve": "dist/serve.js",
|
|
9
|
+
"retrace-git": "dist/git-hook.js",
|
|
10
|
+
"retrace": "dist/doctor.js",
|
|
11
|
+
"retrace-export": "dist/export-cli.js",
|
|
12
|
+
"retrace-github": "dist/github-cli.js",
|
|
13
|
+
"retrace-gdrive": "dist/gdrive-cli.js"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"files": ["dist", "!dist/*.test.js", "!dist/*.test.d.ts", "README.md"],
|
|
18
|
+
"engines": { "node": ">=22" },
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"repository": { "type": "git", "url": "git+https://github.com/jordandru/retrace.git", "directory": "packages/mcp-server" },
|
|
21
|
+
"homepage": "https://github.com/jordandru/retrace#readme",
|
|
22
|
+
"bugs": { "url": "https://github.com/jordandru/retrace/issues" },
|
|
23
|
+
"publishConfig": { "access": "public" },
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"prepack": "npm run build",
|
|
27
|
+
"test": "node --test dist/*.test.js",
|
|
28
|
+
"start": "node dist/index.js",
|
|
29
|
+
"serve": "node dist/serve.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
33
|
+
"@retrace-dev/core": "0.1.0",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
}
|
|
36
|
+
}
|