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/cli.js
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
3
|
var __export = (target, all) => {
|
|
8
4
|
for (var name in all)
|
|
9
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
6
|
};
|
|
11
7
|
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
import "dotenv/config";
|
|
13
|
+
import { createInterface } from "readline";
|
|
14
|
+
|
|
15
|
+
// src/server/index.ts
|
|
16
|
+
import "dotenv/config";
|
|
17
|
+
import { Hono as Hono5 } from "hono";
|
|
18
|
+
import { serve } from "@hono/node-server";
|
|
19
|
+
import { cors } from "hono/cors";
|
|
20
|
+
import { logger } from "hono/logger";
|
|
21
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
22
|
+
import { resolve as resolve6, dirname as dirname4, join as join3 } from "path";
|
|
23
|
+
import { spawn } from "child_process";
|
|
24
|
+
import { createServer as createNetServer } from "net";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
|
|
27
|
+
// src/server/routes/sessions.ts
|
|
28
|
+
import { Hono } from "hono";
|
|
29
|
+
import { zValidator } from "@hono/zod-validator";
|
|
30
|
+
import { z as z8 } from "zod";
|
|
31
|
+
|
|
32
|
+
// src/db/index.ts
|
|
33
|
+
import Database from "better-sqlite3";
|
|
34
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
35
|
+
import { eq, desc, and, sql } from "drizzle-orm";
|
|
36
|
+
import { nanoid } from "nanoid";
|
|
37
|
+
|
|
12
38
|
// src/db/schema.ts
|
|
13
39
|
var schema_exports = {};
|
|
14
40
|
__export(schema_exports, {
|
|
15
41
|
activeStreams: () => activeStreams,
|
|
42
|
+
checkpoints: () => checkpoints,
|
|
43
|
+
fileBackups: () => fileBackups,
|
|
16
44
|
loadedSkills: () => loadedSkills,
|
|
17
45
|
messages: () => messages,
|
|
18
46
|
sessions: () => sessions,
|
|
@@ -21,107 +49,108 @@ __export(schema_exports, {
|
|
|
21
49
|
toolExecutions: () => toolExecutions
|
|
22
50
|
});
|
|
23
51
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
24
|
-
var sessions
|
|
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
|
-
|
|
104
|
-
|
|
52
|
+
var sessions = sqliteTable("sessions", {
|
|
53
|
+
id: text("id").primaryKey(),
|
|
54
|
+
name: text("name"),
|
|
55
|
+
workingDirectory: text("working_directory").notNull(),
|
|
56
|
+
model: text("model").notNull(),
|
|
57
|
+
status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
|
|
58
|
+
config: text("config", { mode: "json" }).$type(),
|
|
59
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
60
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
61
|
+
});
|
|
62
|
+
var messages = sqliteTable("messages", {
|
|
63
|
+
id: text("id").primaryKey(),
|
|
64
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
65
|
+
// Store the entire ModelMessage as JSON (role + content)
|
|
66
|
+
modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
|
|
67
|
+
// Sequence number within session to maintain exact ordering
|
|
68
|
+
sequence: integer("sequence").notNull().default(0),
|
|
69
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
70
|
+
});
|
|
71
|
+
var toolExecutions = sqliteTable("tool_executions", {
|
|
72
|
+
id: text("id").primaryKey(),
|
|
73
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
74
|
+
messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
|
|
75
|
+
toolName: text("tool_name").notNull(),
|
|
76
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
77
|
+
input: text("input", { mode: "json" }),
|
|
78
|
+
output: text("output", { mode: "json" }),
|
|
79
|
+
status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
|
|
80
|
+
requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
|
|
81
|
+
error: text("error"),
|
|
82
|
+
startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
83
|
+
completedAt: integer("completed_at", { mode: "timestamp" })
|
|
84
|
+
});
|
|
85
|
+
var todoItems = sqliteTable("todo_items", {
|
|
86
|
+
id: text("id").primaryKey(),
|
|
87
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
88
|
+
content: text("content").notNull(),
|
|
89
|
+
status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
|
|
90
|
+
order: integer("order").notNull().default(0),
|
|
91
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
92
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
93
|
+
});
|
|
94
|
+
var loadedSkills = sqliteTable("loaded_skills", {
|
|
95
|
+
id: text("id").primaryKey(),
|
|
96
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
97
|
+
skillName: text("skill_name").notNull(),
|
|
98
|
+
loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
99
|
+
});
|
|
100
|
+
var terminals = sqliteTable("terminals", {
|
|
101
|
+
id: text("id").primaryKey(),
|
|
102
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
103
|
+
name: text("name"),
|
|
104
|
+
// Optional friendly name (e.g., "dev-server")
|
|
105
|
+
command: text("command").notNull(),
|
|
106
|
+
// The command that was run
|
|
107
|
+
cwd: text("cwd").notNull(),
|
|
108
|
+
// Working directory
|
|
109
|
+
pid: integer("pid"),
|
|
110
|
+
// Process ID (null if not running)
|
|
111
|
+
status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
|
|
112
|
+
exitCode: integer("exit_code"),
|
|
113
|
+
// Exit code if stopped
|
|
114
|
+
error: text("error"),
|
|
115
|
+
// Error message if status is 'error'
|
|
116
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
117
|
+
stoppedAt: integer("stopped_at", { mode: "timestamp" })
|
|
118
|
+
});
|
|
119
|
+
var activeStreams = sqliteTable("active_streams", {
|
|
120
|
+
id: text("id").primaryKey(),
|
|
121
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
122
|
+
streamId: text("stream_id").notNull().unique(),
|
|
123
|
+
// Unique stream identifier
|
|
124
|
+
status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
|
|
125
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
126
|
+
finishedAt: integer("finished_at", { mode: "timestamp" })
|
|
127
|
+
});
|
|
128
|
+
var checkpoints = sqliteTable("checkpoints", {
|
|
129
|
+
id: text("id").primaryKey(),
|
|
130
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
131
|
+
// The message sequence number this checkpoint was created BEFORE
|
|
132
|
+
// (i.e., the state before this user message was processed)
|
|
133
|
+
messageSequence: integer("message_sequence").notNull(),
|
|
134
|
+
// Optional git commit hash if in a git repo
|
|
135
|
+
gitHead: text("git_head"),
|
|
136
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
137
|
+
});
|
|
138
|
+
var fileBackups = sqliteTable("file_backups", {
|
|
139
|
+
id: text("id").primaryKey(),
|
|
140
|
+
checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
|
|
141
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
142
|
+
// Relative path from working directory
|
|
143
|
+
filePath: text("file_path").notNull(),
|
|
144
|
+
// Original content (null means file didn't exist before)
|
|
145
|
+
originalContent: text("original_content"),
|
|
146
|
+
// Whether the file existed before this checkpoint
|
|
147
|
+
existed: integer("existed", { mode: "boolean" }).notNull().default(true),
|
|
148
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
105
149
|
});
|
|
106
150
|
|
|
107
151
|
// src/db/index.ts
|
|
108
|
-
var
|
|
109
|
-
|
|
110
|
-
activeStreamQueries: () => activeStreamQueries,
|
|
111
|
-
closeDatabase: () => closeDatabase,
|
|
112
|
-
getDb: () => getDb,
|
|
113
|
-
initDatabase: () => initDatabase,
|
|
114
|
-
messageQueries: () => messageQueries,
|
|
115
|
-
sessionQueries: () => sessionQueries,
|
|
116
|
-
skillQueries: () => skillQueries,
|
|
117
|
-
terminalQueries: () => terminalQueries,
|
|
118
|
-
todoQueries: () => todoQueries,
|
|
119
|
-
toolExecutionQueries: () => toolExecutionQueries
|
|
120
|
-
});
|
|
121
|
-
import Database from "better-sqlite3";
|
|
122
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
123
|
-
import { eq, desc, and, sql } from "drizzle-orm";
|
|
124
|
-
import { nanoid } from "nanoid";
|
|
152
|
+
var db = null;
|
|
153
|
+
var sqlite = null;
|
|
125
154
|
function initDatabase(dbPath) {
|
|
126
155
|
sqlite = new Database(dbPath);
|
|
127
156
|
sqlite.pragma("journal_mode = WAL");
|
|
@@ -202,12 +231,35 @@ function initDatabase(dbPath) {
|
|
|
202
231
|
finished_at INTEGER
|
|
203
232
|
);
|
|
204
233
|
|
|
234
|
+
-- Checkpoints table - created before each user message
|
|
235
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
236
|
+
id TEXT PRIMARY KEY,
|
|
237
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
238
|
+
message_sequence INTEGER NOT NULL,
|
|
239
|
+
git_head TEXT,
|
|
240
|
+
created_at INTEGER NOT NULL
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
-- File backups table - stores original file content
|
|
244
|
+
CREATE TABLE IF NOT EXISTS file_backups (
|
|
245
|
+
id TEXT PRIMARY KEY,
|
|
246
|
+
checkpoint_id TEXT NOT NULL REFERENCES checkpoints(id) ON DELETE CASCADE,
|
|
247
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
248
|
+
file_path TEXT NOT NULL,
|
|
249
|
+
original_content TEXT,
|
|
250
|
+
existed INTEGER NOT NULL DEFAULT 1,
|
|
251
|
+
created_at INTEGER NOT NULL
|
|
252
|
+
);
|
|
253
|
+
|
|
205
254
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
206
255
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
|
|
207
256
|
CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
|
|
208
257
|
CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
|
|
209
258
|
CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
|
|
210
259
|
CREATE INDEX IF NOT EXISTS idx_active_streams_session ON active_streams(session_id);
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_file_backups_checkpoint ON file_backups(checkpoint_id);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_file_backups_session ON file_backups(session_id);
|
|
211
263
|
`);
|
|
212
264
|
return db;
|
|
213
265
|
}
|
|
@@ -224,328 +276,385 @@ function closeDatabase() {
|
|
|
224
276
|
db = null;
|
|
225
277
|
}
|
|
226
278
|
}
|
|
227
|
-
var
|
|
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
|
-
},
|
|
258
|
-
update(id, updates) {
|
|
259
|
-
return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
260
|
-
},
|
|
261
|
-
delete(id) {
|
|
262
|
-
const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
|
|
263
|
-
return result.changes > 0;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
messageQueries = {
|
|
267
|
-
/**
|
|
268
|
-
* Get the next sequence number for a session
|
|
269
|
-
*/
|
|
270
|
-
getNextSequence(sessionId) {
|
|
271
|
-
const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
272
|
-
return (result?.maxSeq ?? -1) + 1;
|
|
273
|
-
},
|
|
274
|
-
/**
|
|
275
|
-
* Create a single message from a ModelMessage
|
|
276
|
-
*/
|
|
277
|
-
create(sessionId, modelMessage) {
|
|
278
|
-
const id = nanoid();
|
|
279
|
-
const sequence = this.getNextSequence(sessionId);
|
|
280
|
-
const result = getDb().insert(messages).values({
|
|
281
|
-
id,
|
|
282
|
-
sessionId,
|
|
283
|
-
modelMessage,
|
|
284
|
-
sequence,
|
|
285
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
286
|
-
}).returning().get();
|
|
287
|
-
return result;
|
|
288
|
-
},
|
|
289
|
-
/**
|
|
290
|
-
* Add multiple ModelMessages at once (from response.messages)
|
|
291
|
-
* Maintains insertion order via sequence numbers
|
|
292
|
-
*/
|
|
293
|
-
addMany(sessionId, modelMessages) {
|
|
294
|
-
const results = [];
|
|
295
|
-
let sequence = this.getNextSequence(sessionId);
|
|
296
|
-
for (const msg of modelMessages) {
|
|
297
|
-
const id = nanoid();
|
|
298
|
-
const result = getDb().insert(messages).values({
|
|
299
|
-
id,
|
|
300
|
-
sessionId,
|
|
301
|
-
modelMessage: msg,
|
|
302
|
-
sequence,
|
|
303
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
304
|
-
}).returning().get();
|
|
305
|
-
results.push(result);
|
|
306
|
-
sequence++;
|
|
307
|
-
}
|
|
308
|
-
return results;
|
|
309
|
-
},
|
|
310
|
-
/**
|
|
311
|
-
* Get all messages for a session as ModelMessage[]
|
|
312
|
-
* Ordered by sequence to maintain exact insertion order
|
|
313
|
-
*/
|
|
314
|
-
getBySession(sessionId) {
|
|
315
|
-
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
|
|
316
|
-
},
|
|
317
|
-
/**
|
|
318
|
-
* Get ModelMessages directly (for passing to AI SDK)
|
|
319
|
-
*/
|
|
320
|
-
getModelMessages(sessionId) {
|
|
321
|
-
const messages2 = this.getBySession(sessionId);
|
|
322
|
-
return messages2.map((m) => m.modelMessage);
|
|
323
|
-
},
|
|
324
|
-
getRecentBySession(sessionId, limit = 50) {
|
|
325
|
-
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
|
|
326
|
-
},
|
|
327
|
-
countBySession(sessionId) {
|
|
328
|
-
const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
329
|
-
return result?.count ?? 0;
|
|
330
|
-
},
|
|
331
|
-
deleteBySession(sessionId) {
|
|
332
|
-
const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
|
|
333
|
-
return result.changes;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
toolExecutionQueries = {
|
|
337
|
-
create(data) {
|
|
338
|
-
const id = nanoid();
|
|
339
|
-
const result = getDb().insert(toolExecutions).values({
|
|
340
|
-
id,
|
|
341
|
-
...data,
|
|
342
|
-
startedAt: /* @__PURE__ */ new Date()
|
|
343
|
-
}).returning().get();
|
|
344
|
-
return result;
|
|
345
|
-
},
|
|
346
|
-
getById(id) {
|
|
347
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
|
|
348
|
-
},
|
|
349
|
-
getByToolCallId(toolCallId) {
|
|
350
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
|
|
351
|
-
},
|
|
352
|
-
getPendingApprovals(sessionId) {
|
|
353
|
-
return getDb().select().from(toolExecutions).where(
|
|
354
|
-
and(
|
|
355
|
-
eq(toolExecutions.sessionId, sessionId),
|
|
356
|
-
eq(toolExecutions.status, "pending"),
|
|
357
|
-
eq(toolExecutions.requiresApproval, true)
|
|
358
|
-
)
|
|
359
|
-
).all();
|
|
360
|
-
},
|
|
361
|
-
approve(id) {
|
|
362
|
-
return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
363
|
-
},
|
|
364
|
-
reject(id) {
|
|
365
|
-
return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
366
|
-
},
|
|
367
|
-
complete(id, output, error) {
|
|
368
|
-
return getDb().update(toolExecutions).set({
|
|
369
|
-
status: error ? "error" : "completed",
|
|
370
|
-
output,
|
|
371
|
-
error,
|
|
372
|
-
completedAt: /* @__PURE__ */ new Date()
|
|
373
|
-
}).where(eq(toolExecutions.id, id)).returning().get();
|
|
374
|
-
},
|
|
375
|
-
getBySession(sessionId) {
|
|
376
|
-
return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
todoQueries = {
|
|
380
|
-
create(data) {
|
|
381
|
-
const id = nanoid();
|
|
382
|
-
const now = /* @__PURE__ */ new Date();
|
|
383
|
-
const result = getDb().insert(todoItems).values({
|
|
384
|
-
id,
|
|
385
|
-
...data,
|
|
386
|
-
createdAt: now,
|
|
387
|
-
updatedAt: now
|
|
388
|
-
}).returning().get();
|
|
389
|
-
return result;
|
|
390
|
-
},
|
|
391
|
-
createMany(sessionId, items) {
|
|
392
|
-
const now = /* @__PURE__ */ new Date();
|
|
393
|
-
const values = items.map((item, index) => ({
|
|
394
|
-
id: nanoid(),
|
|
395
|
-
sessionId,
|
|
396
|
-
content: item.content,
|
|
397
|
-
order: item.order ?? index,
|
|
398
|
-
createdAt: now,
|
|
399
|
-
updatedAt: now
|
|
400
|
-
}));
|
|
401
|
-
return getDb().insert(todoItems).values(values).returning().all();
|
|
402
|
-
},
|
|
403
|
-
getBySession(sessionId) {
|
|
404
|
-
return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
|
|
405
|
-
},
|
|
406
|
-
updateStatus(id, status) {
|
|
407
|
-
return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
|
|
408
|
-
},
|
|
409
|
-
delete(id) {
|
|
410
|
-
const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
|
|
411
|
-
return result.changes > 0;
|
|
412
|
-
},
|
|
413
|
-
clearSession(sessionId) {
|
|
414
|
-
const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
|
|
415
|
-
return result.changes;
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
skillQueries = {
|
|
419
|
-
load(sessionId, skillName) {
|
|
420
|
-
const id = nanoid();
|
|
421
|
-
const result = getDb().insert(loadedSkills).values({
|
|
422
|
-
id,
|
|
423
|
-
sessionId,
|
|
424
|
-
skillName,
|
|
425
|
-
loadedAt: /* @__PURE__ */ new Date()
|
|
426
|
-
}).returning().get();
|
|
427
|
-
return result;
|
|
428
|
-
},
|
|
429
|
-
getBySession(sessionId) {
|
|
430
|
-
return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
|
|
431
|
-
},
|
|
432
|
-
isLoaded(sessionId, skillName) {
|
|
433
|
-
const result = getDb().select().from(loadedSkills).where(
|
|
434
|
-
and(
|
|
435
|
-
eq(loadedSkills.sessionId, sessionId),
|
|
436
|
-
eq(loadedSkills.skillName, skillName)
|
|
437
|
-
)
|
|
438
|
-
).get();
|
|
439
|
-
return !!result;
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
terminalQueries = {
|
|
443
|
-
create(data) {
|
|
444
|
-
const id = nanoid();
|
|
445
|
-
const result = getDb().insert(terminals).values({
|
|
446
|
-
id,
|
|
447
|
-
...data,
|
|
448
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
449
|
-
}).returning().get();
|
|
450
|
-
return result;
|
|
451
|
-
},
|
|
452
|
-
getById(id) {
|
|
453
|
-
return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
|
|
454
|
-
},
|
|
455
|
-
getBySession(sessionId) {
|
|
456
|
-
return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
|
|
457
|
-
},
|
|
458
|
-
getRunning(sessionId) {
|
|
459
|
-
return getDb().select().from(terminals).where(
|
|
460
|
-
and(
|
|
461
|
-
eq(terminals.sessionId, sessionId),
|
|
462
|
-
eq(terminals.status, "running")
|
|
463
|
-
)
|
|
464
|
-
).all();
|
|
465
|
-
},
|
|
466
|
-
updateStatus(id, status, exitCode, error) {
|
|
467
|
-
return getDb().update(terminals).set({
|
|
468
|
-
status,
|
|
469
|
-
exitCode,
|
|
470
|
-
error,
|
|
471
|
-
stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
|
|
472
|
-
}).where(eq(terminals.id, id)).returning().get();
|
|
473
|
-
},
|
|
474
|
-
updatePid(id, pid) {
|
|
475
|
-
return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
|
|
476
|
-
},
|
|
477
|
-
delete(id) {
|
|
478
|
-
const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
|
|
479
|
-
return result.changes > 0;
|
|
480
|
-
},
|
|
481
|
-
deleteBySession(sessionId) {
|
|
482
|
-
const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
|
|
483
|
-
return result.changes;
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
activeStreamQueries = {
|
|
487
|
-
create(sessionId, streamId) {
|
|
488
|
-
const id = nanoid();
|
|
489
|
-
const result = getDb().insert(activeStreams).values({
|
|
490
|
-
id,
|
|
491
|
-
sessionId,
|
|
492
|
-
streamId,
|
|
493
|
-
status: "active",
|
|
494
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
495
|
-
}).returning().get();
|
|
496
|
-
return result;
|
|
497
|
-
},
|
|
498
|
-
getBySessionId(sessionId) {
|
|
499
|
-
return getDb().select().from(activeStreams).where(
|
|
500
|
-
and(
|
|
501
|
-
eq(activeStreams.sessionId, sessionId),
|
|
502
|
-
eq(activeStreams.status, "active")
|
|
503
|
-
)
|
|
504
|
-
).get();
|
|
505
|
-
},
|
|
506
|
-
getByStreamId(streamId) {
|
|
507
|
-
return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
|
|
508
|
-
},
|
|
509
|
-
finish(streamId) {
|
|
510
|
-
return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
511
|
-
},
|
|
512
|
-
markError(streamId) {
|
|
513
|
-
return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
514
|
-
},
|
|
515
|
-
deleteBySession(sessionId) {
|
|
516
|
-
const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
|
|
517
|
-
return result.changes;
|
|
518
|
-
}
|
|
519
|
-
};
|
|
279
|
+
var sessionQueries = {
|
|
280
|
+
create(data) {
|
|
281
|
+
const id = nanoid();
|
|
282
|
+
const now = /* @__PURE__ */ new Date();
|
|
283
|
+
const result = getDb().insert(sessions).values({
|
|
284
|
+
id,
|
|
285
|
+
...data,
|
|
286
|
+
createdAt: now,
|
|
287
|
+
updatedAt: now
|
|
288
|
+
}).returning().get();
|
|
289
|
+
return result;
|
|
290
|
+
},
|
|
291
|
+
getById(id) {
|
|
292
|
+
return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
|
|
293
|
+
},
|
|
294
|
+
list(limit = 50, offset = 0) {
|
|
295
|
+
return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
|
|
296
|
+
},
|
|
297
|
+
updateStatus(id, status) {
|
|
298
|
+
return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
299
|
+
},
|
|
300
|
+
updateModel(id, model) {
|
|
301
|
+
return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
302
|
+
},
|
|
303
|
+
update(id, updates) {
|
|
304
|
+
return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
305
|
+
},
|
|
306
|
+
delete(id) {
|
|
307
|
+
const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
|
|
308
|
+
return result.changes > 0;
|
|
520
309
|
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
310
|
+
};
|
|
311
|
+
var messageQueries = {
|
|
312
|
+
/**
|
|
313
|
+
* Get the next sequence number for a session
|
|
314
|
+
*/
|
|
315
|
+
getNextSequence(sessionId) {
|
|
316
|
+
const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
317
|
+
return (result?.maxSeq ?? -1) + 1;
|
|
318
|
+
},
|
|
319
|
+
/**
|
|
320
|
+
* Create a single message from a ModelMessage
|
|
321
|
+
*/
|
|
322
|
+
create(sessionId, modelMessage) {
|
|
323
|
+
const id = nanoid();
|
|
324
|
+
const sequence = this.getNextSequence(sessionId);
|
|
325
|
+
const result = getDb().insert(messages).values({
|
|
326
|
+
id,
|
|
327
|
+
sessionId,
|
|
328
|
+
modelMessage,
|
|
329
|
+
sequence,
|
|
330
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
331
|
+
}).returning().get();
|
|
332
|
+
return result;
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* Add multiple ModelMessages at once (from response.messages)
|
|
336
|
+
* Maintains insertion order via sequence numbers
|
|
337
|
+
*/
|
|
338
|
+
addMany(sessionId, modelMessages) {
|
|
339
|
+
const results = [];
|
|
340
|
+
let sequence = this.getNextSequence(sessionId);
|
|
341
|
+
for (const msg of modelMessages) {
|
|
342
|
+
const id = nanoid();
|
|
343
|
+
const result = getDb().insert(messages).values({
|
|
344
|
+
id,
|
|
345
|
+
sessionId,
|
|
346
|
+
modelMessage: msg,
|
|
347
|
+
sequence,
|
|
348
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
349
|
+
}).returning().get();
|
|
350
|
+
results.push(result);
|
|
351
|
+
sequence++;
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
},
|
|
355
|
+
/**
|
|
356
|
+
* Get all messages for a session as ModelMessage[]
|
|
357
|
+
* Ordered by sequence to maintain exact insertion order
|
|
358
|
+
*/
|
|
359
|
+
getBySession(sessionId) {
|
|
360
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
|
|
361
|
+
},
|
|
362
|
+
/**
|
|
363
|
+
* Get ModelMessages directly (for passing to AI SDK)
|
|
364
|
+
*/
|
|
365
|
+
getModelMessages(sessionId) {
|
|
366
|
+
const messages2 = this.getBySession(sessionId);
|
|
367
|
+
return messages2.map((m) => m.modelMessage);
|
|
368
|
+
},
|
|
369
|
+
getRecentBySession(sessionId, limit = 50) {
|
|
370
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
|
|
371
|
+
},
|
|
372
|
+
countBySession(sessionId) {
|
|
373
|
+
const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
374
|
+
return result?.count ?? 0;
|
|
375
|
+
},
|
|
376
|
+
deleteBySession(sessionId) {
|
|
377
|
+
const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
|
|
378
|
+
return result.changes;
|
|
379
|
+
},
|
|
380
|
+
/**
|
|
381
|
+
* Delete all messages with sequence >= the given sequence number
|
|
382
|
+
* (Used when reverting to a checkpoint)
|
|
383
|
+
*/
|
|
384
|
+
deleteFromSequence(sessionId, fromSequence) {
|
|
385
|
+
const result = getDb().delete(messages).where(
|
|
386
|
+
and(
|
|
387
|
+
eq(messages.sessionId, sessionId),
|
|
388
|
+
sql`sequence >= ${fromSequence}`
|
|
389
|
+
)
|
|
390
|
+
).run();
|
|
391
|
+
return result.changes;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
var toolExecutionQueries = {
|
|
395
|
+
create(data) {
|
|
396
|
+
const id = nanoid();
|
|
397
|
+
const result = getDb().insert(toolExecutions).values({
|
|
398
|
+
id,
|
|
399
|
+
...data,
|
|
400
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
401
|
+
}).returning().get();
|
|
402
|
+
return result;
|
|
403
|
+
},
|
|
404
|
+
getById(id) {
|
|
405
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
|
|
406
|
+
},
|
|
407
|
+
getByToolCallId(toolCallId) {
|
|
408
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
|
|
409
|
+
},
|
|
410
|
+
getPendingApprovals(sessionId) {
|
|
411
|
+
return getDb().select().from(toolExecutions).where(
|
|
412
|
+
and(
|
|
413
|
+
eq(toolExecutions.sessionId, sessionId),
|
|
414
|
+
eq(toolExecutions.status, "pending"),
|
|
415
|
+
eq(toolExecutions.requiresApproval, true)
|
|
416
|
+
)
|
|
417
|
+
).all();
|
|
418
|
+
},
|
|
419
|
+
approve(id) {
|
|
420
|
+
return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
421
|
+
},
|
|
422
|
+
reject(id) {
|
|
423
|
+
return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
424
|
+
},
|
|
425
|
+
complete(id, output, error) {
|
|
426
|
+
return getDb().update(toolExecutions).set({
|
|
427
|
+
status: error ? "error" : "completed",
|
|
428
|
+
output,
|
|
429
|
+
error,
|
|
430
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
431
|
+
}).where(eq(toolExecutions.id, id)).returning().get();
|
|
432
|
+
},
|
|
433
|
+
getBySession(sessionId) {
|
|
434
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Delete all tool executions after a given timestamp
|
|
438
|
+
* (Used when reverting to a checkpoint)
|
|
439
|
+
*/
|
|
440
|
+
deleteAfterTime(sessionId, afterTime) {
|
|
441
|
+
const result = getDb().delete(toolExecutions).where(
|
|
442
|
+
and(
|
|
443
|
+
eq(toolExecutions.sessionId, sessionId),
|
|
444
|
+
sql`started_at > ${afterTime.getTime()}`
|
|
445
|
+
)
|
|
446
|
+
).run();
|
|
447
|
+
return result.changes;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
var todoQueries = {
|
|
451
|
+
create(data) {
|
|
452
|
+
const id = nanoid();
|
|
453
|
+
const now = /* @__PURE__ */ new Date();
|
|
454
|
+
const result = getDb().insert(todoItems).values({
|
|
455
|
+
id,
|
|
456
|
+
...data,
|
|
457
|
+
createdAt: now,
|
|
458
|
+
updatedAt: now
|
|
459
|
+
}).returning().get();
|
|
460
|
+
return result;
|
|
461
|
+
},
|
|
462
|
+
createMany(sessionId, items) {
|
|
463
|
+
const now = /* @__PURE__ */ new Date();
|
|
464
|
+
const values = items.map((item, index) => ({
|
|
465
|
+
id: nanoid(),
|
|
466
|
+
sessionId,
|
|
467
|
+
content: item.content,
|
|
468
|
+
order: item.order ?? index,
|
|
469
|
+
createdAt: now,
|
|
470
|
+
updatedAt: now
|
|
471
|
+
}));
|
|
472
|
+
return getDb().insert(todoItems).values(values).returning().all();
|
|
473
|
+
},
|
|
474
|
+
getBySession(sessionId) {
|
|
475
|
+
return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
|
|
476
|
+
},
|
|
477
|
+
updateStatus(id, status) {
|
|
478
|
+
return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
|
|
479
|
+
},
|
|
480
|
+
delete(id) {
|
|
481
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
|
|
482
|
+
return result.changes > 0;
|
|
483
|
+
},
|
|
484
|
+
clearSession(sessionId) {
|
|
485
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
|
|
486
|
+
return result.changes;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
var skillQueries = {
|
|
490
|
+
load(sessionId, skillName) {
|
|
491
|
+
const id = nanoid();
|
|
492
|
+
const result = getDb().insert(loadedSkills).values({
|
|
493
|
+
id,
|
|
494
|
+
sessionId,
|
|
495
|
+
skillName,
|
|
496
|
+
loadedAt: /* @__PURE__ */ new Date()
|
|
497
|
+
}).returning().get();
|
|
498
|
+
return result;
|
|
499
|
+
},
|
|
500
|
+
getBySession(sessionId) {
|
|
501
|
+
return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
|
|
502
|
+
},
|
|
503
|
+
isLoaded(sessionId, skillName) {
|
|
504
|
+
const result = getDb().select().from(loadedSkills).where(
|
|
505
|
+
and(
|
|
506
|
+
eq(loadedSkills.sessionId, sessionId),
|
|
507
|
+
eq(loadedSkills.skillName, skillName)
|
|
508
|
+
)
|
|
509
|
+
).get();
|
|
510
|
+
return !!result;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var activeStreamQueries = {
|
|
514
|
+
create(sessionId, streamId) {
|
|
515
|
+
const id = nanoid();
|
|
516
|
+
const result = getDb().insert(activeStreams).values({
|
|
517
|
+
id,
|
|
518
|
+
sessionId,
|
|
519
|
+
streamId,
|
|
520
|
+
status: "active",
|
|
521
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
522
|
+
}).returning().get();
|
|
523
|
+
return result;
|
|
524
|
+
},
|
|
525
|
+
getBySessionId(sessionId) {
|
|
526
|
+
return getDb().select().from(activeStreams).where(
|
|
527
|
+
and(
|
|
528
|
+
eq(activeStreams.sessionId, sessionId),
|
|
529
|
+
eq(activeStreams.status, "active")
|
|
530
|
+
)
|
|
531
|
+
).get();
|
|
532
|
+
},
|
|
533
|
+
getByStreamId(streamId) {
|
|
534
|
+
return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
|
|
535
|
+
},
|
|
536
|
+
finish(streamId) {
|
|
537
|
+
return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
538
|
+
},
|
|
539
|
+
markError(streamId) {
|
|
540
|
+
return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
|
|
541
|
+
},
|
|
542
|
+
deleteBySession(sessionId) {
|
|
543
|
+
const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
|
|
544
|
+
return result.changes;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
var checkpointQueries = {
|
|
548
|
+
create(data) {
|
|
549
|
+
const id = nanoid();
|
|
550
|
+
const result = getDb().insert(checkpoints).values({
|
|
551
|
+
id,
|
|
552
|
+
sessionId: data.sessionId,
|
|
553
|
+
messageSequence: data.messageSequence,
|
|
554
|
+
gitHead: data.gitHead,
|
|
555
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
556
|
+
}).returning().get();
|
|
557
|
+
return result;
|
|
558
|
+
},
|
|
559
|
+
getById(id) {
|
|
560
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.id, id)).get();
|
|
561
|
+
},
|
|
562
|
+
getBySession(sessionId) {
|
|
563
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(checkpoints.messageSequence).all();
|
|
564
|
+
},
|
|
565
|
+
getByMessageSequence(sessionId, messageSequence) {
|
|
566
|
+
return getDb().select().from(checkpoints).where(
|
|
567
|
+
and(
|
|
568
|
+
eq(checkpoints.sessionId, sessionId),
|
|
569
|
+
eq(checkpoints.messageSequence, messageSequence)
|
|
570
|
+
)
|
|
571
|
+
).get();
|
|
572
|
+
},
|
|
573
|
+
getLatest(sessionId) {
|
|
574
|
+
return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(desc(checkpoints.messageSequence)).limit(1).get();
|
|
575
|
+
},
|
|
576
|
+
/**
|
|
577
|
+
* Delete all checkpoints after a given sequence number
|
|
578
|
+
* (Used when reverting to a checkpoint)
|
|
579
|
+
*/
|
|
580
|
+
deleteAfterSequence(sessionId, messageSequence) {
|
|
581
|
+
const result = getDb().delete(checkpoints).where(
|
|
582
|
+
and(
|
|
583
|
+
eq(checkpoints.sessionId, sessionId),
|
|
584
|
+
sql`message_sequence > ${messageSequence}`
|
|
585
|
+
)
|
|
586
|
+
).run();
|
|
587
|
+
return result.changes;
|
|
588
|
+
},
|
|
589
|
+
deleteBySession(sessionId) {
|
|
590
|
+
const result = getDb().delete(checkpoints).where(eq(checkpoints.sessionId, sessionId)).run();
|
|
591
|
+
return result.changes;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var fileBackupQueries = {
|
|
595
|
+
create(data) {
|
|
596
|
+
const id = nanoid();
|
|
597
|
+
const result = getDb().insert(fileBackups).values({
|
|
598
|
+
id,
|
|
599
|
+
checkpointId: data.checkpointId,
|
|
600
|
+
sessionId: data.sessionId,
|
|
601
|
+
filePath: data.filePath,
|
|
602
|
+
originalContent: data.originalContent,
|
|
603
|
+
existed: data.existed,
|
|
604
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
605
|
+
}).returning().get();
|
|
606
|
+
return result;
|
|
607
|
+
},
|
|
608
|
+
getByCheckpoint(checkpointId) {
|
|
609
|
+
return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
|
|
610
|
+
},
|
|
611
|
+
getBySession(sessionId) {
|
|
612
|
+
return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
|
|
613
|
+
},
|
|
614
|
+
/**
|
|
615
|
+
* Get all file backups from a given checkpoint sequence onwards (inclusive)
|
|
616
|
+
* (Used when reverting - need to restore these files)
|
|
617
|
+
*
|
|
618
|
+
* When reverting to checkpoint X, we need backups from checkpoint X and all later ones
|
|
619
|
+
* because checkpoint X's backups represent the state BEFORE processing message X.
|
|
620
|
+
*/
|
|
621
|
+
getFromSequence(sessionId, messageSequence) {
|
|
622
|
+
const checkpointsFrom = getDb().select().from(checkpoints).where(
|
|
623
|
+
and(
|
|
624
|
+
eq(checkpoints.sessionId, sessionId),
|
|
625
|
+
sql`message_sequence >= ${messageSequence}`
|
|
626
|
+
)
|
|
627
|
+
).all();
|
|
628
|
+
if (checkpointsFrom.length === 0) {
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
const checkpointIds = checkpointsFrom.map((c) => c.id);
|
|
632
|
+
const allBackups = [];
|
|
633
|
+
for (const cpId of checkpointIds) {
|
|
634
|
+
const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
|
|
635
|
+
allBackups.push(...backups);
|
|
636
|
+
}
|
|
637
|
+
return allBackups;
|
|
638
|
+
},
|
|
639
|
+
/**
|
|
640
|
+
* Check if a file already has a backup in the current checkpoint
|
|
641
|
+
*/
|
|
642
|
+
hasBackup(checkpointId, filePath) {
|
|
643
|
+
const result = getDb().select().from(fileBackups).where(
|
|
644
|
+
and(
|
|
645
|
+
eq(fileBackups.checkpointId, checkpointId),
|
|
646
|
+
eq(fileBackups.filePath, filePath)
|
|
647
|
+
)
|
|
648
|
+
).get();
|
|
649
|
+
return !!result;
|
|
650
|
+
},
|
|
651
|
+
deleteBySession(sessionId) {
|
|
652
|
+
const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
|
|
653
|
+
return result.changes;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
546
656
|
|
|
547
657
|
// src/agent/index.ts
|
|
548
|
-
init_db();
|
|
549
658
|
import {
|
|
550
659
|
streamText,
|
|
551
660
|
generateText as generateText2,
|
|
@@ -918,7 +1027,6 @@ async function isTmuxAvailable() {
|
|
|
918
1027
|
try {
|
|
919
1028
|
const { stdout } = await execAsync("tmux -V");
|
|
920
1029
|
tmuxAvailableCache = true;
|
|
921
|
-
console.log(`[tmux] Available: ${stdout.trim()}`);
|
|
922
1030
|
return true;
|
|
923
1031
|
} catch (error) {
|
|
924
1032
|
tmuxAvailableCache = false;
|
|
@@ -1538,9 +1646,198 @@ Use this to understand existing code, check file contents, or gather context.`,
|
|
|
1538
1646
|
// src/tools/write-file.ts
|
|
1539
1647
|
import { tool as tool3 } from "ai";
|
|
1540
1648
|
import { z as z4 } from "zod";
|
|
1541
|
-
import { readFile as
|
|
1542
|
-
import { resolve as
|
|
1649
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
1650
|
+
import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
|
|
1651
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1652
|
+
|
|
1653
|
+
// src/checkpoints/index.ts
|
|
1654
|
+
import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
|
|
1543
1655
|
import { existsSync as existsSync4 } from "fs";
|
|
1656
|
+
import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
|
|
1657
|
+
import { exec as exec3 } from "child_process";
|
|
1658
|
+
import { promisify as promisify3 } from "util";
|
|
1659
|
+
var execAsync3 = promisify3(exec3);
|
|
1660
|
+
async function getGitHead(workingDirectory) {
|
|
1661
|
+
try {
|
|
1662
|
+
const { stdout } = await execAsync3("git rev-parse HEAD", {
|
|
1663
|
+
cwd: workingDirectory,
|
|
1664
|
+
timeout: 5e3
|
|
1665
|
+
});
|
|
1666
|
+
return stdout.trim();
|
|
1667
|
+
} catch {
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
var activeManagers = /* @__PURE__ */ new Map();
|
|
1672
|
+
function getCheckpointManager(sessionId, workingDirectory) {
|
|
1673
|
+
let manager = activeManagers.get(sessionId);
|
|
1674
|
+
if (!manager) {
|
|
1675
|
+
manager = {
|
|
1676
|
+
sessionId,
|
|
1677
|
+
workingDirectory,
|
|
1678
|
+
currentCheckpointId: null
|
|
1679
|
+
};
|
|
1680
|
+
activeManagers.set(sessionId, manager);
|
|
1681
|
+
}
|
|
1682
|
+
return manager;
|
|
1683
|
+
}
|
|
1684
|
+
async function createCheckpoint(sessionId, workingDirectory, messageSequence) {
|
|
1685
|
+
const gitHead = await getGitHead(workingDirectory);
|
|
1686
|
+
const checkpoint = checkpointQueries.create({
|
|
1687
|
+
sessionId,
|
|
1688
|
+
messageSequence,
|
|
1689
|
+
gitHead
|
|
1690
|
+
});
|
|
1691
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1692
|
+
manager.currentCheckpointId = checkpoint.id;
|
|
1693
|
+
return checkpoint;
|
|
1694
|
+
}
|
|
1695
|
+
async function backupFile(sessionId, workingDirectory, filePath) {
|
|
1696
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1697
|
+
if (!manager.currentCheckpointId) {
|
|
1698
|
+
console.warn("[checkpoint] No active checkpoint, skipping file backup");
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1702
|
+
const relativePath = relative2(workingDirectory, absolutePath);
|
|
1703
|
+
if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
let originalContent = null;
|
|
1707
|
+
let existed = false;
|
|
1708
|
+
if (existsSync4(absolutePath)) {
|
|
1709
|
+
try {
|
|
1710
|
+
originalContent = await readFile3(absolutePath, "utf-8");
|
|
1711
|
+
existed = true;
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const backup = fileBackupQueries.create({
|
|
1717
|
+
checkpointId: manager.currentCheckpointId,
|
|
1718
|
+
sessionId,
|
|
1719
|
+
filePath: relativePath,
|
|
1720
|
+
originalContent,
|
|
1721
|
+
existed
|
|
1722
|
+
});
|
|
1723
|
+
return backup;
|
|
1724
|
+
}
|
|
1725
|
+
async function revertToCheckpoint(sessionId, checkpointId) {
|
|
1726
|
+
const session = sessionQueries.getById(sessionId);
|
|
1727
|
+
if (!session) {
|
|
1728
|
+
return {
|
|
1729
|
+
success: false,
|
|
1730
|
+
filesRestored: 0,
|
|
1731
|
+
filesDeleted: 0,
|
|
1732
|
+
messagesDeleted: 0,
|
|
1733
|
+
checkpointsDeleted: 0,
|
|
1734
|
+
error: "Session not found"
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
const checkpoint = checkpointQueries.getById(checkpointId);
|
|
1738
|
+
if (!checkpoint || checkpoint.sessionId !== sessionId) {
|
|
1739
|
+
return {
|
|
1740
|
+
success: false,
|
|
1741
|
+
filesRestored: 0,
|
|
1742
|
+
filesDeleted: 0,
|
|
1743
|
+
messagesDeleted: 0,
|
|
1744
|
+
checkpointsDeleted: 0,
|
|
1745
|
+
error: "Checkpoint not found"
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
const workingDirectory = session.workingDirectory;
|
|
1749
|
+
const backupsToRevert = fileBackupQueries.getFromSequence(sessionId, checkpoint.messageSequence);
|
|
1750
|
+
const fileToEarliestBackup = /* @__PURE__ */ new Map();
|
|
1751
|
+
for (const backup of backupsToRevert) {
|
|
1752
|
+
if (!fileToEarliestBackup.has(backup.filePath)) {
|
|
1753
|
+
fileToEarliestBackup.set(backup.filePath, backup);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
let filesRestored = 0;
|
|
1757
|
+
let filesDeleted = 0;
|
|
1758
|
+
for (const [filePath, backup] of fileToEarliestBackup) {
|
|
1759
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1760
|
+
try {
|
|
1761
|
+
if (backup.existed && backup.originalContent !== null) {
|
|
1762
|
+
const dir = dirname2(absolutePath);
|
|
1763
|
+
if (!existsSync4(dir)) {
|
|
1764
|
+
await mkdir2(dir, { recursive: true });
|
|
1765
|
+
}
|
|
1766
|
+
await writeFile2(absolutePath, backup.originalContent, "utf-8");
|
|
1767
|
+
filesRestored++;
|
|
1768
|
+
} else if (!backup.existed) {
|
|
1769
|
+
if (existsSync4(absolutePath)) {
|
|
1770
|
+
await unlink(absolutePath);
|
|
1771
|
+
filesDeleted++;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
console.error(`Failed to restore ${filePath}: ${error.message}`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const messagesDeleted = messageQueries.deleteFromSequence(sessionId, checkpoint.messageSequence);
|
|
1779
|
+
toolExecutionQueries.deleteAfterTime(sessionId, checkpoint.createdAt);
|
|
1780
|
+
const checkpointsDeleted = checkpointQueries.deleteAfterSequence(sessionId, checkpoint.messageSequence);
|
|
1781
|
+
const manager = getCheckpointManager(sessionId, workingDirectory);
|
|
1782
|
+
manager.currentCheckpointId = checkpoint.id;
|
|
1783
|
+
return {
|
|
1784
|
+
success: true,
|
|
1785
|
+
filesRestored,
|
|
1786
|
+
filesDeleted,
|
|
1787
|
+
messagesDeleted,
|
|
1788
|
+
checkpointsDeleted
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function getCheckpoints(sessionId) {
|
|
1792
|
+
return checkpointQueries.getBySession(sessionId);
|
|
1793
|
+
}
|
|
1794
|
+
async function getSessionDiff(sessionId) {
|
|
1795
|
+
const session = sessionQueries.getById(sessionId);
|
|
1796
|
+
if (!session) {
|
|
1797
|
+
return { files: [] };
|
|
1798
|
+
}
|
|
1799
|
+
const workingDirectory = session.workingDirectory;
|
|
1800
|
+
const allBackups = fileBackupQueries.getBySession(sessionId);
|
|
1801
|
+
const fileToOriginalBackup = /* @__PURE__ */ new Map();
|
|
1802
|
+
for (const backup of allBackups) {
|
|
1803
|
+
if (!fileToOriginalBackup.has(backup.filePath)) {
|
|
1804
|
+
fileToOriginalBackup.set(backup.filePath, backup);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const files = [];
|
|
1808
|
+
for (const [filePath, originalBackup] of fileToOriginalBackup) {
|
|
1809
|
+
const absolutePath = resolve3(workingDirectory, filePath);
|
|
1810
|
+
let currentContent = null;
|
|
1811
|
+
let currentExists = false;
|
|
1812
|
+
if (existsSync4(absolutePath)) {
|
|
1813
|
+
try {
|
|
1814
|
+
currentContent = await readFile3(absolutePath, "utf-8");
|
|
1815
|
+
currentExists = true;
|
|
1816
|
+
} catch {
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
let status;
|
|
1820
|
+
if (!originalBackup.existed && currentExists) {
|
|
1821
|
+
status = "created";
|
|
1822
|
+
} else if (originalBackup.existed && !currentExists) {
|
|
1823
|
+
status = "deleted";
|
|
1824
|
+
} else {
|
|
1825
|
+
status = "modified";
|
|
1826
|
+
}
|
|
1827
|
+
files.push({
|
|
1828
|
+
path: filePath,
|
|
1829
|
+
status,
|
|
1830
|
+
originalContent: originalBackup.originalContent,
|
|
1831
|
+
currentContent
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return { files };
|
|
1835
|
+
}
|
|
1836
|
+
function clearCheckpointManager(sessionId) {
|
|
1837
|
+
activeManagers.delete(sessionId);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/tools/write-file.ts
|
|
1544
1841
|
var writeFileInputSchema = z4.object({
|
|
1545
1842
|
path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
|
|
1546
1843
|
mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
|
|
@@ -1569,8 +1866,8 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1569
1866
|
inputSchema: writeFileInputSchema,
|
|
1570
1867
|
execute: async ({ path, mode, content, old_string, new_string }) => {
|
|
1571
1868
|
try {
|
|
1572
|
-
const absolutePath = isAbsolute2(path) ? path :
|
|
1573
|
-
const relativePath =
|
|
1869
|
+
const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
|
|
1870
|
+
const relativePath = relative3(options.workingDirectory, absolutePath);
|
|
1574
1871
|
if (relativePath.startsWith("..") && !isAbsolute2(path)) {
|
|
1575
1872
|
return {
|
|
1576
1873
|
success: false,
|
|
@@ -1584,16 +1881,17 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1584
1881
|
error: 'Content is required for "full" mode'
|
|
1585
1882
|
};
|
|
1586
1883
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1884
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
1885
|
+
const dir = dirname3(absolutePath);
|
|
1886
|
+
if (!existsSync5(dir)) {
|
|
1887
|
+
await mkdir3(dir, { recursive: true });
|
|
1590
1888
|
}
|
|
1591
|
-
const existed =
|
|
1592
|
-
await
|
|
1889
|
+
const existed = existsSync5(absolutePath);
|
|
1890
|
+
await writeFile3(absolutePath, content, "utf-8");
|
|
1593
1891
|
return {
|
|
1594
1892
|
success: true,
|
|
1595
1893
|
path: absolutePath,
|
|
1596
|
-
relativePath:
|
|
1894
|
+
relativePath: relative3(options.workingDirectory, absolutePath),
|
|
1597
1895
|
mode: "full",
|
|
1598
1896
|
action: existed ? "replaced" : "created",
|
|
1599
1897
|
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
@@ -1606,13 +1904,14 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1606
1904
|
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
1607
1905
|
};
|
|
1608
1906
|
}
|
|
1609
|
-
if (!
|
|
1907
|
+
if (!existsSync5(absolutePath)) {
|
|
1610
1908
|
return {
|
|
1611
1909
|
success: false,
|
|
1612
1910
|
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
1613
1911
|
};
|
|
1614
1912
|
}
|
|
1615
|
-
|
|
1913
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
1914
|
+
const currentContent = await readFile4(absolutePath, "utf-8");
|
|
1616
1915
|
if (!currentContent.includes(old_string)) {
|
|
1617
1916
|
const lines = currentContent.split("\n");
|
|
1618
1917
|
const preview = lines.slice(0, 20).join("\n");
|
|
@@ -1633,13 +1932,13 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1633
1932
|
};
|
|
1634
1933
|
}
|
|
1635
1934
|
const newContent = currentContent.replace(old_string, new_string);
|
|
1636
|
-
await
|
|
1935
|
+
await writeFile3(absolutePath, newContent, "utf-8");
|
|
1637
1936
|
const oldLines = old_string.split("\n").length;
|
|
1638
1937
|
const newLines = new_string.split("\n").length;
|
|
1639
1938
|
return {
|
|
1640
1939
|
success: true,
|
|
1641
1940
|
path: absolutePath,
|
|
1642
|
-
relativePath:
|
|
1941
|
+
relativePath: relative3(options.workingDirectory, absolutePath),
|
|
1643
1942
|
mode: "str_replace",
|
|
1644
1943
|
linesRemoved: oldLines,
|
|
1645
1944
|
linesAdded: newLines,
|
|
@@ -1661,7 +1960,6 @@ Working directory: ${options.workingDirectory}`,
|
|
|
1661
1960
|
}
|
|
1662
1961
|
|
|
1663
1962
|
// src/tools/todo.ts
|
|
1664
|
-
init_db();
|
|
1665
1963
|
import { tool as tool4 } from "ai";
|
|
1666
1964
|
import { z as z5 } from "zod";
|
|
1667
1965
|
var todoInputSchema = z5.object({
|
|
@@ -1791,9 +2089,9 @@ import { tool as tool5 } from "ai";
|
|
|
1791
2089
|
import { z as z6 } from "zod";
|
|
1792
2090
|
|
|
1793
2091
|
// src/skills/index.ts
|
|
1794
|
-
import { readFile as
|
|
1795
|
-
import { resolve as
|
|
1796
|
-
import { existsSync as
|
|
2092
|
+
import { readFile as readFile5, readdir } from "fs/promises";
|
|
2093
|
+
import { resolve as resolve5, basename, extname } from "path";
|
|
2094
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1797
2095
|
function parseSkillFrontmatter(content) {
|
|
1798
2096
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1799
2097
|
if (!frontmatterMatch) {
|
|
@@ -1824,15 +2122,15 @@ function getSkillNameFromPath(filePath) {
|
|
|
1824
2122
|
return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1825
2123
|
}
|
|
1826
2124
|
async function loadSkillsFromDirectory(directory) {
|
|
1827
|
-
if (!
|
|
2125
|
+
if (!existsSync6(directory)) {
|
|
1828
2126
|
return [];
|
|
1829
2127
|
}
|
|
1830
2128
|
const skills = [];
|
|
1831
2129
|
const files = await readdir(directory);
|
|
1832
2130
|
for (const file of files) {
|
|
1833
2131
|
if (!file.endsWith(".md")) continue;
|
|
1834
|
-
const filePath =
|
|
1835
|
-
const content = await
|
|
2132
|
+
const filePath = resolve5(directory, file);
|
|
2133
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1836
2134
|
const parsed = parseSkillFrontmatter(content);
|
|
1837
2135
|
if (parsed) {
|
|
1838
2136
|
skills.push({
|
|
@@ -1874,7 +2172,7 @@ async function loadSkillContent(skillName, directories) {
|
|
|
1874
2172
|
if (!skill) {
|
|
1875
2173
|
return null;
|
|
1876
2174
|
}
|
|
1877
|
-
const content = await
|
|
2175
|
+
const content = await readFile5(skill.filePath, "utf-8");
|
|
1878
2176
|
const parsed = parseSkillFrontmatter(content);
|
|
1879
2177
|
return {
|
|
1880
2178
|
...skill,
|
|
@@ -1893,7 +2191,6 @@ function formatSkillsForContext(skills) {
|
|
|
1893
2191
|
}
|
|
1894
2192
|
|
|
1895
2193
|
// src/tools/load-skill.ts
|
|
1896
|
-
init_db();
|
|
1897
2194
|
var loadSkillInputSchema = z6.object({
|
|
1898
2195
|
action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
|
|
1899
2196
|
skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
|
|
@@ -1986,7 +2283,8 @@ function createTools(options) {
|
|
|
1986
2283
|
workingDirectory: options.workingDirectory
|
|
1987
2284
|
}),
|
|
1988
2285
|
write_file: createWriteFileTool({
|
|
1989
|
-
workingDirectory: options.workingDirectory
|
|
2286
|
+
workingDirectory: options.workingDirectory,
|
|
2287
|
+
sessionId: options.sessionId
|
|
1990
2288
|
}),
|
|
1991
2289
|
todo: createTodoTool({
|
|
1992
2290
|
sessionId: options.sessionId
|
|
@@ -1999,13 +2297,11 @@ function createTools(options) {
|
|
|
1999
2297
|
}
|
|
2000
2298
|
|
|
2001
2299
|
// src/agent/context.ts
|
|
2002
|
-
init_db();
|
|
2003
2300
|
import { generateText } from "ai";
|
|
2004
2301
|
import { gateway } from "@ai-sdk/gateway";
|
|
2005
2302
|
|
|
2006
2303
|
// src/agent/prompts.ts
|
|
2007
2304
|
import os from "os";
|
|
2008
|
-
init_db();
|
|
2009
2305
|
function getSearchInstructions() {
|
|
2010
2306
|
const platform3 = process.platform;
|
|
2011
2307
|
const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
|
|
@@ -2046,6 +2342,9 @@ You have access to powerful tools for:
|
|
|
2046
2342
|
- **todo**: Manage your task list to track progress on complex operations
|
|
2047
2343
|
- **load_skill**: Load specialized knowledge documents for specific tasks
|
|
2048
2344
|
|
|
2345
|
+
|
|
2346
|
+
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.
|
|
2347
|
+
|
|
2049
2348
|
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.
|
|
2050
2349
|
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.
|
|
2051
2350
|
You can clear the todo and restart it, and do multiple things inside of one session.
|
|
@@ -2482,8 +2781,8 @@ var Agent = class _Agent {
|
|
|
2482
2781
|
this.pendingApprovals.set(toolCallId, execution);
|
|
2483
2782
|
options.onApprovalRequired?.(execution);
|
|
2484
2783
|
sessionQueries.updateStatus(this.session.id, "waiting");
|
|
2485
|
-
const approved = await new Promise((
|
|
2486
|
-
approvalResolvers.set(toolCallId, { resolve:
|
|
2784
|
+
const approved = await new Promise((resolve8) => {
|
|
2785
|
+
approvalResolvers.set(toolCallId, { resolve: resolve8, sessionId: this.session.id });
|
|
2487
2786
|
});
|
|
2488
2787
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
2489
2788
|
approvalResolvers.delete(toolCallId);
|
|
@@ -2784,6 +3083,7 @@ sessions2.delete("/:id", async (c) => {
|
|
|
2784
3083
|
}
|
|
2785
3084
|
} catch (e) {
|
|
2786
3085
|
}
|
|
3086
|
+
clearCheckpointManager(id);
|
|
2787
3087
|
const deleted = sessionQueries.delete(id);
|
|
2788
3088
|
if (!deleted) {
|
|
2789
3089
|
return c.json({ error: "Session not found" }, 404);
|
|
@@ -2835,9 +3135,100 @@ sessions2.get("/:id/todos", async (c) => {
|
|
|
2835
3135
|
} : null
|
|
2836
3136
|
});
|
|
2837
3137
|
});
|
|
3138
|
+
sessions2.get("/:id/checkpoints", async (c) => {
|
|
3139
|
+
const id = c.req.param("id");
|
|
3140
|
+
const session = sessionQueries.getById(id);
|
|
3141
|
+
if (!session) {
|
|
3142
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3143
|
+
}
|
|
3144
|
+
const checkpoints2 = getCheckpoints(id);
|
|
3145
|
+
return c.json({
|
|
3146
|
+
sessionId: id,
|
|
3147
|
+
checkpoints: checkpoints2.map((cp) => ({
|
|
3148
|
+
id: cp.id,
|
|
3149
|
+
messageSequence: cp.messageSequence,
|
|
3150
|
+
gitHead: cp.gitHead,
|
|
3151
|
+
createdAt: cp.createdAt.toISOString()
|
|
3152
|
+
})),
|
|
3153
|
+
count: checkpoints2.length
|
|
3154
|
+
});
|
|
3155
|
+
});
|
|
3156
|
+
sessions2.post("/:id/revert/:checkpointId", async (c) => {
|
|
3157
|
+
const sessionId = c.req.param("id");
|
|
3158
|
+
const checkpointId = c.req.param("checkpointId");
|
|
3159
|
+
const session = sessionQueries.getById(sessionId);
|
|
3160
|
+
if (!session) {
|
|
3161
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3162
|
+
}
|
|
3163
|
+
const activeStream = activeStreamQueries.getBySessionId(sessionId);
|
|
3164
|
+
if (activeStream) {
|
|
3165
|
+
return c.json({
|
|
3166
|
+
error: "Cannot revert while a stream is active. Stop the stream first.",
|
|
3167
|
+
streamId: activeStream.streamId
|
|
3168
|
+
}, 409);
|
|
3169
|
+
}
|
|
3170
|
+
const result = await revertToCheckpoint(sessionId, checkpointId);
|
|
3171
|
+
if (!result.success) {
|
|
3172
|
+
return c.json({ error: result.error }, 400);
|
|
3173
|
+
}
|
|
3174
|
+
return c.json({
|
|
3175
|
+
success: true,
|
|
3176
|
+
sessionId,
|
|
3177
|
+
checkpointId,
|
|
3178
|
+
filesRestored: result.filesRestored,
|
|
3179
|
+
filesDeleted: result.filesDeleted,
|
|
3180
|
+
messagesDeleted: result.messagesDeleted,
|
|
3181
|
+
checkpointsDeleted: result.checkpointsDeleted
|
|
3182
|
+
});
|
|
3183
|
+
});
|
|
3184
|
+
sessions2.get("/:id/diff", async (c) => {
|
|
3185
|
+
const id = c.req.param("id");
|
|
3186
|
+
const session = sessionQueries.getById(id);
|
|
3187
|
+
if (!session) {
|
|
3188
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3189
|
+
}
|
|
3190
|
+
const diff = await getSessionDiff(id);
|
|
3191
|
+
return c.json({
|
|
3192
|
+
sessionId: id,
|
|
3193
|
+
files: diff.files.map((f) => ({
|
|
3194
|
+
path: f.path,
|
|
3195
|
+
status: f.status,
|
|
3196
|
+
hasOriginal: f.originalContent !== null,
|
|
3197
|
+
hasCurrent: f.currentContent !== null
|
|
3198
|
+
// Optionally include content (can be large)
|
|
3199
|
+
// originalContent: f.originalContent,
|
|
3200
|
+
// currentContent: f.currentContent,
|
|
3201
|
+
})),
|
|
3202
|
+
summary: {
|
|
3203
|
+
created: diff.files.filter((f) => f.status === "created").length,
|
|
3204
|
+
modified: diff.files.filter((f) => f.status === "modified").length,
|
|
3205
|
+
deleted: diff.files.filter((f) => f.status === "deleted").length,
|
|
3206
|
+
total: diff.files.length
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
});
|
|
3210
|
+
sessions2.get("/:id/diff/:filePath", async (c) => {
|
|
3211
|
+
const sessionId = c.req.param("id");
|
|
3212
|
+
const filePath = decodeURIComponent(c.req.param("filePath"));
|
|
3213
|
+
const session = sessionQueries.getById(sessionId);
|
|
3214
|
+
if (!session) {
|
|
3215
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3216
|
+
}
|
|
3217
|
+
const diff = await getSessionDiff(sessionId);
|
|
3218
|
+
const fileDiff = diff.files.find((f) => f.path === filePath);
|
|
3219
|
+
if (!fileDiff) {
|
|
3220
|
+
return c.json({ error: "File not found in diff" }, 404);
|
|
3221
|
+
}
|
|
3222
|
+
return c.json({
|
|
3223
|
+
sessionId,
|
|
3224
|
+
path: fileDiff.path,
|
|
3225
|
+
status: fileDiff.status,
|
|
3226
|
+
originalContent: fileDiff.originalContent,
|
|
3227
|
+
currentContent: fileDiff.currentContent
|
|
3228
|
+
});
|
|
3229
|
+
});
|
|
2838
3230
|
|
|
2839
3231
|
// src/server/routes/agents.ts
|
|
2840
|
-
init_db();
|
|
2841
3232
|
import { Hono as Hono2 } from "hono";
|
|
2842
3233
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
2843
3234
|
import { z as z9 } from "zod";
|
|
@@ -3111,8 +3502,9 @@ agents.post(
|
|
|
3111
3502
|
if (!session) {
|
|
3112
3503
|
return c.json({ error: "Session not found" }, 404);
|
|
3113
3504
|
}
|
|
3114
|
-
const
|
|
3115
|
-
|
|
3505
|
+
const nextSequence = messageQueries.getNextSequence(id);
|
|
3506
|
+
await createCheckpoint(id, session.workingDirectory, nextSequence);
|
|
3507
|
+
messageQueries.create(id, { role: "user", content: prompt });
|
|
3116
3508
|
const streamId = `stream_${id}_${nanoid4(10)}`;
|
|
3117
3509
|
activeStreamQueries.create(id, streamId);
|
|
3118
3510
|
const stream = await streamContext.resumableStream(
|
|
@@ -3311,6 +3703,7 @@ agents.post(
|
|
|
3311
3703
|
});
|
|
3312
3704
|
const session = agent.getSession();
|
|
3313
3705
|
const streamId = `stream_${session.id}_${nanoid4(10)}`;
|
|
3706
|
+
await createCheckpoint(session.id, session.workingDirectory, 0);
|
|
3314
3707
|
activeStreamQueries.create(session.id, streamId);
|
|
3315
3708
|
const createQuickStreamProducer = () => {
|
|
3316
3709
|
const { readable, writable } = new TransformStream();
|
|
@@ -3491,10 +3884,14 @@ import { z as z10 } from "zod";
|
|
|
3491
3884
|
var health = new Hono3();
|
|
3492
3885
|
health.get("/", async (c) => {
|
|
3493
3886
|
const config = getConfig();
|
|
3887
|
+
const apiKeyStatus = getApiKeyStatus();
|
|
3888
|
+
const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
|
|
3889
|
+
const hasApiKey = gatewayKey?.configured ?? false;
|
|
3494
3890
|
return c.json({
|
|
3495
3891
|
status: "ok",
|
|
3496
3892
|
version: "0.1.0",
|
|
3497
3893
|
uptime: process.uptime(),
|
|
3894
|
+
apiKeyConfigured: hasApiKey,
|
|
3498
3895
|
config: {
|
|
3499
3896
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
3500
3897
|
defaultModel: config.defaultModel,
|
|
@@ -3571,7 +3968,6 @@ health.delete("/api-keys/:provider", async (c) => {
|
|
|
3571
3968
|
import { Hono as Hono4 } from "hono";
|
|
3572
3969
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3573
3970
|
import { z as z11 } from "zod";
|
|
3574
|
-
init_db();
|
|
3575
3971
|
var terminals2 = new Hono4();
|
|
3576
3972
|
var spawnSchema = z11.object({
|
|
3577
3973
|
command: z11.string(),
|
|
@@ -3869,14 +4265,11 @@ data: ${JSON.stringify({ status: "stopped" })}
|
|
|
3869
4265
|
);
|
|
3870
4266
|
});
|
|
3871
4267
|
|
|
3872
|
-
// src/server/index.ts
|
|
3873
|
-
init_db();
|
|
3874
|
-
|
|
3875
4268
|
// src/utils/dependencies.ts
|
|
3876
|
-
import { exec as
|
|
3877
|
-
import { promisify as
|
|
4269
|
+
import { exec as exec4 } from "child_process";
|
|
4270
|
+
import { promisify as promisify4 } from "util";
|
|
3878
4271
|
import { platform as platform2 } from "os";
|
|
3879
|
-
var
|
|
4272
|
+
var execAsync4 = promisify4(exec4);
|
|
3880
4273
|
function getInstallInstructions() {
|
|
3881
4274
|
const os2 = platform2();
|
|
3882
4275
|
if (os2 === "darwin") {
|
|
@@ -3909,7 +4302,7 @@ Install tmux:
|
|
|
3909
4302
|
}
|
|
3910
4303
|
async function checkTmux() {
|
|
3911
4304
|
try {
|
|
3912
|
-
const { stdout } = await
|
|
4305
|
+
const { stdout } = await execAsync4("tmux -V", { timeout: 5e3 });
|
|
3913
4306
|
const version = stdout.trim();
|
|
3914
4307
|
return {
|
|
3915
4308
|
available: true,
|
|
@@ -3953,21 +4346,21 @@ async function tryAutoInstallTmux() {
|
|
|
3953
4346
|
try {
|
|
3954
4347
|
if (os2 === "darwin") {
|
|
3955
4348
|
try {
|
|
3956
|
-
await
|
|
4349
|
+
await execAsync4("which brew", { timeout: 5e3 });
|
|
3957
4350
|
} catch {
|
|
3958
4351
|
return false;
|
|
3959
4352
|
}
|
|
3960
4353
|
console.log("\u{1F4E6} Installing tmux via Homebrew...");
|
|
3961
|
-
await
|
|
4354
|
+
await execAsync4("brew install tmux", { timeout: 3e5 });
|
|
3962
4355
|
console.log("\u2705 tmux installed successfully");
|
|
3963
4356
|
return true;
|
|
3964
4357
|
}
|
|
3965
4358
|
if (os2 === "linux") {
|
|
3966
4359
|
try {
|
|
3967
|
-
await
|
|
4360
|
+
await execAsync4("which apt-get", { timeout: 5e3 });
|
|
3968
4361
|
console.log("\u{1F4E6} Installing tmux via apt-get...");
|
|
3969
4362
|
console.log(" (This may require sudo password)");
|
|
3970
|
-
await
|
|
4363
|
+
await execAsync4("sudo apt-get update && sudo apt-get install -y tmux", {
|
|
3971
4364
|
timeout: 3e5
|
|
3972
4365
|
});
|
|
3973
4366
|
console.log("\u2705 tmux installed successfully");
|
|
@@ -3975,9 +4368,9 @@ async function tryAutoInstallTmux() {
|
|
|
3975
4368
|
} catch {
|
|
3976
4369
|
}
|
|
3977
4370
|
try {
|
|
3978
|
-
await
|
|
4371
|
+
await execAsync4("which dnf", { timeout: 5e3 });
|
|
3979
4372
|
console.log("\u{1F4E6} Installing tmux via dnf...");
|
|
3980
|
-
await
|
|
4373
|
+
await execAsync4("sudo dnf install -y tmux", { timeout: 3e5 });
|
|
3981
4374
|
console.log("\u2705 tmux installed successfully");
|
|
3982
4375
|
return true;
|
|
3983
4376
|
} catch {
|
|
@@ -4011,13 +4404,13 @@ var DEFAULT_WEB_PORT = 6969;
|
|
|
4011
4404
|
var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
|
|
4012
4405
|
function getWebDirectory() {
|
|
4013
4406
|
try {
|
|
4014
|
-
const currentDir =
|
|
4015
|
-
const webDir =
|
|
4016
|
-
if (
|
|
4407
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
4408
|
+
const webDir = resolve6(currentDir, "..", "web");
|
|
4409
|
+
if (existsSync7(webDir) && existsSync7(join3(webDir, "package.json"))) {
|
|
4017
4410
|
return webDir;
|
|
4018
4411
|
}
|
|
4019
|
-
const altWebDir =
|
|
4020
|
-
if (
|
|
4412
|
+
const altWebDir = resolve6(currentDir, "..", "..", "web");
|
|
4413
|
+
if (existsSync7(altWebDir) && existsSync7(join3(altWebDir, "package.json"))) {
|
|
4021
4414
|
return altWebDir;
|
|
4022
4415
|
}
|
|
4023
4416
|
return null;
|
|
@@ -4040,18 +4433,18 @@ async function isSparkcoderWebRunning(port) {
|
|
|
4040
4433
|
}
|
|
4041
4434
|
}
|
|
4042
4435
|
function isPortInUse(port) {
|
|
4043
|
-
return new Promise((
|
|
4436
|
+
return new Promise((resolve8) => {
|
|
4044
4437
|
const server = createNetServer();
|
|
4045
4438
|
server.once("error", (err) => {
|
|
4046
4439
|
if (err.code === "EADDRINUSE") {
|
|
4047
|
-
|
|
4440
|
+
resolve8(true);
|
|
4048
4441
|
} else {
|
|
4049
|
-
|
|
4442
|
+
resolve8(false);
|
|
4050
4443
|
}
|
|
4051
4444
|
});
|
|
4052
4445
|
server.once("listening", () => {
|
|
4053
4446
|
server.close();
|
|
4054
|
-
|
|
4447
|
+
resolve8(false);
|
|
4055
4448
|
});
|
|
4056
4449
|
server.listen(port, "0.0.0.0");
|
|
4057
4450
|
});
|
|
@@ -4085,7 +4478,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
|
|
|
4085
4478
|
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
4086
4479
|
return { process: null, port: actualPort };
|
|
4087
4480
|
}
|
|
4088
|
-
const useNpm =
|
|
4481
|
+
const useNpm = existsSync7(join3(webDir, "package-lock.json"));
|
|
4089
4482
|
const command = useNpm ? "npm" : "npx";
|
|
4090
4483
|
const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
|
|
4091
4484
|
const child = spawn(command, args, {
|
|
@@ -4188,7 +4581,7 @@ async function startServer(options = {}) {
|
|
|
4188
4581
|
if (options.workingDirectory) {
|
|
4189
4582
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
4190
4583
|
}
|
|
4191
|
-
if (!
|
|
4584
|
+
if (!existsSync7(config.resolvedWorkingDirectory)) {
|
|
4192
4585
|
mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
|
|
4193
4586
|
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
4194
4587
|
}
|
|
@@ -4686,8 +5079,8 @@ function generateOpenAPISpec() {
|
|
|
4686
5079
|
}
|
|
4687
5080
|
|
|
4688
5081
|
// src/cli.ts
|
|
4689
|
-
import { writeFileSync as writeFileSync2, existsSync as
|
|
4690
|
-
import { resolve as
|
|
5082
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync8 } from "fs";
|
|
5083
|
+
import { resolve as resolve7, join as join4 } from "path";
|
|
4691
5084
|
async function apiRequest(baseUrl, path, options = {}) {
|
|
4692
5085
|
const url = `${baseUrl}${path}`;
|
|
4693
5086
|
const init = {
|
|
@@ -4722,13 +5115,13 @@ async function getActiveStream(baseUrl, sessionId) {
|
|
|
4722
5115
|
return { hasActiveStream: false };
|
|
4723
5116
|
}
|
|
4724
5117
|
function promptApproval(rl, toolName, input) {
|
|
4725
|
-
return new Promise((
|
|
5118
|
+
return new Promise((resolve8) => {
|
|
4726
5119
|
const inputStr = JSON.stringify(input);
|
|
4727
5120
|
const truncatedInput = inputStr.length > 100 ? inputStr.slice(0, 100) + "..." : inputStr;
|
|
4728
5121
|
console.log(chalk.dim(` Command: ${truncatedInput}`));
|
|
4729
5122
|
rl.question(chalk.yellow(` Approve? [y/n]: `), (answer) => {
|
|
4730
5123
|
const approved = answer.toLowerCase().startsWith("y");
|
|
4731
|
-
|
|
5124
|
+
resolve8(approved);
|
|
4732
5125
|
});
|
|
4733
5126
|
});
|
|
4734
5127
|
}
|
|
@@ -4840,7 +5233,27 @@ async function runChat(options) {
|
|
|
4840
5233
|
const baseUrl = `http://${options.host}:${options.port}`;
|
|
4841
5234
|
try {
|
|
4842
5235
|
const running = await isServerRunning(baseUrl);
|
|
4843
|
-
if (
|
|
5236
|
+
if (running) {
|
|
5237
|
+
const webPortSequence = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
|
|
5238
|
+
let actualWebPort = options.webPort || "6969";
|
|
5239
|
+
for (const port of webPortSequence) {
|
|
5240
|
+
try {
|
|
5241
|
+
const response = await fetch(`http://localhost:${port}/api/health`, {
|
|
5242
|
+
signal: AbortSignal.timeout(500)
|
|
5243
|
+
});
|
|
5244
|
+
if (response.ok) {
|
|
5245
|
+
const data = await response.json();
|
|
5246
|
+
if (data.name === "sparkecoder-web") {
|
|
5247
|
+
actualWebPort = String(port);
|
|
5248
|
+
break;
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
} catch {
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
const webUrl = `http://localhost:${actualWebPort}`;
|
|
5255
|
+
console.log(`\u2714 Web UI: ${chalk.cyan(webUrl)}`);
|
|
5256
|
+
} else {
|
|
4844
5257
|
if (options.autoStart === false) {
|
|
4845
5258
|
console.error(chalk.red(`Server not running at ${baseUrl}`));
|
|
4846
5259
|
console.error(chalk.dim("Start with: sparkecoder server"));
|
|
@@ -4848,7 +5261,7 @@ async function runChat(options) {
|
|
|
4848
5261
|
}
|
|
4849
5262
|
const spinner = ora("Starting server...").start();
|
|
4850
5263
|
try {
|
|
4851
|
-
await startServer({
|
|
5264
|
+
const serverResult = await startServer({
|
|
4852
5265
|
port: parseInt(options.port),
|
|
4853
5266
|
host: options.host,
|
|
4854
5267
|
configPath: options.config,
|
|
@@ -4860,7 +5273,8 @@ async function runChat(options) {
|
|
|
4860
5273
|
webPort: parseInt(options.webPort || "6969")
|
|
4861
5274
|
});
|
|
4862
5275
|
serverStartedByUs = true;
|
|
4863
|
-
|
|
5276
|
+
const webUrl = `http://localhost:${serverResult.webPort || options.webPort || "6969"}`;
|
|
5277
|
+
spinner.succeed(`Web UI: ${chalk.cyan(webUrl)}`);
|
|
4864
5278
|
const cleanup = () => {
|
|
4865
5279
|
if (serverStartedByUs) {
|
|
4866
5280
|
stopServer();
|
|
@@ -4874,6 +5288,36 @@ async function runChat(options) {
|
|
|
4874
5288
|
process.exit(1);
|
|
4875
5289
|
}
|
|
4876
5290
|
}
|
|
5291
|
+
const healthResponse = await fetch(`${baseUrl}/health`);
|
|
5292
|
+
const healthData = await healthResponse.json();
|
|
5293
|
+
if (!healthData.apiKeyConfigured) {
|
|
5294
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F No AI Gateway API key configured."));
|
|
5295
|
+
console.log(chalk.dim("An API key is required to use SparkECoder.\n"));
|
|
5296
|
+
console.log(chalk.dim("Get your API key from: https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys\n"));
|
|
5297
|
+
const keyRl = createInterface({
|
|
5298
|
+
input: process.stdin,
|
|
5299
|
+
output: process.stdout
|
|
5300
|
+
});
|
|
5301
|
+
const apiKey = await new Promise((resolve8) => {
|
|
5302
|
+
keyRl.question(chalk.cyan("Enter your AI Gateway API key: "), (answer) => {
|
|
5303
|
+
resolve8(answer.trim());
|
|
5304
|
+
});
|
|
5305
|
+
});
|
|
5306
|
+
keyRl.close();
|
|
5307
|
+
if (!apiKey) {
|
|
5308
|
+
console.error(chalk.red("\nAPI key is required. Exiting."));
|
|
5309
|
+
process.exit(1);
|
|
5310
|
+
}
|
|
5311
|
+
const saveResponse = await apiRequest(baseUrl, "/health/api-keys", {
|
|
5312
|
+
method: "POST",
|
|
5313
|
+
body: { provider: "ai-gateway", apiKey }
|
|
5314
|
+
});
|
|
5315
|
+
if (!saveResponse.ok) {
|
|
5316
|
+
console.error(chalk.red("\nFailed to save API key. Please try again."));
|
|
5317
|
+
process.exit(1);
|
|
5318
|
+
}
|
|
5319
|
+
console.log(chalk.green("\u2713 API key saved successfully.\n"));
|
|
5320
|
+
}
|
|
4877
5321
|
const rl = createInterface({
|
|
4878
5322
|
input: process.stdin,
|
|
4879
5323
|
output: process.stdout
|
|
@@ -5150,17 +5594,15 @@ program.command("server").description("Start the SparkECoder server (API + Web U
|
|
|
5150
5594
|
webUI: options.web !== false,
|
|
5151
5595
|
webPort: parseInt(options.webPort) || 6969
|
|
5152
5596
|
});
|
|
5153
|
-
spinner.succeed(chalk.green(`SparkECoder server running`));
|
|
5154
|
-
console.log("");
|
|
5155
|
-
console.log(chalk.bold(" API Server:"));
|
|
5156
|
-
console.log(chalk.dim(` \u2192 http://${host}:${port}`));
|
|
5157
|
-
console.log(chalk.dim(` \u2192 Swagger: http://${host}:${port}/swagger`));
|
|
5158
5597
|
if (webPort) {
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5598
|
+
spinner.succeed(chalk.green(`SparkECoder running at http://localhost:${webPort}`));
|
|
5599
|
+
} else {
|
|
5600
|
+
spinner.succeed(chalk.green(`SparkECoder server running`));
|
|
5162
5601
|
}
|
|
5163
5602
|
console.log("");
|
|
5603
|
+
console.log(chalk.dim(` API: http://${host}:${port}`));
|
|
5604
|
+
console.log(chalk.dim(` Swagger: http://${host}:${port}/swagger`));
|
|
5605
|
+
console.log("");
|
|
5164
5606
|
console.log(chalk.dim("Press Ctrl+C to stop"));
|
|
5165
5607
|
const cleanup = () => {
|
|
5166
5608
|
console.log(chalk.dim("\nShutting down..."));
|
|
@@ -5185,10 +5627,10 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
|
|
|
5185
5627
|
configPath = join4(appDataDir, "sparkecoder.config.json");
|
|
5186
5628
|
configLocation = "global";
|
|
5187
5629
|
} else {
|
|
5188
|
-
configPath =
|
|
5630
|
+
configPath = resolve7(process.cwd(), "sparkecoder.config.json");
|
|
5189
5631
|
configLocation = "local";
|
|
5190
5632
|
}
|
|
5191
|
-
if (
|
|
5633
|
+
if (existsSync8(configPath) && !options.force) {
|
|
5192
5634
|
console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
|
|
5193
5635
|
console.log(chalk.dim(` ${configPath}`));
|
|
5194
5636
|
return;
|