sparkecoder 0.1.4 → 0.1.6
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/agent/index.d.ts +2 -2
- package/dist/agent/index.js +193 -25
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +937 -495
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +53 -2
- package/dist/db/index.js +184 -0
- package/dist/db/index.js.map +1 -1
- package/dist/{index-DkR9Ln_7.d.ts → index-BeKylVnB.d.ts} +5 -5
- package/dist/index.d.ts +68 -5
- package/dist/index.js +864 -463
- package/dist/index.js.map +1 -1
- package/dist/{schema-cUDLVN-b.d.ts → schema-CkrIadxa.d.ts} +246 -2
- package/dist/server/index.js +858 -465
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +272 -133
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
-
};
|
|
6
2
|
var __export = (target, all) => {
|
|
7
3
|
for (var name in all)
|
|
8
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
5
|
};
|
|
10
6
|
|
|
7
|
+
// src/server/index.ts
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
import { Hono as Hono5 } from "hono";
|
|
10
|
+
import { serve } from "@hono/node-server";
|
|
11
|
+
import { cors } from "hono/cors";
|
|
12
|
+
import { logger } from "hono/logger";
|
|
13
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
14
|
+
import { resolve as resolve6, dirname as dirname4, join as join3 } from "path";
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
import { createServer as createNetServer } from "net";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
// src/server/routes/sessions.ts
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { zValidator } from "@hono/zod-validator";
|
|
22
|
+
import { z as z8 } from "zod";
|
|
23
|
+
|
|
24
|
+
// src/db/index.ts
|
|
25
|
+
import Database from "better-sqlite3";
|
|
26
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
27
|
+
import { eq, desc, and, sql } from "drizzle-orm";
|
|
28
|
+
import { nanoid } from "nanoid";
|
|
29
|
+
|
|
11
30
|
// src/db/schema.ts
|
|
12
31
|
var schema_exports = {};
|
|
13
32
|
__export(schema_exports, {
|
|
14
33
|
activeStreams: () => activeStreams,
|
|
34
|
+
checkpoints: () => checkpoints,
|
|
35
|
+
fileBackups: () => fileBackups,
|
|
15
36
|
loadedSkills: () => loadedSkills,
|
|
16
37
|
messages: () => messages,
|
|
17
38
|
sessions: () => sessions,
|
|
@@ -20,107 +41,108 @@ __export(schema_exports, {
|
|
|
20
41
|
toolExecutions: () => toolExecutions
|
|
21
42
|
});
|
|
22
43
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
23
|
-
var sessions
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
44
|
+
var sessions = sqliteTable("sessions", {
|
|
45
|
+
id: text("id").primaryKey(),
|
|
46
|
+
name: text("name"),
|
|
47
|
+
workingDirectory: text("working_directory").notNull(),
|
|
48
|
+
model: text("model").notNull(),
|
|
49
|
+
status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
|
|
50
|
+
config: text("config", { mode: "json" }).$type(),
|
|
51
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
52
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
53
|
+
});
|
|
54
|
+
var messages = sqliteTable("messages", {
|
|
55
|
+
id: text("id").primaryKey(),
|
|
56
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
57
|
+
// Store the entire ModelMessage as JSON (role + content)
|
|
58
|
+
modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
|
|
59
|
+
// Sequence number within session to maintain exact ordering
|
|
60
|
+
sequence: integer("sequence").notNull().default(0),
|
|
61
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
62
|
+
});
|
|
63
|
+
var toolExecutions = sqliteTable("tool_executions", {
|
|
64
|
+
id: text("id").primaryKey(),
|
|
65
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
66
|
+
messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
|
|
67
|
+
toolName: text("tool_name").notNull(),
|
|
68
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
69
|
+
input: text("input", { mode: "json" }),
|
|
70
|
+
output: text("output", { mode: "json" }),
|
|
71
|
+
status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
|
|
72
|
+
requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
|
|
73
|
+
error: text("error"),
|
|
74
|
+
startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
75
|
+
completedAt: integer("completed_at", { mode: "timestamp" })
|
|
76
|
+
});
|
|
77
|
+
var todoItems = sqliteTable("todo_items", {
|
|
78
|
+
id: text("id").primaryKey(),
|
|
79
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
80
|
+
content: text("content").notNull(),
|
|
81
|
+
status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
|
|
82
|
+
order: integer("order").notNull().default(0),
|
|
83
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
84
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
85
|
+
});
|
|
86
|
+
var loadedSkills = sqliteTable("loaded_skills", {
|
|
87
|
+
id: text("id").primaryKey(),
|
|
88
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
89
|
+
skillName: text("skill_name").notNull(),
|
|
90
|
+
loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
91
|
+
});
|
|
92
|
+
var terminals = sqliteTable("terminals", {
|
|
93
|
+
id: text("id").primaryKey(),
|
|
94
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
95
|
+
name: text("name"),
|
|
96
|
+
// Optional friendly name (e.g., "dev-server")
|
|
97
|
+
command: text("command").notNull(),
|
|
98
|
+
// The command that was run
|
|
99
|
+
cwd: text("cwd").notNull(),
|
|
100
|
+
// Working directory
|
|
101
|
+
pid: integer("pid"),
|
|
102
|
+
// Process ID (null if not running)
|
|
103
|
+
status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
|
|
104
|
+
exitCode: integer("exit_code"),
|
|
105
|
+
// Exit code if stopped
|
|
106
|
+
error: text("error"),
|
|
107
|
+
// Error message if status is 'error'
|
|
108
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
109
|
+
stoppedAt: integer("stopped_at", { mode: "timestamp" })
|
|
110
|
+
});
|
|
111
|
+
var activeStreams = sqliteTable("active_streams", {
|
|
112
|
+
id: text("id").primaryKey(),
|
|
113
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
114
|
+
streamId: text("stream_id").notNull().unique(),
|
|
115
|
+
// Unique stream identifier
|
|
116
|
+
status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
|
|
117
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
118
|
+
finishedAt: integer("finished_at", { mode: "timestamp" })
|
|
119
|
+
});
|
|
120
|
+
var checkpoints = sqliteTable("checkpoints", {
|
|
121
|
+
id: text("id").primaryKey(),
|
|
122
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
123
|
+
// The message sequence number this checkpoint was created BEFORE
|
|
124
|
+
// (i.e., the state before this user message was processed)
|
|
125
|
+
messageSequence: integer("message_sequence").notNull(),
|
|
126
|
+
// Optional git commit hash if in a git repo
|
|
127
|
+
gitHead: text("git_head"),
|
|
128
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
129
|
+
});
|
|
130
|
+
var fileBackups = sqliteTable("file_backups", {
|
|
131
|
+
id: text("id").primaryKey(),
|
|
132
|
+
checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
|
|
133
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
134
|
+
// Relative path from working directory
|
|
135
|
+
filePath: text("file_path").notNull(),
|
|
136
|
+
// Original content (null means file didn't exist before)
|
|
137
|
+
originalContent: text("original_content"),
|
|
138
|
+
// Whether the file existed before this checkpoint
|
|
139
|
+
existed: integer("existed", { mode: "boolean" }).notNull().default(true),
|
|
140
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
104
141
|
});
|
|
105
142
|
|
|
106
143
|
// src/db/index.ts
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
activeStreamQueries: () => activeStreamQueries,
|
|
110
|
-
closeDatabase: () => closeDatabase,
|
|
111
|
-
getDb: () => getDb,
|
|
112
|
-
initDatabase: () => initDatabase,
|
|
113
|
-
messageQueries: () => messageQueries,
|
|
114
|
-
sessionQueries: () => sessionQueries,
|
|
115
|
-
skillQueries: () => skillQueries,
|
|
116
|
-
terminalQueries: () => terminalQueries,
|
|
117
|
-
todoQueries: () => todoQueries,
|
|
118
|
-
toolExecutionQueries: () => toolExecutionQueries
|
|
119
|
-
});
|
|
120
|
-
import Database from "better-sqlite3";
|
|
121
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
122
|
-
import { eq, desc, and, sql } from "drizzle-orm";
|
|
123
|
-
import { nanoid } from "nanoid";
|
|
144
|
+
var db = null;
|
|
145
|
+
var sqlite = null;
|
|
124
146
|
function initDatabase(dbPath) {
|
|
125
147
|
sqlite = new Database(dbPath);
|
|
126
148
|
sqlite.pragma("journal_mode = WAL");
|
|
@@ -201,12 +223,35 @@ function initDatabase(dbPath) {
|
|
|
201
223
|
finished_at INTEGER
|
|
202
224
|
);
|
|
203
225
|
|
|
226
|
+
-- Checkpoints table - created before each user message
|
|
227
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
228
|
+
id TEXT PRIMARY KEY,
|
|
229
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
230
|
+
message_sequence INTEGER NOT NULL,
|
|
231
|
+
git_head TEXT,
|
|
232
|
+
created_at INTEGER NOT NULL
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
-- File backups table - stores original file content
|
|
236
|
+
CREATE TABLE IF NOT EXISTS file_backups (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
checkpoint_id TEXT NOT NULL REFERENCES checkpoints(id) ON DELETE CASCADE,
|
|
239
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
240
|
+
file_path TEXT NOT NULL,
|
|
241
|
+
original_content TEXT,
|
|
242
|
+
existed INTEGER NOT NULL DEFAULT 1,
|
|
243
|
+
created_at INTEGER NOT NULL
|
|
244
|
+
);
|
|
245
|
+
|
|
204
246
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
205
247
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
|
|
206
248
|
CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
|
|
207
249
|
CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
|
|
208
250
|
CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
|
|
209
251
|
CREATE INDEX IF NOT EXISTS idx_active_streams_session ON active_streams(session_id);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_file_backups_checkpoint ON file_backups(checkpoint_id);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_file_backups_session ON file_backups(session_id);
|
|
210
255
|
`);
|
|
211
256
|
return db;
|
|
212
257
|
}
|
|
@@ -223,321 +268,385 @@ function closeDatabase() {
|
|
|
223
268
|
db = null;
|
|
224
269
|
}
|
|
225
270
|
}
|
|
226
|
-
var
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
257
|
-
update(id, updates) {
|
|
258
|
-
return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
259
|
-
},
|
|
260
|
-
delete(id) {
|
|
261
|
-
const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
|
|
262
|
-
return result.changes > 0;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
messageQueries = {
|
|
266
|
-
/**
|
|
267
|
-
* Get the next sequence number for a session
|
|
268
|
-
*/
|
|
269
|
-
getNextSequence(sessionId) {
|
|
270
|
-
const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
271
|
-
return (result?.maxSeq ?? -1) + 1;
|
|
272
|
-
},
|
|
273
|
-
/**
|
|
274
|
-
* Create a single message from a ModelMessage
|
|
275
|
-
*/
|
|
276
|
-
create(sessionId, modelMessage) {
|
|
277
|
-
const id = nanoid();
|
|
278
|
-
const sequence = this.getNextSequence(sessionId);
|
|
279
|
-
const result = getDb().insert(messages).values({
|
|
280
|
-
id,
|
|
281
|
-
sessionId,
|
|
282
|
-
modelMessage,
|
|
283
|
-
sequence,
|
|
284
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
285
|
-
}).returning().get();
|
|
286
|
-
return result;
|
|
287
|
-
},
|
|
288
|
-
/**
|
|
289
|
-
* Add multiple ModelMessages at once (from response.messages)
|
|
290
|
-
* Maintains insertion order via sequence numbers
|
|
291
|
-
*/
|
|
292
|
-
addMany(sessionId, modelMessages) {
|
|
293
|
-
const results = [];
|
|
294
|
-
let sequence = this.getNextSequence(sessionId);
|
|
295
|
-
for (const msg of modelMessages) {
|
|
296
|
-
const id = nanoid();
|
|
297
|
-
const result = getDb().insert(messages).values({
|
|
298
|
-
id,
|
|
299
|
-
sessionId,
|
|
300
|
-
modelMessage: msg,
|
|
301
|
-
sequence,
|
|
302
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
303
|
-
}).returning().get();
|
|
304
|
-
results.push(result);
|
|
305
|
-
sequence++;
|
|
306
|
-
}
|
|
307
|
-
return results;
|
|
308
|
-
},
|
|
309
|
-
/**
|
|
310
|
-
* Get all messages for a session as ModelMessage[]
|
|
311
|
-
* Ordered by sequence to maintain exact insertion order
|
|
312
|
-
*/
|
|
313
|
-
getBySession(sessionId) {
|
|
314
|
-
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
|
|
315
|
-
},
|
|
316
|
-
/**
|
|
317
|
-
* Get ModelMessages directly (for passing to AI SDK)
|
|
318
|
-
*/
|
|
319
|
-
getModelMessages(sessionId) {
|
|
320
|
-
const messages2 = this.getBySession(sessionId);
|
|
321
|
-
return messages2.map((m) => m.modelMessage);
|
|
322
|
-
},
|
|
323
|
-
getRecentBySession(sessionId, limit = 50) {
|
|
324
|
-
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
|
|
325
|
-
},
|
|
326
|
-
countBySession(sessionId) {
|
|
327
|
-
const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
328
|
-
return result?.count ?? 0;
|
|
329
|
-
},
|
|
330
|
-
deleteBySession(sessionId) {
|
|
331
|
-
const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
|
|
332
|
-
return result.changes;
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
toolExecutionQueries = {
|
|
336
|
-
create(data) {
|
|
337
|
-
const id = nanoid();
|
|
338
|
-
const result = getDb().insert(toolExecutions).values({
|
|
339
|
-
id,
|
|
340
|
-
...data,
|
|
341
|
-
startedAt: /* @__PURE__ */ new Date()
|
|
342
|
-
}).returning().get();
|
|
343
|
-
return result;
|
|
344
|
-
},
|
|
345
|
-
getById(id) {
|
|
346
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
|
|
347
|
-
},
|
|
348
|
-
getByToolCallId(toolCallId) {
|
|
349
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
|
|
350
|
-
},
|
|
351
|
-
getPendingApprovals(sessionId) {
|
|
352
|
-
return getDb().select().from(toolExecutions).where(
|
|
353
|
-
and(
|
|
354
|
-
eq(toolExecutions.sessionId, sessionId),
|
|
355
|
-
eq(toolExecutions.status, "pending"),
|
|
356
|
-
eq(toolExecutions.requiresApproval, true)
|
|
357
|
-
)
|
|
358
|
-
).all();
|
|
359
|
-
},
|
|
360
|
-
approve(id) {
|
|
361
|
-
return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
362
|
-
},
|
|
363
|
-
reject(id) {
|
|
364
|
-
return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
365
|
-
},
|
|
366
|
-
complete(id, output, error) {
|
|
367
|
-
return getDb().update(toolExecutions).set({
|
|
368
|
-
status: error ? "error" : "completed",
|
|
369
|
-
output,
|
|
370
|
-
error,
|
|
371
|
-
completedAt: /* @__PURE__ */ new Date()
|
|
372
|
-
}).where(eq(toolExecutions.id, id)).returning().get();
|
|
373
|
-
},
|
|
374
|
-
getBySession(sessionId) {
|
|
375
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
todoQueries = {
|
|
379
|
-
create(data) {
|
|
380
|
-
const id = nanoid();
|
|
381
|
-
const now = /* @__PURE__ */ new Date();
|
|
382
|
-
const result = getDb().insert(todoItems).values({
|
|
383
|
-
id,
|
|
384
|
-
...data,
|
|
385
|
-
createdAt: now,
|
|
386
|
-
updatedAt: now
|
|
387
|
-
}).returning().get();
|
|
388
|
-
return result;
|
|
389
|
-
},
|
|
390
|
-
createMany(sessionId, items) {
|
|
391
|
-
const now = /* @__PURE__ */ new Date();
|
|
392
|
-
const values = items.map((item, index) => ({
|
|
393
|
-
id: nanoid(),
|
|
394
|
-
sessionId,
|
|
395
|
-
content: item.content,
|
|
396
|
-
order: item.order ?? index,
|
|
397
|
-
createdAt: now,
|
|
398
|
-
updatedAt: now
|
|
399
|
-
}));
|
|
400
|
-
return getDb().insert(todoItems).values(values).returning().all();
|
|
401
|
-
},
|
|
402
|
-
getBySession(sessionId) {
|
|
403
|
-
return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
|
|
404
|
-
},
|
|
405
|
-
updateStatus(id, status) {
|
|
406
|
-
return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
|
|
407
|
-
},
|
|
408
|
-
delete(id) {
|
|
409
|
-
const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
|
|
410
|
-
return result.changes > 0;
|
|
411
|
-
},
|
|
412
|
-
clearSession(sessionId) {
|
|
413
|
-
const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
|
|
414
|
-
return result.changes;
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
skillQueries = {
|
|
418
|
-
load(sessionId, skillName) {
|
|
419
|
-
const id = nanoid();
|
|
420
|
-
const result = getDb().insert(loadedSkills).values({
|
|
421
|
-
id,
|
|
422
|
-
sessionId,
|
|
423
|
-
skillName,
|
|
424
|
-
loadedAt: /* @__PURE__ */ new Date()
|
|
425
|
-
}).returning().get();
|
|
426
|
-
return result;
|
|
427
|
-
},
|
|
428
|
-
getBySession(sessionId) {
|
|
429
|
-
return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
|
|
430
|
-
},
|
|
431
|
-
isLoaded(sessionId, skillName) {
|
|
432
|
-
const result = getDb().select().from(loadedSkills).where(
|
|
433
|
-
and(
|
|
434
|
-
eq(loadedSkills.sessionId, sessionId),
|
|
435
|
-
eq(loadedSkills.skillName, skillName)
|
|
436
|
-
)
|
|
437
|
-
).get();
|
|
438
|
-
return !!result;
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
terminalQueries = {
|
|
442
|
-
create(data) {
|
|
443
|
-
const id = nanoid();
|
|
444
|
-
const result = getDb().insert(terminals).values({
|
|
445
|
-
id,
|
|
446
|
-
...data,
|
|
447
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
448
|
-
}).returning().get();
|
|
449
|
-
return result;
|
|
450
|
-
},
|
|
451
|
-
getById(id) {
|
|
452
|
-
return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
|
|
453
|
-
},
|
|
454
|
-
getBySession(sessionId) {
|
|
455
|
-
return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
|
|
456
|
-
},
|
|
457
|
-
getRunning(sessionId) {
|
|
458
|
-
return getDb().select().from(terminals).where(
|
|
459
|
-
and(
|
|
460
|
-
eq(terminals.sessionId, sessionId),
|
|
461
|
-
eq(terminals.status, "running")
|
|
462
|
-
)
|
|
463
|
-
).all();
|
|
464
|
-
},
|
|
465
|
-
updateStatus(id, status, exitCode, error) {
|
|
466
|
-
return getDb().update(terminals).set({
|
|
467
|
-
status,
|
|
468
|
-
exitCode,
|
|
469
|
-
error,
|
|
470
|
-
stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
|
|
471
|
-
}).where(eq(terminals.id, id)).returning().get();
|
|
472
|
-
},
|
|
473
|
-
updatePid(id, pid) {
|
|
474
|
-
return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
|
|
475
|
-
},
|
|
476
|
-
delete(id) {
|
|
477
|
-
const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
|
|
478
|
-
return result.changes > 0;
|
|
479
|
-
},
|
|
480
|
-
deleteBySession(sessionId) {
|
|
481
|
-
const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
|
|
482
|
-
return result.changes;
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
activeStreamQueries = {
|
|
486
|
-
create(sessionId, streamId) {
|
|
487
|
-
const id = nanoid();
|
|
488
|
-
const result = getDb().insert(activeStreams).values({
|
|
489
|
-
id,
|
|
490
|
-
sessionId,
|
|
491
|
-
streamId,
|
|
492
|
-
status: "active",
|
|
493
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
494
|
-
}).returning().get();
|
|
495
|
-
return result;
|
|
496
|
-
},
|
|
497
|
-
getBySessionId(sessionId) {
|
|
498
|
-
return getDb().select().from(activeStreams).where(
|
|
499
|
-
and(
|
|
500
|
-
eq(activeStreams.sessionId, sessionId),
|
|
501
|
-
eq(activeStreams.status, "active")
|
|
502
|
-
)
|
|
503
|
-
).get();
|
|
504
|
-
},
|
|
505
|
-
getByStreamId(streamId) {
|
|
506
|
-
return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
|
|
507
|
-
},
|
|
508
|
-
finish(streamId) {
|
|
509
|
-
return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
510
|
-
},
|
|
511
|
-
markError(streamId) {
|
|
512
|
-
return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
513
|
-
},
|
|
514
|
-
deleteBySession(sessionId) {
|
|
515
|
-
const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
|
|
516
|
-
return result.changes;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
271
|
+
var sessionQueries = {
|
|
272
|
+
create(data) {
|
|
273
|
+
const id = nanoid();
|
|
274
|
+
const now = /* @__PURE__ */ new Date();
|
|
275
|
+
const result = getDb().insert(sessions).values({
|
|
276
|
+
id,
|
|
277
|
+
...data,
|
|
278
|
+
createdAt: now,
|
|
279
|
+
updatedAt: now
|
|
280
|
+
}).returning().get();
|
|
281
|
+
return result;
|
|
282
|
+
},
|
|
283
|
+
getById(id) {
|
|
284
|
+
return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
|
|
285
|
+
},
|
|
286
|
+
list(limit = 50, offset = 0) {
|
|
287
|
+
return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
|
|
288
|
+
},
|
|
289
|
+
updateStatus(id, status) {
|
|
290
|
+
return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
291
|
+
},
|
|
292
|
+
updateModel(id, model) {
|
|
293
|
+
return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
294
|
+
},
|
|
295
|
+
update(id, updates) {
|
|
296
|
+
return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
297
|
+
},
|
|
298
|
+
delete(id) {
|
|
299
|
+
const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
|
|
300
|
+
return result.changes > 0;
|
|
519
301
|
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
302
|
+
};
|
|
303
|
+
var messageQueries = {
|
|
304
|
+
/**
|
|
305
|
+
* Get the next sequence number for a session
|
|
306
|
+
*/
|
|
307
|
+
getNextSequence(sessionId) {
|
|
308
|
+
const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
309
|
+
return (result?.maxSeq ?? -1) + 1;
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Create a single message from a ModelMessage
|
|
313
|
+
*/
|
|
314
|
+
create(sessionId, modelMessage) {
|
|
315
|
+
const id = nanoid();
|
|
316
|
+
const sequence = this.getNextSequence(sessionId);
|
|
317
|
+
const result = getDb().insert(messages).values({
|
|
318
|
+
id,
|
|
319
|
+
sessionId,
|
|
320
|
+
modelMessage,
|
|
321
|
+
sequence,
|
|
322
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
323
|
+
}).returning().get();
|
|
324
|
+
return result;
|
|
325
|
+
},
|
|
326
|
+
/**
|
|
327
|
+
* Add multiple ModelMessages at once (from response.messages)
|
|
328
|
+
* Maintains insertion order via sequence numbers
|
|
329
|
+
*/
|
|
330
|
+
addMany(sessionId, modelMessages) {
|
|
331
|
+
const results = [];
|
|
332
|
+
let sequence = this.getNextSequence(sessionId);
|
|
333
|
+
for (const msg of modelMessages) {
|
|
334
|
+
const id = nanoid();
|
|
335
|
+
const result = getDb().insert(messages).values({
|
|
336
|
+
id,
|
|
337
|
+
sessionId,
|
|
338
|
+
modelMessage: msg,
|
|
339
|
+
sequence,
|
|
340
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
341
|
+
}).returning().get();
|
|
342
|
+
results.push(result);
|
|
343
|
+
sequence++;
|
|
344
|
+
}
|
|
345
|
+
return results;
|
|
346
|
+
},
|
|
347
|
+
/**
|
|
348
|
+
* Get all messages for a session as ModelMessage[]
|
|
349
|
+
* Ordered by sequence to maintain exact insertion order
|
|
350
|
+
*/
|
|
351
|
+
getBySession(sessionId) {
|
|
352
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
|
|
353
|
+
},
|
|
354
|
+
/**
|
|
355
|
+
* Get ModelMessages directly (for passing to AI SDK)
|
|
356
|
+
*/
|
|
357
|
+
getModelMessages(sessionId) {
|
|
358
|
+
const messages2 = this.getBySession(sessionId);
|
|
359
|
+
return messages2.map((m) => m.modelMessage);
|
|
360
|
+
},
|
|
361
|
+
getRecentBySession(sessionId, limit = 50) {
|
|
362
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
|
|
363
|
+
},
|
|
364
|
+
countBySession(sessionId) {
|
|
365
|
+
const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
366
|
+
return result?.count ?? 0;
|
|
367
|
+
},
|
|
368
|
+
deleteBySession(sessionId) {
|
|
369
|
+
const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
|
|
370
|
+
return result.changes;
|
|
371
|
+
},
|
|
372
|
+
/**
|
|
373
|
+
* Delete all messages with sequence >= the given sequence number
|
|
374
|
+
* (Used when reverting to a checkpoint)
|
|
375
|
+
*/
|
|
376
|
+
deleteFromSequence(sessionId, fromSequence) {
|
|
377
|
+
const result = getDb().delete(messages).where(
|
|
378
|
+
and(
|
|
379
|
+
eq(messages.sessionId, sessionId),
|
|
380
|
+
sql`sequence >= ${fromSequence}`
|
|
381
|
+
)
|
|
382
|
+
).run();
|
|
383
|
+
return result.changes;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var toolExecutionQueries = {
|
|
387
|
+
create(data) {
|
|
388
|
+
const id = nanoid();
|
|
389
|
+
const result = getDb().insert(toolExecutions).values({
|
|
390
|
+
id,
|
|
391
|
+
...data,
|
|
392
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
393
|
+
}).returning().get();
|
|
394
|
+
return result;
|
|
395
|
+
},
|
|
396
|
+
getById(id) {
|
|
397
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
|
|
398
|
+
},
|
|
399
|
+
getByToolCallId(toolCallId) {
|
|
400
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
|
|
401
|
+
},
|
|
402
|
+
getPendingApprovals(sessionId) {
|
|
403
|
+
return getDb().select().from(toolExecutions).where(
|
|
404
|
+
and(
|
|
405
|
+
eq(toolExecutions.sessionId, sessionId),
|
|
406
|
+
eq(toolExecutions.status, "pending"),
|
|
407
|
+
eq(toolExecutions.requiresApproval, true)
|
|
408
|
+
)
|
|
409
|
+
).all();
|
|
410
|
+
},
|
|
411
|
+
approve(id) {
|
|
412
|
+
return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
413
|
+
},
|
|
414
|
+
reject(id) {
|
|
415
|
+
return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
416
|
+
},
|
|
417
|
+
complete(id, output, error) {
|
|
418
|
+
return getDb().update(toolExecutions).set({
|
|
419
|
+
status: error ? "error" : "completed",
|
|
420
|
+
output,
|
|
421
|
+
error,
|
|
422
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
423
|
+
}).where(eq(toolExecutions.id, id)).returning().get();
|
|
424
|
+
},
|
|
425
|
+
getBySession(sessionId) {
|
|
426
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
|
|
427
|
+
},
|
|
428
|
+
/**
|
|
429
|
+
* Delete all tool executions after a given timestamp
|
|
430
|
+
* (Used when reverting to a checkpoint)
|
|
431
|
+
*/
|
|
432
|
+
deleteAfterTime(sessionId, afterTime) {
|
|
433
|
+
const result = getDb().delete(toolExecutions).where(
|
|
434
|
+
and(
|
|
435
|
+
eq(toolExecutions.sessionId, sessionId),
|
|
436
|
+
sql`started_at > ${afterTime.getTime()}`
|
|
437
|
+
)
|
|
438
|
+
).run();
|
|
439
|
+
return result.changes;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var todoQueries = {
|
|
443
|
+
create(data) {
|
|
444
|
+
const id = nanoid();
|
|
445
|
+
const now = /* @__PURE__ */ new Date();
|
|
446
|
+
const result = getDb().insert(todoItems).values({
|
|
447
|
+
id,
|
|
448
|
+
...data,
|
|
449
|
+
createdAt: now,
|
|
450
|
+
updatedAt: now
|
|
451
|
+
}).returning().get();
|
|
452
|
+
return result;
|
|
453
|
+
},
|
|
454
|
+
createMany(sessionId, items) {
|
|
455
|
+
const now = /* @__PURE__ */ new Date();
|
|
456
|
+
const values = items.map((item, index) => ({
|
|
457
|
+
id: nanoid(),
|
|
458
|
+
sessionId,
|
|
459
|
+
content: item.content,
|
|
460
|
+
order: item.order ?? index,
|
|
461
|
+
createdAt: now,
|
|
462
|
+
updatedAt: now
|
|
463
|
+
}));
|
|
464
|
+
return getDb().insert(todoItems).values(values).returning().all();
|
|
465
|
+
},
|
|
466
|
+
getBySession(sessionId) {
|
|
467
|
+
return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
|
|
468
|
+
},
|
|
469
|
+
updateStatus(id, status) {
|
|
470
|
+
return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
|
|
471
|
+
},
|
|
472
|
+
delete(id) {
|
|
473
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
|
|
474
|
+
return result.changes > 0;
|
|
475
|
+
},
|
|
476
|
+
clearSession(sessionId) {
|
|
477
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
|
|
478
|
+
return result.changes;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var skillQueries = {
|
|
482
|
+
load(sessionId, skillName) {
|
|
483
|
+
const id = nanoid();
|
|
484
|
+
const result = getDb().insert(loadedSkills).values({
|
|
485
|
+
id,
|
|
486
|
+
sessionId,
|
|
487
|
+
skillName,
|
|
488
|
+
loadedAt: /* @__PURE__ */ new Date()
|
|
489
|
+
}).returning().get();
|
|
490
|
+
return result;
|
|
491
|
+
},
|
|
492
|
+
getBySession(sessionId) {
|
|
493
|
+
return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
|
|
494
|
+
},
|
|
495
|
+
isLoaded(sessionId, skillName) {
|
|
496
|
+
const result = getDb().select().from(loadedSkills).where(
|
|
497
|
+
and(
|
|
498
|
+
eq(loadedSkills.sessionId, sessionId),
|
|
499
|
+
eq(loadedSkills.skillName, skillName)
|
|
500
|
+
)
|
|
501
|
+
).get();
|
|
502
|
+
return !!result;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
var activeStreamQueries = {
|
|
506
|
+
create(sessionId, streamId) {
|
|
507
|
+
const id = nanoid();
|
|
508
|
+
const result = getDb().insert(activeStreams).values({
|
|
509
|
+
id,
|
|
510
|
+
sessionId,
|
|
511
|
+
streamId,
|
|
512
|
+
status: "active",
|
|
513
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
514
|
+
}).returning().get();
|
|
515
|
+
return result;
|
|
516
|
+
},
|
|
517
|
+
getBySessionId(sessionId) {
|
|
518
|
+
return getDb().select().from(activeStreams).where(
|
|
519
|
+
and(
|
|
520
|
+
eq(activeStreams.sessionId, sessionId),
|
|
521
|
+
eq(activeStreams.status, "active")
|
|
522
|
+
)
|
|
523
|
+
).get();
|
|
524
|
+
},
|
|
525
|
+
getByStreamId(streamId) {
|
|
526
|
+
return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
|
|
527
|
+
},
|
|
528
|
+
finish(streamId) {
|
|
529
|
+
return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
530
|
+
},
|
|
531
|
+
markError(streamId) {
|
|
532
|
+
return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
533
|
+
},
|
|
534
|
+
deleteBySession(sessionId) {
|
|
535
|
+
const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
|
|
536
|
+
return result.changes;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var checkpointQueries = {
|
|
540
|
+
create(data) {
|
|
541
|
+
const id = nanoid();
|
|
542
|
+
const result = getDb().insert(checkpoints).values({
|
|
543
|
+
id,
|
|
544
|
+
sessionId: data.sessionId,
|
|
545
|
+
messageSequence: data.messageSequence,
|
|
546
|
+
gitHead: data.gitHead,
|
|
547
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
548
|
+
}).returning().get();
|
|
549
|
+
return result;
|
|
550
|
+
},
|
|
551
|
+
getById(id) {
|
|
552
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.id, id)).get();
|
|
553
|
+
},
|
|
554
|
+
getBySession(sessionId) {
|
|
555
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(checkpoints.messageSequence).all();
|
|
556
|
+
},
|
|
557
|
+
getByMessageSequence(sessionId, messageSequence) {
|
|
558
|
+
return getDb().select().from(checkpoints).where(
|
|
559
|
+
and(
|
|
560
|
+
eq(checkpoints.sessionId, sessionId),
|
|
561
|
+
eq(checkpoints.messageSequence, messageSequence)
|
|
562
|
+
)
|
|
563
|
+
).get();
|
|
564
|
+
},
|
|
565
|
+
getLatest(sessionId) {
|
|
566
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(desc(checkpoints.messageSequence)).limit(1).get();
|
|
567
|
+
},
|
|
568
|
+
/**
|
|
569
|
+
* Delete all checkpoints after a given sequence number
|
|
570
|
+
* (Used when reverting to a checkpoint)
|
|
571
|
+
*/
|
|
572
|
+
deleteAfterSequence(sessionId, messageSequence) {
|
|
573
|
+
const result = getDb().delete(checkpoints).where(
|
|
574
|
+
and(
|
|
575
|
+
eq(checkpoints.sessionId, sessionId),
|
|
576
|
+
sql`message_sequence > ${messageSequence}`
|
|
577
|
+
)
|
|
578
|
+
).run();
|
|
579
|
+
return result.changes;
|
|
580
|
+
},
|
|
581
|
+
deleteBySession(sessionId) {
|
|
582
|
+
const result = getDb().delete(checkpoints).where(eq(checkpoints.sessionId, sessionId)).run();
|
|
583
|
+
return result.changes;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var fileBackupQueries = {
|
|
587
|
+
create(data) {
|
|
588
|
+
const id = nanoid();
|
|
589
|
+
const result = getDb().insert(fileBackups).values({
|
|
590
|
+
id,
|
|
591
|
+
checkpointId: data.checkpointId,
|
|
592
|
+
sessionId: data.sessionId,
|
|
593
|
+
filePath: data.filePath,
|
|
594
|
+
originalContent: data.originalContent,
|
|
595
|
+
existed: data.existed,
|
|
596
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
597
|
+
}).returning().get();
|
|
598
|
+
return result;
|
|
599
|
+
},
|
|
600
|
+
getByCheckpoint(checkpointId) {
|
|
601
|
+
return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
|
|
602
|
+
},
|
|
603
|
+
getBySession(sessionId) {
|
|
604
|
+
return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
|
|
605
|
+
},
|
|
606
|
+
/**
|
|
607
|
+
* Get all file backups from a given checkpoint sequence onwards (inclusive)
|
|
608
|
+
* (Used when reverting - need to restore these files)
|
|
609
|
+
*
|
|
610
|
+
* When reverting to checkpoint X, we need backups from checkpoint X and all later ones
|
|
611
|
+
* because checkpoint X's backups represent the state BEFORE processing message X.
|
|
612
|
+
*/
|
|
613
|
+
getFromSequence(sessionId, messageSequence) {
|
|
614
|
+
const checkpointsFrom = getDb().select().from(checkpoints).where(
|
|
615
|
+
and(
|
|
616
|
+
eq(checkpoints.sessionId, sessionId),
|
|
617
|
+
sql`message_sequence >= ${messageSequence}`
|
|
618
|
+
)
|
|
619
|
+
).all();
|
|
620
|
+
if (checkpointsFrom.length === 0) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
const checkpointIds = checkpointsFrom.map((c) => c.id);
|
|
624
|
+
const allBackups = [];
|
|
625
|
+
for (const cpId of checkpointIds) {
|
|
626
|
+
const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
|
|
627
|
+
allBackups.push(...backups);
|
|
628
|
+
}
|
|
629
|
+
return allBackups;
|
|
630
|
+
},
|
|
631
|
+
/**
|
|
632
|
+
* Check if a file already has a backup in the current checkpoint
|
|
633
|
+
*/
|
|
634
|
+
hasBackup(checkpointId, filePath) {
|
|
635
|
+
const result = getDb().select().from(fileBackups).where(
|
|
636
|
+
and(
|
|
637
|
+
eq(fileBackups.checkpointId, checkpointId),
|
|
638
|
+
eq(fileBackups.filePath, filePath)
|
|
639
|
+
)
|
|
640
|
+
).get();
|
|
641
|
+
return !!result;
|
|
642
|
+
},
|
|
643
|
+
deleteBySession(sessionId) {
|
|
644
|
+
const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
|
|
645
|
+
return result.changes;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
538
648
|
|
|
539
649
|
// src/agent/index.ts
|
|
540
|
-
init_db();
|
|
541
650
|
import {
|
|
542
651
|
streamText,
|
|
543
652
|
generateText as generateText2,
|
|
@@ -883,7 +992,6 @@ async function isTmuxAvailable() {
|
|
|
883
992
|
try {
|
|
884
993
|
const { stdout } = await execAsync("tmux -V");
|
|
885
994
|
tmuxAvailableCache = true;
|
|
886
|
-
console.log(`[tmux] Available: ${stdout.trim()}`);
|
|
887
995
|
return true;
|
|
888
996
|
} catch (error) {
|
|
889
997
|
tmuxAvailableCache = false;
|
|
@@ -1503,9 +1611,198 @@ Use this to understand existing code, check file contents, or gather context.`,
|
|
|
1503
1611
|
// src/tools/write-file.ts
|
|
1504
1612
|
import { tool as tool3 } from "ai";
|
|
1505
1613
|
import { z as z4 } from "zod";
|
|
1506
|
-
import { readFile as
|
|
1507
|
-
import { resolve as
|
|
1614
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
1615
|
+
import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
|
|
1616
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1617
|
+
|
|
1618
|
+
// src/checkpoints/index.ts
|
|
1619
|
+
import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
|
|
1508
1620
|
import { existsSync as existsSync4 } from "fs";
|
|
1621
|
+
import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
|
|
1622
|
+
import { exec as exec3 } from "child_process";
|
|
1623
|
+
import { promisify as promisify3 } from "util";
|
|
1624
|
+
var execAsync3 = promisify3(exec3);
|
|
1625
|
+
async function getGitHead(workingDirectory) {
|
|
1626
|
+
try {
|
|
1627
|
+
const { stdout } = await execAsync3("git rev-parse HEAD", {
|
|
1628
|
+
cwd: workingDirectory,
|
|
1629
|
+
timeout: 5e3
|
|
1630
|
+
});
|
|
1631
|
+
return stdout.trim();
|
|
1632
|
+
} catch {
|
|
1633
|
+
return void 0;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
var activeManagers = /* @__PURE__ */ new Map();
|
|
1637
|
+
function getCheckpointManager(sessionId, workingDirectory) {
|
|
1638
|
+
let manager = activeManagers.get(sessionId);
|
|
1639
|
+
if (!manager) {
|
|
1640
|
+
manager = {
|
|
1641
|
+
sessionId,
|
|
1642
|
+
workingDirectory,
|
|
1643
|
+
currentCheckpointId: null
|
|
1644
|
+
};
|
|
1645
|
+
activeManagers.set(sessionId, manager);
|
|
1646
|
+
}
|
|
1647
|
+
return manager;
|
|
1648
|
+
}
|
|
1649
|
+
async function createCheckpoint(sessionId, workingDirectory, messageSequence) {
|
|
1650
|
+
const gitHead = await getGitHead(workingDirectory);
|
|
1651
|
+
const checkpoint = checkpointQueries.create({
|
|
1652
|
+
sessionId,
|
|
1653
|
+
messageSequence,
|
|
1654
|
+
gitHead
|
|
1655
|
+
});
|
|
1656
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1657
|
+
manager.currentCheckpointId = checkpoint.id;
|
|
1658
|
+
return checkpoint;
|
|
1659
|
+
}
|
|
1660
|
+
async function backupFile(sessionId, workingDirectory, filePath) {
|
|
1661
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1662
|
+
if (!manager.currentCheckpointId) {
|
|
1663
|
+
console.warn("[checkpoint] No active checkpoint, skipping file backup");
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1667
|
+
const relativePath = relative2(workingDirectory, absolutePath);
|
|
1668
|
+
if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
let originalContent = null;
|
|
1672
|
+
let existed = false;
|
|
1673
|
+
if (existsSync4(absolutePath)) {
|
|
1674
|
+
try {
|
|
1675
|
+
originalContent = await readFile3(absolutePath, "utf-8");
|
|
1676
|
+
existed = true;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const backup = fileBackupQueries.create({
|
|
1682
|
+
checkpointId: manager.currentCheckpointId,
|
|
1683
|
+
sessionId,
|
|
1684
|
+
filePath: relativePath,
|
|
1685
|
+
originalContent,
|
|
1686
|
+
existed
|
|
1687
|
+
});
|
|
1688
|
+
return backup;
|
|
1689
|
+
}
|
|
1690
|
+
async function revertToCheckpoint(sessionId, checkpointId) {
|
|
1691
|
+
const session = sessionQueries.getById(sessionId);
|
|
1692
|
+
if (!session) {
|
|
1693
|
+
return {
|
|
1694
|
+
success: false,
|
|
1695
|
+
filesRestored: 0,
|
|
1696
|
+
filesDeleted: 0,
|
|
1697
|
+
messagesDeleted: 0,
|
|
1698
|
+
checkpointsDeleted: 0,
|
|
1699
|
+
error: "Session not found"
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
const checkpoint = checkpointQueries.getById(checkpointId);
|
|
1703
|
+
if (!checkpoint || checkpoint.sessionId !== sessionId) {
|
|
1704
|
+
return {
|
|
1705
|
+
success: false,
|
|
1706
|
+
filesRestored: 0,
|
|
1707
|
+
filesDeleted: 0,
|
|
1708
|
+
messagesDeleted: 0,
|
|
1709
|
+
checkpointsDeleted: 0,
|
|
1710
|
+
error: "Checkpoint not found"
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const workingDirectory = session.workingDirectory;
|
|
1714
|
+
const backupsToRevert = fileBackupQueries.getFromSequence(sessionId, checkpoint.messageSequence);
|
|
1715
|
+
const fileToEarliestBackup = /* @__PURE__ */ new Map();
|
|
1716
|
+
for (const backup of backupsToRevert) {
|
|
1717
|
+
if (!fileToEarliestBackup.has(backup.filePath)) {
|
|
1718
|
+
fileToEarliestBackup.set(backup.filePath, backup);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
let filesRestored = 0;
|
|
1722
|
+
let filesDeleted = 0;
|
|
1723
|
+
for (const [filePath, backup] of fileToEarliestBackup) {
|
|
1724
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1725
|
+
try {
|
|
1726
|
+
if (backup.existed && backup.originalContent !== null) {
|
|
1727
|
+
const dir = dirname2(absolutePath);
|
|
1728
|
+
if (!existsSync4(dir)) {
|
|
1729
|
+
await mkdir2(dir, { recursive: true });
|
|
1730
|
+
}
|
|
1731
|
+
await writeFile2(absolutePath, backup.originalContent, "utf-8");
|
|
1732
|
+
filesRestored++;
|
|
1733
|
+
} else if (!backup.existed) {
|
|
1734
|
+
if (existsSync4(absolutePath)) {
|
|
1735
|
+
await unlink(absolutePath);
|
|
1736
|
+
filesDeleted++;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
console.error(`Failed to restore ${filePath}: ${error.message}`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const messagesDeleted = messageQueries.deleteFromSequence(sessionId, checkpoint.messageSequence);
|
|
1744
|
+
toolExecutionQueries.deleteAfterTime(sessionId, checkpoint.createdAt);
|
|
1745
|
+
const checkpointsDeleted = checkpointQueries.deleteAfterSequence(sessionId, checkpoint.messageSequence);
|
|
1746
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1747
|
+
manager.currentCheckpointId = checkpoint.id;
|
|
1748
|
+
return {
|
|
1749
|
+
success: true,
|
|
1750
|
+
filesRestored,
|
|
1751
|
+
filesDeleted,
|
|
1752
|
+
messagesDeleted,
|
|
1753
|
+
checkpointsDeleted
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function getCheckpoints(sessionId) {
|
|
1757
|
+
return checkpointQueries.getBySession(sessionId);
|
|
1758
|
+
}
|
|
1759
|
+
async function getSessionDiff(sessionId) {
|
|
1760
|
+
const session = sessionQueries.getById(sessionId);
|
|
1761
|
+
if (!session) {
|
|
1762
|
+
return { files: [] };
|
|
1763
|
+
}
|
|
1764
|
+
const workingDirectory = session.workingDirectory;
|
|
1765
|
+
const allBackups = fileBackupQueries.getBySession(sessionId);
|
|
1766
|
+
const fileToOriginalBackup = /* @__PURE__ */ new Map();
|
|
1767
|
+
for (const backup of allBackups) {
|
|
1768
|
+
if (!fileToOriginalBackup.has(backup.filePath)) {
|
|
1769
|
+
fileToOriginalBackup.set(backup.filePath, backup);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const files = [];
|
|
1773
|
+
for (const [filePath, originalBackup] of fileToOriginalBackup) {
|
|
1774
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1775
|
+
let currentContent = null;
|
|
1776
|
+
let currentExists = false;
|
|
1777
|
+
if (existsSync4(absolutePath)) {
|
|
1778
|
+
try {
|
|
1779
|
+
currentContent = await readFile3(absolutePath, "utf-8");
|
|
1780
|
+
currentExists = true;
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
let status;
|
|
1785
|
+
if (!originalBackup.existed && currentExists) {
|
|
1786
|
+
status = "created";
|
|
1787
|
+
} else if (originalBackup.existed && !currentExists) {
|
|
1788
|
+
status = "deleted";
|
|
1789
|
+
} else {
|
|
1790
|
+
status = "modified";
|
|
1791
|
+
}
|
|
1792
|
+
files.push({
|
|
1793
|
+
path: filePath,
|
|
1794
|
+
status,
|
|
1795
|
+
originalContent: originalBackup.originalContent,
|
|
1796
|
+
currentContent
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return { files };
|
|
1800
|
+
}
|
|
1801
|
+
function clearCheckpointManager(sessionId) {
|
|
1802
|
+
activeManagers.delete(sessionId);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// src/tools/write-file.ts
|
|
1509
1806
|
var writeFileInputSchema = z4.object({
|
|
1510
1807
|
path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
|
|
1511
1808
|
mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
|
|
@@ -1534,8 +1831,8 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1534
1831
|
inputSchema: writeFileInputSchema,
|
|
1535
1832
|
execute: async ({ path, mode, content, old_string, new_string }) => {
|
|
1536
1833
|
try {
|
|
1537
|
-
const absolutePath = isAbsolute2(path) ? path :
|
|
1538
|
-
const relativePath =
|
|
1834
|
+
const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
|
|
1835
|
+
const relativePath = relative3(options.workingDirectory, absolutePath);
|
|
1539
1836
|
if (relativePath.startsWith("..") && !isAbsolute2(path)) {
|
|
1540
1837
|
return {
|
|
1541
1838
|
success: false,
|
|
@@ -1549,16 +1846,17 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1549
1846
|
error: 'Content is required for "full" mode'
|
|
1550
1847
|
};
|
|
1551
1848
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1849
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
1850
|
+
const dir = dirname3(absolutePath);
|
|
1851
|
+
if (!existsSync5(dir)) {
|
|
1852
|
+
await mkdir3(dir, { recursive: true });
|
|
1555
1853
|
}
|
|
1556
|
-
const existed =
|
|
1557
|
-
await
|
|
1854
|
+
const existed = existsSync5(absolutePath);
|
|
1855
|
+
await writeFile3(absolutePath, content, "utf-8");
|
|
1558
1856
|
return {
|
|
1559
1857
|
success: true,
|
|
1560
1858
|
path: absolutePath,
|
|
1561
|
-
relativePath:
|
|
1859
|
+
relativePath: relative3(options.workingDirectory, absolutePath),
|
|
1562
1860
|
mode: "full",
|
|
1563
1861
|
action: existed ? "replaced" : "created",
|
|
1564
1862
|
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
@@ -1571,13 +1869,14 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1571
1869
|
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
1572
1870
|
};
|
|
1573
1871
|
}
|
|
1574
|
-
if (!
|
|
1872
|
+
if (!existsSync5(absolutePath)) {
|
|
1575
1873
|
return {
|
|
1576
1874
|
success: false,
|
|
1577
1875
|
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
1578
1876
|
};
|
|
1579
1877
|
}
|
|
1580
|
-
|
|
1878
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
1879
|
+
const currentContent = await readFile4(absolutePath, "utf-8");
|
|
1581
1880
|
if (!currentContent.includes(old_string)) {
|
|
1582
1881
|
const lines = currentContent.split("\n");
|
|
1583
1882
|
const preview = lines.slice(0, 20).join("\n");
|
|
@@ -1598,13 +1897,13 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1598
1897
|
};
|
|
1599
1898
|
}
|
|
1600
1899
|
const newContent = currentContent.replace(old_string, new_string);
|
|
1601
|
-
await
|
|
1900
|
+
await writeFile3(absolutePath, newContent, "utf-8");
|
|
1602
1901
|
const oldLines = old_string.split("\n").length;
|
|
1603
1902
|
const newLines = new_string.split("\n").length;
|
|
1604
1903
|
return {
|
|
1605
1904
|
success: true,
|
|
1606
1905
|
path: absolutePath,
|
|
1607
|
-
relativePath:
|
|
1906
|
+
relativePath: relative3(options.workingDirectory, absolutePath),
|
|
1608
1907
|
mode: "str_replace",
|
|
1609
1908
|
linesRemoved: oldLines,
|
|
1610
1909
|
linesAdded: newLines,
|
|
@@ -1626,7 +1925,6 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1626
1925
|
}
|
|
1627
1926
|
|
|
1628
1927
|
// src/tools/todo.ts
|
|
1629
|
-
init_db();
|
|
1630
1928
|
import { tool as tool4 } from "ai";
|
|
1631
1929
|
import { z as z5 } from "zod";
|
|
1632
1930
|
var todoInputSchema = z5.object({
|
|
@@ -1756,9 +2054,9 @@ import { tool as tool5 } from "ai";
|
|
|
1756
2054
|
import { z as z6 } from "zod";
|
|
1757
2055
|
|
|
1758
2056
|
// src/skills/index.ts
|
|
1759
|
-
import { readFile as
|
|
1760
|
-
import { resolve as
|
|
1761
|
-
import { existsSync as
|
|
2057
|
+
import { readFile as readFile5, readdir } from "fs/promises";
|
|
2058
|
+
import { resolve as resolve5, basename, extname } from "path";
|
|
2059
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1762
2060
|
function parseSkillFrontmatter(content) {
|
|
1763
2061
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1764
2062
|
if (!frontmatterMatch) {
|
|
@@ -1789,15 +2087,15 @@ function getSkillNameFromPath(filePath) {
|
|
|
1789
2087
|
return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1790
2088
|
}
|
|
1791
2089
|
async function loadSkillsFromDirectory(directory) {
|
|
1792
|
-
if (!
|
|
2090
|
+
if (!existsSync6(directory)) {
|
|
1793
2091
|
return [];
|
|
1794
2092
|
}
|
|
1795
2093
|
const skills = [];
|
|
1796
2094
|
const files = await readdir(directory);
|
|
1797
2095
|
for (const file of files) {
|
|
1798
2096
|
if (!file.endsWith(".md")) continue;
|
|
1799
|
-
const filePath =
|
|
1800
|
-
const content = await
|
|
2097
|
+
const filePath = resolve5(directory, file);
|
|
2098
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1801
2099
|
const parsed = parseSkillFrontmatter(content);
|
|
1802
2100
|
if (parsed) {
|
|
1803
2101
|
skills.push({
|
|
@@ -1839,7 +2137,7 @@ async function loadSkillContent(skillName, directories) {
|
|
|
1839
2137
|
if (!skill) {
|
|
1840
2138
|
return null;
|
|
1841
2139
|
}
|
|
1842
|
-
const content = await
|
|
2140
|
+
const content = await readFile5(skill.filePath, "utf-8");
|
|
1843
2141
|
const parsed = parseSkillFrontmatter(content);
|
|
1844
2142
|
return {
|
|
1845
2143
|
...skill,
|
|
@@ -1858,7 +2156,6 @@ function formatSkillsForContext(skills) {
|
|
|
1858
2156
|
}
|
|
1859
2157
|
|
|
1860
2158
|
// src/tools/load-skill.ts
|
|
1861
|
-
init_db();
|
|
1862
2159
|
var loadSkillInputSchema = z6.object({
|
|
1863
2160
|
action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
|
|
1864
2161
|
skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
|
|
@@ -1951,7 +2248,8 @@ function createTools(options) {
|
|
|
1951
2248
|
workingDirectory: options.workingDirectory
|
|
1952
2249
|
}),
|
|
1953
2250
|
write_file: createWriteFileTool({
|
|
1954
|
-
workingDirectory: options.workingDirectory
|
|
2251
|
+
workingDirectory: options.workingDirectory,
|
|
2252
|
+
sessionId: options.sessionId
|
|
1955
2253
|
}),
|
|
1956
2254
|
todo: createTodoTool({
|
|
1957
2255
|
sessionId: options.sessionId
|
|
@@ -1964,13 +2262,11 @@ function createTools(options) {
|
|
|
1964
2262
|
}
|
|
1965
2263
|
|
|
1966
2264
|
// src/agent/context.ts
|
|
1967
|
-
init_db();
|
|
1968
2265
|
import { generateText } from "ai";
|
|
1969
2266
|
import { gateway } from "@ai-sdk/gateway";
|
|
1970
2267
|
|
|
1971
2268
|
// src/agent/prompts.ts
|
|
1972
2269
|
import os from "os";
|
|
1973
|
-
init_db();
|
|
1974
2270
|
function getSearchInstructions() {
|
|
1975
2271
|
const platform3 = process.platform;
|
|
1976
2272
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
@@ -2011,6 +2307,9 @@ You have access to powerful tools for:
|
|
|
2011
2307
|
- **todo**: Manage your task list to track progress on complex operations
|
|
2012
2308
|
- **load_skill**: Load specialized knowledge documents for specific tasks
|
|
2013
2309
|
|
|
2310
|
+
|
|
2311
|
+
IMPORTANT: If you have zero context of where you are working, always explore it first to understand the structure before doing things for the user.
|
|
2312
|
+
|
|
2014
2313
|
Use the TODO tool to manage your task list to track progress on complex operations. Always ask the user what they want to do specifically before doing it, and make a plan.
|
|
2015
2314
|
Step 1 of the plan should be researching files and understanding the components/structure of what you're working on (if you don't already have context), then after u have done that, plan out the rest of the tasks u need to do.
|
|
2016
2315
|
You can clear the todo and restart it, and do multiple things inside of one session.
|
|
@@ -2447,8 +2746,8 @@ var Agent = class _Agent {
|
|
|
2447
2746
|
this.pendingApprovals.set(toolCallId, execution);
|
|
2448
2747
|
options.onApprovalRequired?.(execution);
|
|
2449
2748
|
sessionQueries.updateStatus(this.session.id, "waiting");
|
|
2450
|
-
const approved = await new Promise((
|
|
2451
|
-
approvalResolvers.set(toolCallId, { resolve:
|
|
2749
|
+
const approved = await new Promise((resolve7) => {
|
|
2750
|
+
approvalResolvers.set(toolCallId, { resolve: resolve7, sessionId: this.session.id });
|
|
2452
2751
|
});
|
|
2453
2752
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
2454
2753
|
approvalResolvers.delete(toolCallId);
|
|
@@ -2749,6 +3048,7 @@ sessions2.delete("/:id", async (c) => {
|
|
|
2749
3048
|
}
|
|
2750
3049
|
} catch (e) {
|
|
2751
3050
|
}
|
|
3051
|
+
clearCheckpointManager(id);
|
|
2752
3052
|
const deleted = sessionQueries.delete(id);
|
|
2753
3053
|
if (!deleted) {
|
|
2754
3054
|
return c.json({ error: "Session not found" }, 404);
|
|
@@ -2800,9 +3100,100 @@ sessions2.get("/:id/todos", async (c) => {
|
|
|
2800
3100
|
} : null
|
|
2801
3101
|
});
|
|
2802
3102
|
});
|
|
3103
|
+
sessions2.get("/:id/checkpoints", async (c) => {
|
|
3104
|
+
const id = c.req.param("id");
|
|
3105
|
+
const session = sessionQueries.getById(id);
|
|
3106
|
+
if (!session) {
|
|
3107
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3108
|
+
}
|
|
3109
|
+
const checkpoints2 = getCheckpoints(id);
|
|
3110
|
+
return c.json({
|
|
3111
|
+
sessionId: id,
|
|
3112
|
+
checkpoints: checkpoints2.map((cp) => ({
|
|
3113
|
+
id: cp.id,
|
|
3114
|
+
messageSequence: cp.messageSequence,
|
|
3115
|
+
gitHead: cp.gitHead,
|
|
3116
|
+
createdAt: cp.createdAt.toISOString()
|
|
3117
|
+
})),
|
|
3118
|
+
count: checkpoints2.length
|
|
3119
|
+
});
|
|
3120
|
+
});
|
|
3121
|
+
sessions2.post("/:id/revert/:checkpointId", async (c) => {
|
|
3122
|
+
const sessionId = c.req.param("id");
|
|
3123
|
+
const checkpointId = c.req.param("checkpointId");
|
|
3124
|
+
const session = sessionQueries.getById(sessionId);
|
|
3125
|
+
if (!session) {
|
|
3126
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3127
|
+
}
|
|
3128
|
+
const activeStream = activeStreamQueries.getBySessionId(sessionId);
|
|
3129
|
+
if (activeStream) {
|
|
3130
|
+
return c.json({
|
|
3131
|
+
error: "Cannot revert while a stream is active. Stop the stream first.",
|
|
3132
|
+
streamId: activeStream.streamId
|
|
3133
|
+
}, 409);
|
|
3134
|
+
}
|
|
3135
|
+
const result = await revertToCheckpoint(sessionId, checkpointId);
|
|
3136
|
+
if (!result.success) {
|
|
3137
|
+
return c.json({ error: result.error }, 400);
|
|
3138
|
+
}
|
|
3139
|
+
return c.json({
|
|
3140
|
+
success: true,
|
|
3141
|
+
sessionId,
|
|
3142
|
+
checkpointId,
|
|
3143
|
+
filesRestored: result.filesRestored,
|
|
3144
|
+
filesDeleted: result.filesDeleted,
|
|
3145
|
+
messagesDeleted: result.messagesDeleted,
|
|
3146
|
+
checkpointsDeleted: result.checkpointsDeleted
|
|
3147
|
+
});
|
|
3148
|
+
});
|
|
3149
|
+
sessions2.get("/:id/diff", async (c) => {
|
|
3150
|
+
const id = c.req.param("id");
|
|
3151
|
+
const session = sessionQueries.getById(id);
|
|
3152
|
+
if (!session) {
|
|
3153
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3154
|
+
}
|
|
3155
|
+
const diff = await getSessionDiff(id);
|
|
3156
|
+
return c.json({
|
|
3157
|
+
sessionId: id,
|
|
3158
|
+
files: diff.files.map((f) => ({
|
|
3159
|
+
path: f.path,
|
|
3160
|
+
status: f.status,
|
|
3161
|
+
hasOriginal: f.originalContent !== null,
|
|
3162
|
+
hasCurrent: f.currentContent !== null
|
|
3163
|
+
// Optionally include content (can be large)
|
|
3164
|
+
// originalContent: f.originalContent,
|
|
3165
|
+
// currentContent: f.currentContent,
|
|
3166
|
+
})),
|
|
3167
|
+
summary: {
|
|
3168
|
+
created: diff.files.filter((f) => f.status === "created").length,
|
|
3169
|
+
modified: diff.files.filter((f) => f.status === "modified").length,
|
|
3170
|
+
deleted: diff.files.filter((f) => f.status === "deleted").length,
|
|
3171
|
+
total: diff.files.length
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
});
|
|
3175
|
+
sessions2.get("/:id/diff/:filePath", async (c) => {
|
|
3176
|
+
const sessionId = c.req.param("id");
|
|
3177
|
+
const filePath = decodeURIComponent(c.req.param("filePath"));
|
|
3178
|
+
const session = sessionQueries.getById(sessionId);
|
|
3179
|
+
if (!session) {
|
|
3180
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3181
|
+
}
|
|
3182
|
+
const diff = await getSessionDiff(sessionId);
|
|
3183
|
+
const fileDiff = diff.files.find((f) => f.path === filePath);
|
|
3184
|
+
if (!fileDiff) {
|
|
3185
|
+
return c.json({ error: "File not found in diff" }, 404);
|
|
3186
|
+
}
|
|
3187
|
+
return c.json({
|
|
3188
|
+
sessionId,
|
|
3189
|
+
path: fileDiff.path,
|
|
3190
|
+
status: fileDiff.status,
|
|
3191
|
+
originalContent: fileDiff.originalContent,
|
|
3192
|
+
currentContent: fileDiff.currentContent
|
|
3193
|
+
});
|
|
3194
|
+
});
|
|
2803
3195
|
|
|
2804
3196
|
// src/server/routes/agents.ts
|
|
2805
|
-
init_db();
|
|
2806
3197
|
import { Hono as Hono2 } from "hono";
|
|
2807
3198
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
2808
3199
|
import { z as z9 } from "zod";
|
|
@@ -3076,8 +3467,9 @@ agents.post(
|
|
|
3076
3467
|
if (!session) {
|
|
3077
3468
|
return c.json({ error: "Session not found" }, 404);
|
|
3078
3469
|
}
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3470
|
+
const nextSequence = messageQueries.getNextSequence(id);
|
|
3471
|
+
await createCheckpoint(id, session.workingDirectory, nextSequence);
|
|
3472
|
+
messageQueries.create(id, { role: "user", content: prompt });
|
|
3081
3473
|
const streamId = `stream_${id}_${nanoid4(10)}`;
|
|
3082
3474
|
activeStreamQueries.create(id, streamId);
|
|
3083
3475
|
const stream = await streamContext.resumableStream(
|
|
@@ -3276,6 +3668,7 @@ agents.post(
|
|
|
3276
3668
|
});
|
|
3277
3669
|
const session = agent.getSession();
|
|
3278
3670
|
const streamId = `stream_${session.id}_${nanoid4(10)}`;
|
|
3671
|
+
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
3279
3672
|
activeStreamQueries.create(session.id, streamId);
|
|
3280
3673
|
const createQuickStreamProducer = () => {
|
|
3281
3674
|
const { readable, writable } = new TransformStream();
|
|
@@ -3456,10 +3849,14 @@ import { z as z10 } from "zod";
|
|
|
3456
3849
|
var health = new Hono3();
|
|
3457
3850
|
health.get("/", async (c) => {
|
|
3458
3851
|
const config = getConfig();
|
|
3852
|
+
const apiKeyStatus = getApiKeyStatus();
|
|
3853
|
+
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
3854
|
+
const hasApiKey = gatewayKey?.configured ?? false;
|
|
3459
3855
|
return c.json({
|
|
3460
3856
|
status: "ok",
|
|
3461
3857
|
version: "0.1.0",
|
|
3462
3858
|
uptime: process.uptime(),
|
|
3859
|
+
apiKeyConfigured: hasApiKey,
|
|
3463
3860
|
config: {
|
|
3464
3861
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
3465
3862
|
defaultModel: config.defaultModel,
|
|
@@ -3536,7 +3933,6 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
3536
3933
|
import { Hono as Hono4 } from "hono";
|
|
3537
3934
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3538
3935
|
import { z as z11 } from "zod";
|
|
3539
|
-
init_db();
|
|
3540
3936
|
var terminals2 = new Hono4();
|
|
3541
3937
|
var spawnSchema = z11.object({
|
|
3542
3938
|
command: z11.string(),
|
|
@@ -3834,14 +4230,11 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
3834
4230
|
);
|
|
3835
4231
|
});
|
|
3836
4232
|
|
|
3837
|
-
// src/server/index.ts
|
|
3838
|
-
init_db();
|
|
3839
|
-
|
|
3840
4233
|
// src/utils/dependencies.ts
|
|
3841
|
-
import { exec as
|
|
3842
|
-
import { promisify as
|
|
4234
|
+
import { exec as exec4 } from "child_process";
|
|
4235
|
+
import { promisify as promisify4 } from "util";
|
|
3843
4236
|
import { platform as platform2 } from "os";
|
|
3844
|
-
var
|
|
4237
|
+
var execAsync4 = promisify4(exec4);
|
|
3845
4238
|
function getInstallInstructions() {
|
|
3846
4239
|
const os2 = platform2();
|
|
3847
4240
|
if (os2 === "darwin") {
|
|
@@ -3874,7 +4267,7 @@ Install tmux:
|
|
|
3874
4267
|
}
|
|
3875
4268
|
async function checkTmux() {
|
|
3876
4269
|
try {
|
|
3877
|
-
const { stdout } = await
|
|
4270
|
+
const { stdout } = await execAsync4("tmux -V", { timeout: 5e3 });
|
|
3878
4271
|
const version = stdout.trim();
|
|
3879
4272
|
return {
|
|
3880
4273
|
available: true,
|
|
@@ -3921,13 +4314,13 @@ var DEFAULT_WEB_PORT = 6969;
|
|
|
3921
4314
|
var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
|
|
3922
4315
|
function getWebDirectory() {
|
|
3923
4316
|
try {
|
|
3924
|
-
const currentDir =
|
|
3925
|
-
const webDir =
|
|
3926
|
-
if (
|
|
4317
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
4318
|
+
const webDir = resolve6(currentDir, "..", "web");
|
|
4319
|
+
if (existsSync7(webDir) && existsSync7(join3(webDir, "package.json"))) {
|
|
3927
4320
|
return webDir;
|
|
3928
4321
|
}
|
|
3929
|
-
const altWebDir =
|
|
3930
|
-
if (
|
|
4322
|
+
const altWebDir = resolve6(currentDir, "..", "..", "web");
|
|
4323
|
+
if (existsSync7(altWebDir) && existsSync7(join3(altWebDir, "package.json"))) {
|
|
3931
4324
|
return altWebDir;
|
|
3932
4325
|
}
|
|
3933
4326
|
return null;
|
|
@@ -3950,18 +4343,18 @@ async function isSparkcoderWebRunning(port) {
|
|
|
3950
4343
|
}
|
|
3951
4344
|
}
|
|
3952
4345
|
function isPortInUse(port) {
|
|
3953
|
-
return new Promise((
|
|
4346
|
+
return new Promise((resolve7) => {
|
|
3954
4347
|
const server = createNetServer();
|
|
3955
4348
|
server.once("error", (err) => {
|
|
3956
4349
|
if (err.code === "EADDRINUSE") {
|
|
3957
|
-
|
|
4350
|
+
resolve7(true);
|
|
3958
4351
|
} else {
|
|
3959
|
-
|
|
4352
|
+
resolve7(false);
|
|
3960
4353
|
}
|
|
3961
4354
|
});
|
|
3962
4355
|
server.once("listening", () => {
|
|
3963
4356
|
server.close();
|
|
3964
|
-
|
|
4357
|
+
resolve7(false);
|
|
3965
4358
|
});
|
|
3966
4359
|
server.listen(port, "0.0.0.0");
|
|
3967
4360
|
});
|
|
@@ -3995,7 +4388,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
|
|
|
3995
4388
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
3996
4389
|
return { process: null, port: actualPort };
|
|
3997
4390
|
}
|
|
3998
|
-
const useNpm =
|
|
4391
|
+
const useNpm = existsSync7(join3(webDir, "package-lock.json"));
|
|
3999
4392
|
const command = useNpm ? "npm" : "npx";
|
|
4000
4393
|
const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
|
|
4001
4394
|
const child = spawn(command, args, {
|
|
@@ -4098,7 +4491,7 @@ async function startServer(options = {}) {
|
|
|
4098
4491
|
if (options.workingDirectory) {
|
|
4099
4492
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
4100
4493
|
}
|
|
4101
|
-
if (!
|
|
4494
|
+
if (!existsSync7(config.resolvedWorkingDirectory)) {
|
|
4102
4495
|
mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
|
|
4103
4496
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
4104
4497
|
}
|