@grackle-ai/server 0.14.9 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +39 -6
- package/dist/db.js.map +1 -1
- package/dist/grpc-service.d.ts +7 -0
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +265 -26
- package/dist/grpc-service.js.map +1 -1
- package/dist/persona-store.d.ts +15 -0
- package/dist/persona-store.d.ts.map +1 -0
- package/dist/persona-store.js +53 -0
- package/dist/persona-store.js.map +1 -0
- package/dist/schema.d.ts +237 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +58 -13
- package/dist/schema.js.map +1 -1
- package/dist/task-store.d.ts +1 -1
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +40 -17
- package/dist/task-store.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +49 -9
- package/dist/ws-bridge.js.map +1 -1
- package/package.json +3 -3
package/dist/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAatC,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAatC,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,IAAI,CAgMnC;AAKD,yDAAyD;AACzD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,QAAA,MAAM,EAAE,EAAE,qBAAqB,CAAC,OAAO,MAAM,CAAC,GAAG;IAC/C,OAAO,EAAE,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;CACV,CAAC;AAEhC,eAAe,EAAE,CAAC"}
|
package/dist/db.js
CHANGED
|
@@ -81,7 +81,9 @@ export function initDatabase() {
|
|
|
81
81
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
82
82
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
83
83
|
parent_task_id TEXT NOT NULL DEFAULT '',
|
|
84
|
-
depth INTEGER NOT NULL DEFAULT 0
|
|
84
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
can_decompose INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
persona_id TEXT NOT NULL DEFAULT ''
|
|
85
87
|
);
|
|
86
88
|
|
|
87
89
|
CREATE TABLE IF NOT EXISTS findings (
|
|
@@ -96,18 +98,36 @@ export function initDatabase() {
|
|
|
96
98
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
97
99
|
);
|
|
98
100
|
|
|
101
|
+
CREATE TABLE IF NOT EXISTS personas (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
name TEXT NOT NULL UNIQUE,
|
|
104
|
+
description TEXT NOT NULL DEFAULT '',
|
|
105
|
+
system_prompt TEXT NOT NULL,
|
|
106
|
+
tool_config TEXT NOT NULL DEFAULT '{}',
|
|
107
|
+
runtime TEXT NOT NULL DEFAULT '',
|
|
108
|
+
model TEXT NOT NULL DEFAULT '',
|
|
109
|
+
max_turns INTEGER NOT NULL DEFAULT 0,
|
|
110
|
+
mcp_servers TEXT NOT NULL DEFAULT '[]',
|
|
111
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
112
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
113
|
+
);
|
|
114
|
+
|
|
99
115
|
CREATE INDEX IF NOT EXISTS idx_findings_project ON findings(project_id);
|
|
100
116
|
`);
|
|
101
117
|
// Migration: add powerline_token column if missing (older databases)
|
|
102
118
|
try {
|
|
103
119
|
sqlite.exec("ALTER TABLE environments ADD COLUMN powerline_token TEXT NOT NULL DEFAULT ''");
|
|
104
120
|
}
|
|
105
|
-
catch {
|
|
121
|
+
catch {
|
|
122
|
+
/* column already exists */
|
|
123
|
+
}
|
|
106
124
|
// Migration: rename sidecar_token → powerline_token (from older databases)
|
|
107
125
|
try {
|
|
108
126
|
sqlite.exec("ALTER TABLE environments RENAME COLUMN sidecar_token TO powerline_token");
|
|
109
127
|
}
|
|
110
|
-
catch {
|
|
128
|
+
catch {
|
|
129
|
+
/* column already renamed or doesn't exist */
|
|
130
|
+
}
|
|
111
131
|
// Migration: backfill NULLs in stage-2 tables from older schemas that lacked NOT NULL
|
|
112
132
|
sqlite.exec(`
|
|
113
133
|
UPDATE projects SET description = '' WHERE description IS NULL;
|
|
@@ -138,11 +158,15 @@ export function initDatabase() {
|
|
|
138
158
|
try {
|
|
139
159
|
sqlite.exec("ALTER TABLE tasks ADD COLUMN parent_task_id TEXT NOT NULL DEFAULT ''");
|
|
140
160
|
}
|
|
141
|
-
catch {
|
|
161
|
+
catch {
|
|
162
|
+
/* column already exists */
|
|
163
|
+
}
|
|
142
164
|
try {
|
|
143
165
|
sqlite.exec("ALTER TABLE tasks ADD COLUMN depth INTEGER NOT NULL DEFAULT 0");
|
|
144
166
|
}
|
|
145
|
-
catch {
|
|
167
|
+
catch {
|
|
168
|
+
/* column already exists */
|
|
169
|
+
}
|
|
146
170
|
// Migration: add can_decompose column if missing (older databases)
|
|
147
171
|
try {
|
|
148
172
|
sqlite.exec("ALTER TABLE tasks ADD COLUMN can_decompose INTEGER NOT NULL DEFAULT 0");
|
|
@@ -158,7 +182,16 @@ export function initDatabase() {
|
|
|
158
182
|
)
|
|
159
183
|
`);
|
|
160
184
|
}
|
|
161
|
-
catch {
|
|
185
|
+
catch {
|
|
186
|
+
/* column already exists */
|
|
187
|
+
}
|
|
188
|
+
// Migration: add persona_id column to tasks if missing
|
|
189
|
+
try {
|
|
190
|
+
sqlite.exec("ALTER TABLE tasks ADD COLUMN persona_id TEXT NOT NULL DEFAULT ''");
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* column already exists */
|
|
194
|
+
}
|
|
162
195
|
}
|
|
163
196
|
// Run init immediately for backwards compatibility — stores import db at module load
|
|
164
197
|
initDatabase();
|
package/dist/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5C,MAAM,MAAM,GAAW,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,GAAkC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnE,yDAAyD;AACzD,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAEpC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAEnC,+EAA+E;AAC/E,MAAM,UAAU,YAAY;IAC1B,wDAAwD;IACxD,MAAM,CAAC,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5C,MAAM,MAAM,GAAW,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,GAAkC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnE,yDAAyD;AACzD,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAEpC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAEnC,+EAA+E;AAC/E,MAAM,UAAU,YAAY;IAC1B,wDAAwD;IACxD,MAAM,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkGX,CAAC,CAAC;IAEH,qEAAqE;IACrE,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,yEAAyE,CAC1E,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,6CAA6C;IAC/C,CAAC;IAED,sFAAsF;IACtF,MAAM,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;GAwBX,CAAC,CAAC;IAEH,+EAA+E;IAC/E,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,sEAAsE,CACvE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,+DAA+D,CAChE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;IAED,mEAAmE;IACnE,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,uEAAuE,CACxE,CAAC;QAEF,6EAA6E;QAC7E,MAAM,CAAC,IAAI,CAAC;;;;;;;;;KASX,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;IAED,uDAAuD;IACvD,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CACT,kEAAkE,CACnE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,YAAY,EAAE,CAAC;AAIf,MAAM,EAAE,GAEJ,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAEhC,eAAe,EAAE,CAAC"}
|
package/dist/grpc-service.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
+
/** Build a JSON string of MCP server configs for the PowerLine SpawnRequest. */
|
|
3
|
+
export declare function buildMcpServersJson(mcpServers: {
|
|
4
|
+
name: string;
|
|
5
|
+
command: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
tools?: string[];
|
|
8
|
+
}[]): string;
|
|
2
9
|
/** Register all Grackle gRPC service handlers on the given ConnectRPC router. */
|
|
3
10
|
export declare function registerGrackleRoutes(router: ConnectRouter): void;
|
|
4
11
|
//# sourceMappingURL=grpc-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grpc-service.d.ts","sourceRoot":"","sources":["../src/grpc-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"grpc-service.d.ts","sourceRoot":"","sources":["../src/grpc-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAsMzD,gFAAgF;AAChF,wBAAgB,mBAAmB,CACjC,UAAU,EAAE;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,EAAE,GACF,MAAM,CAUR;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAy1BjE"}
|
package/dist/grpc-service.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as tokenBroker from "./token-broker.js";
|
|
|
10
10
|
import * as projectStore from "./project-store.js";
|
|
11
11
|
import * as taskStore from "./task-store.js";
|
|
12
12
|
import * as findingStore from "./finding-store.js";
|
|
13
|
+
import * as personaStore from "./persona-store.js";
|
|
13
14
|
import { broadcast } from "./ws-broadcast.js";
|
|
14
15
|
import { processEventStream } from "./event-processor.js";
|
|
15
16
|
import { join } from "node:path";
|
|
@@ -82,12 +83,87 @@ function taskRowToProto(row, childIds) {
|
|
|
82
83
|
sortOrder: row.sortOrder,
|
|
83
84
|
parentTaskId: row.parentTaskId,
|
|
84
85
|
depth: row.depth,
|
|
85
|
-
childTaskIds: childIds ?? taskStore.getChildren(row.id).map(c => c.id),
|
|
86
|
+
childTaskIds: childIds ?? taskStore.getChildren(row.id).map((c) => c.id),
|
|
86
87
|
canDecompose: row.canDecompose,
|
|
88
|
+
personaId: row.personaId,
|
|
87
89
|
});
|
|
88
90
|
}
|
|
89
91
|
function findingRowToProto(row) {
|
|
90
|
-
return create(grackle.FindingSchema, {
|
|
92
|
+
return create(grackle.FindingSchema, {
|
|
93
|
+
...row,
|
|
94
|
+
tags: safeParseJsonArray(row.tags),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Safely parse a JSON string, returning the fallback value on failure. */
|
|
98
|
+
function safeParseJson(value, fallback) {
|
|
99
|
+
if (!value) {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(value);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return fallback;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Convert a persona database row to a Persona proto message. */
|
|
110
|
+
function personaRowToProto(row) {
|
|
111
|
+
const toolConfig = safeParseJson(row.toolConfig, {});
|
|
112
|
+
const mcpServers = safeParseJson(row.mcpServers, []);
|
|
113
|
+
return create(grackle.PersonaSchema, {
|
|
114
|
+
id: row.id,
|
|
115
|
+
name: row.name,
|
|
116
|
+
description: row.description,
|
|
117
|
+
systemPrompt: row.systemPrompt,
|
|
118
|
+
toolConfig: create(grackle.ToolConfigSchema, {
|
|
119
|
+
allowedTools: Array.isArray(toolConfig.allowedTools)
|
|
120
|
+
? toolConfig.allowedTools.filter((t) => typeof t === "string")
|
|
121
|
+
: [],
|
|
122
|
+
disallowedTools: Array.isArray(toolConfig.disallowedTools)
|
|
123
|
+
? toolConfig.disallowedTools.filter((t) => typeof t === "string")
|
|
124
|
+
: [],
|
|
125
|
+
}),
|
|
126
|
+
runtime: row.runtime,
|
|
127
|
+
model: row.model,
|
|
128
|
+
maxTurns: row.maxTurns,
|
|
129
|
+
mcpServers: mcpServers
|
|
130
|
+
.filter((s) => typeof s === "object" &&
|
|
131
|
+
s !== null &&
|
|
132
|
+
typeof s.name === "string" &&
|
|
133
|
+
typeof s.command === "string")
|
|
134
|
+
.map((s) => create(grackle.McpServerConfigSchema, {
|
|
135
|
+
name: s.name,
|
|
136
|
+
command: s.command,
|
|
137
|
+
args: Array.isArray(s.args)
|
|
138
|
+
? s.args.filter((a) => typeof a === "string")
|
|
139
|
+
: [],
|
|
140
|
+
tools: Array.isArray(s.tools)
|
|
141
|
+
? s.tools.filter((t) => typeof t === "string")
|
|
142
|
+
: [],
|
|
143
|
+
})),
|
|
144
|
+
createdAt: row.createdAt,
|
|
145
|
+
updatedAt: row.updatedAt,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Convert persona MCP server configs to a JSON string for the PowerLine SpawnRequest. */
|
|
149
|
+
function personaMcpServersToJson(row) {
|
|
150
|
+
const mcpServers = JSON.parse(row.mcpServers || "[]");
|
|
151
|
+
if (mcpServers.length === 0) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
return buildMcpServersJson(mcpServers);
|
|
155
|
+
}
|
|
156
|
+
/** Build a JSON string of MCP server configs for the PowerLine SpawnRequest. */
|
|
157
|
+
export function buildMcpServersJson(mcpServers) {
|
|
158
|
+
const obj = {};
|
|
159
|
+
for (const s of mcpServers) {
|
|
160
|
+
obj[s.name] = {
|
|
161
|
+
command: s.command,
|
|
162
|
+
args: s.args || [],
|
|
163
|
+
...(s.tools && s.tools.length > 0 ? { tools: s.tools } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return JSON.stringify(obj);
|
|
91
167
|
}
|
|
92
168
|
/** Register all Grackle gRPC service handlers on the given ConnectRPC router. */
|
|
93
169
|
export function registerGrackleRoutes(router) {
|
|
@@ -114,14 +190,19 @@ export function registerGrackleRoutes(router) {
|
|
|
114
190
|
try {
|
|
115
191
|
await adapter.disconnect(req.id);
|
|
116
192
|
}
|
|
117
|
-
catch {
|
|
193
|
+
catch {
|
|
194
|
+
/* best-effort */
|
|
195
|
+
}
|
|
118
196
|
}
|
|
119
197
|
}
|
|
120
198
|
adapterManager.removeConnection(req.id);
|
|
121
199
|
// Delete sessions referencing this environment (FK constraint)
|
|
122
200
|
sessionStore.deleteByEnvironment(req.id);
|
|
123
201
|
envRegistry.removeEnvironment(req.id);
|
|
124
|
-
broadcast({
|
|
202
|
+
broadcast({
|
|
203
|
+
type: "environment_removed",
|
|
204
|
+
payload: { environmentId: req.id },
|
|
205
|
+
});
|
|
125
206
|
return create(grackle.EmptySchema, {});
|
|
126
207
|
},
|
|
127
208
|
async *provisionEnvironment(req) {
|
|
@@ -210,22 +291,42 @@ export function registerGrackleRoutes(router) {
|
|
|
210
291
|
if (!conn) {
|
|
211
292
|
throw new Error(`Environment ${req.environmentId} not connected`);
|
|
212
293
|
}
|
|
294
|
+
// Resolve persona if specified
|
|
295
|
+
const persona = req.personaId
|
|
296
|
+
? personaStore.getPersona(req.personaId)
|
|
297
|
+
: undefined;
|
|
298
|
+
if (req.personaId && !persona) {
|
|
299
|
+
throw new Error(`Persona not found: ${req.personaId}`);
|
|
300
|
+
}
|
|
213
301
|
const sessionId = uuid();
|
|
214
|
-
const runtime = req.runtime || env.defaultRuntime;
|
|
215
|
-
const model = req.model ||
|
|
302
|
+
const runtime = req.runtime || persona?.runtime || env.defaultRuntime;
|
|
303
|
+
const model = req.model ||
|
|
304
|
+
persona?.model ||
|
|
305
|
+
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
306
|
+
DEFAULT_MODEL;
|
|
216
307
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
308
|
+
let systemContext = req.systemContext || "";
|
|
309
|
+
if (persona) {
|
|
310
|
+
systemContext =
|
|
311
|
+
persona.systemPrompt + (systemContext ? "\n\n" + systemContext : "");
|
|
312
|
+
}
|
|
217
313
|
sessionStore.createSession(sessionId, req.environmentId, runtime, req.prompt, model, logPath);
|
|
314
|
+
const mcpServersJson = persona ? personaMcpServersToJson(persona) : "";
|
|
218
315
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
219
316
|
sessionId,
|
|
220
317
|
runtime,
|
|
221
318
|
prompt: req.prompt,
|
|
222
319
|
model,
|
|
223
|
-
maxTurns: 0,
|
|
320
|
+
maxTurns: persona?.maxTurns || 0,
|
|
224
321
|
branch: req.branch || "",
|
|
225
322
|
worktreeBasePath: req.branch ? "/workspace" : "",
|
|
226
|
-
systemContext
|
|
323
|
+
systemContext,
|
|
324
|
+
mcpServersJson,
|
|
325
|
+
});
|
|
326
|
+
processEventStream(conn.client.spawn(powerlineReq), {
|
|
327
|
+
sessionId,
|
|
328
|
+
logPath,
|
|
227
329
|
});
|
|
228
|
-
processEventStream(conn.client.spawn(powerlineReq), { sessionId, logPath });
|
|
229
330
|
const row = sessionStore.getSession(sessionId);
|
|
230
331
|
return sessionRowToProto(row);
|
|
231
332
|
},
|
|
@@ -244,7 +345,10 @@ export function registerGrackleRoutes(router) {
|
|
|
244
345
|
runtime: session.runtime,
|
|
245
346
|
});
|
|
246
347
|
const logPath = session.logPath || join(grackleHome, LOGS_DIR, session.id);
|
|
247
|
-
processEventStream(conn.client.resume(powerlineReq), {
|
|
348
|
+
processEventStream(conn.client.resume(powerlineReq), {
|
|
349
|
+
sessionId: session.id,
|
|
350
|
+
logPath,
|
|
351
|
+
});
|
|
248
352
|
const row = sessionStore.getSession(session.id);
|
|
249
353
|
return sessionRowToProto(row);
|
|
250
354
|
},
|
|
@@ -374,7 +478,7 @@ export function registerGrackleRoutes(router) {
|
|
|
374
478
|
const rows = taskStore.listTasks(req.id);
|
|
375
479
|
const childIdsMap = taskStore.buildChildIdsMap(rows);
|
|
376
480
|
return create(grackle.TaskListSchema, {
|
|
377
|
-
tasks: rows.map(r => taskRowToProto(r, childIdsMap.get(r.id) ?? [])),
|
|
481
|
+
tasks: rows.map((r) => taskRowToProto(r, childIdsMap.get(r.id) ?? [])),
|
|
378
482
|
});
|
|
379
483
|
},
|
|
380
484
|
async createTask(req) {
|
|
@@ -395,9 +499,12 @@ export function registerGrackleRoutes(router) {
|
|
|
395
499
|
}
|
|
396
500
|
const id = uuid().slice(0, 8);
|
|
397
501
|
const environmentId = req.environmentId || project.defaultEnvironmentId;
|
|
398
|
-
taskStore.createTask(id, req.projectId, req.title, req.description, environmentId, [...req.dependsOn], slugify(project.name), req.parentTaskId, req.canDecompose);
|
|
502
|
+
taskStore.createTask(id, req.projectId, req.title, req.description, environmentId, [...req.dependsOn], slugify(project.name), req.parentTaskId, req.canDecompose, req.personaId);
|
|
399
503
|
const row = taskStore.getTask(id);
|
|
400
|
-
broadcast({
|
|
504
|
+
broadcast({
|
|
505
|
+
type: "task_created",
|
|
506
|
+
payload: { task: row ? { ...row } : null },
|
|
507
|
+
});
|
|
401
508
|
return taskRowToProto(row);
|
|
402
509
|
},
|
|
403
510
|
async getTask(req) {
|
|
@@ -418,7 +525,9 @@ export function registerGrackleRoutes(router) {
|
|
|
418
525
|
}
|
|
419
526
|
reqStatus = converted;
|
|
420
527
|
}
|
|
421
|
-
taskStore.updateTask(req.id, req.title !== "" ? req.title : existing.title, req.description !== "" ? req.description : existing.description, reqStatus, req.environmentId !== "" ? req.environmentId : existing.environmentId, req.dependsOn.length > 0
|
|
528
|
+
taskStore.updateTask(req.id, req.title !== "" ? req.title : existing.title, req.description !== "" ? req.description : existing.description, reqStatus, req.environmentId !== "" ? req.environmentId : existing.environmentId, req.dependsOn.length > 0
|
|
529
|
+
? [...req.dependsOn]
|
|
530
|
+
: safeParseJsonArray(existing.dependsOn), req.reviewNotes !== "" ? req.reviewNotes : existing.reviewNotes);
|
|
422
531
|
const row = taskStore.getTask(req.id);
|
|
423
532
|
return taskRowToProto(row);
|
|
424
533
|
},
|
|
@@ -441,27 +550,50 @@ export function registerGrackleRoutes(router) {
|
|
|
441
550
|
const conn = adapterManager.getConnection(environmentId);
|
|
442
551
|
if (!conn)
|
|
443
552
|
throw new Error(`Environment ${environmentId} not connected`);
|
|
553
|
+
// Resolve persona (StartTaskRequest override > task's stored persona)
|
|
554
|
+
const personaId = req.personaId || task.personaId;
|
|
555
|
+
const persona = personaId
|
|
556
|
+
? personaStore.getPersona(personaId)
|
|
557
|
+
: undefined;
|
|
558
|
+
if (personaId && !persona) {
|
|
559
|
+
throw new Error(`Persona not found: ${personaId}`);
|
|
560
|
+
}
|
|
444
561
|
const env = envRegistry.getEnvironment(environmentId);
|
|
445
562
|
const sessionId = uuid();
|
|
446
|
-
const runtime = req.runtime ||
|
|
447
|
-
|
|
563
|
+
const runtime = req.runtime ||
|
|
564
|
+
persona?.runtime ||
|
|
565
|
+
env?.defaultRuntime ||
|
|
566
|
+
DEFAULT_RUNTIME;
|
|
567
|
+
const model = req.model ||
|
|
568
|
+
persona?.model ||
|
|
569
|
+
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
570
|
+
DEFAULT_MODEL;
|
|
571
|
+
const maxTurns = persona?.maxTurns || 0;
|
|
448
572
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
449
|
-
|
|
573
|
+
let systemContext = buildTaskSystemContext(task.title, task.description, task.reviewNotes, task.canDecompose);
|
|
574
|
+
if (persona) {
|
|
575
|
+
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
576
|
+
}
|
|
450
577
|
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath);
|
|
451
578
|
taskStore.setTaskSession(task.id, sessionId);
|
|
452
579
|
taskStore.markTaskStarted(task.id);
|
|
453
|
-
broadcast({
|
|
580
|
+
broadcast({
|
|
581
|
+
type: "task_started",
|
|
582
|
+
payload: { taskId: task.id, sessionId, projectId: task.projectId },
|
|
583
|
+
});
|
|
584
|
+
const mcpServersJson = persona ? personaMcpServersToJson(persona) : "";
|
|
454
585
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
455
586
|
sessionId,
|
|
456
587
|
runtime,
|
|
457
588
|
prompt: task.title,
|
|
458
589
|
model,
|
|
459
|
-
maxTurns
|
|
590
|
+
maxTurns,
|
|
460
591
|
branch: task.branch,
|
|
461
592
|
worktreeBasePath: task.branch ? "/workspace" : "",
|
|
462
593
|
systemContext,
|
|
463
594
|
projectId: task.projectId,
|
|
464
595
|
taskId: task.id,
|
|
596
|
+
mcpServersJson,
|
|
465
597
|
});
|
|
466
598
|
processEventStream(conn.client.spawn(powerlineReq), {
|
|
467
599
|
sessionId,
|
|
@@ -479,7 +611,10 @@ export function registerGrackleRoutes(router) {
|
|
|
479
611
|
else if (sess?.status === "failed") {
|
|
480
612
|
taskStore.markTaskCompleted(task.id, "failed");
|
|
481
613
|
}
|
|
482
|
-
broadcast({
|
|
614
|
+
broadcast({
|
|
615
|
+
type: "task_updated",
|
|
616
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
617
|
+
});
|
|
483
618
|
}
|
|
484
619
|
},
|
|
485
620
|
});
|
|
@@ -498,11 +633,18 @@ export function registerGrackleRoutes(router) {
|
|
|
498
633
|
sessionId: "",
|
|
499
634
|
type: grackle.EventType.SYSTEM,
|
|
500
635
|
timestamp: new Date().toISOString(),
|
|
501
|
-
content: JSON.stringify({
|
|
636
|
+
content: JSON.stringify({
|
|
637
|
+
type: "task_unblocked",
|
|
638
|
+
taskId: t.id,
|
|
639
|
+
title: t.title,
|
|
640
|
+
}),
|
|
502
641
|
raw: "",
|
|
503
642
|
}));
|
|
504
643
|
}
|
|
505
|
-
broadcast({
|
|
644
|
+
broadcast({
|
|
645
|
+
type: "task_approved",
|
|
646
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
647
|
+
});
|
|
506
648
|
const row = taskStore.getTask(task.id);
|
|
507
649
|
return taskRowToProto(row);
|
|
508
650
|
},
|
|
@@ -511,7 +653,10 @@ export function registerGrackleRoutes(router) {
|
|
|
511
653
|
if (!task)
|
|
512
654
|
throw new Error(`Task not found: ${req.id}`);
|
|
513
655
|
taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), req.reviewNotes || "");
|
|
514
|
-
broadcast({
|
|
656
|
+
broadcast({
|
|
657
|
+
type: "task_rejected",
|
|
658
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
659
|
+
});
|
|
515
660
|
const row = taskStore.getTask(task.id);
|
|
516
661
|
return taskRowToProto(row);
|
|
517
662
|
},
|
|
@@ -522,14 +667,107 @@ export function registerGrackleRoutes(router) {
|
|
|
522
667
|
throw new Error("Cannot delete task with children. Delete children first.");
|
|
523
668
|
}
|
|
524
669
|
taskStore.deleteTask(req.id);
|
|
525
|
-
broadcast({
|
|
670
|
+
broadcast({
|
|
671
|
+
type: "task_deleted",
|
|
672
|
+
payload: { taskId: req.id, projectId: task?.projectId },
|
|
673
|
+
});
|
|
674
|
+
return create(grackle.EmptySchema, {});
|
|
675
|
+
},
|
|
676
|
+
// ─── Personas ───────────────────────────────────────────────
|
|
677
|
+
async listPersonas() {
|
|
678
|
+
const rows = personaStore.listPersonas();
|
|
679
|
+
return create(grackle.PersonaListSchema, {
|
|
680
|
+
personas: rows.map(personaRowToProto),
|
|
681
|
+
});
|
|
682
|
+
},
|
|
683
|
+
async createPersona(req) {
|
|
684
|
+
if (!req.name)
|
|
685
|
+
throw new Error("Persona name is required");
|
|
686
|
+
if (!req.systemPrompt)
|
|
687
|
+
throw new Error("Persona system_prompt is required");
|
|
688
|
+
// Enforce unique ID and unique name
|
|
689
|
+
let id = slugify(req.name) || uuid().slice(0, 8);
|
|
690
|
+
if (personaStore.getPersona(id)) {
|
|
691
|
+
id = `${id}-${uuid().slice(0, 4)}`;
|
|
692
|
+
}
|
|
693
|
+
if (personaStore.getPersonaByName(req.name)) {
|
|
694
|
+
throw new Error(`Persona with name "${req.name}" already exists`);
|
|
695
|
+
}
|
|
696
|
+
const toolConfigJson = JSON.stringify({
|
|
697
|
+
allowedTools: [...(req.toolConfig?.allowedTools || [])],
|
|
698
|
+
disallowedTools: [...(req.toolConfig?.disallowedTools || [])],
|
|
699
|
+
});
|
|
700
|
+
const mcpServersJson = JSON.stringify(req.mcpServers.map((s) => ({
|
|
701
|
+
name: s.name,
|
|
702
|
+
command: s.command,
|
|
703
|
+
args: [...s.args],
|
|
704
|
+
tools: [...s.tools],
|
|
705
|
+
})));
|
|
706
|
+
personaStore.createPersona(id, req.name, req.description, req.systemPrompt, toolConfigJson, req.runtime, req.model, req.maxTurns, mcpServersJson);
|
|
707
|
+
broadcast({ type: "persona_created", payload: { personaId: id } });
|
|
708
|
+
const row = personaStore.getPersona(id);
|
|
709
|
+
return personaRowToProto(row);
|
|
710
|
+
},
|
|
711
|
+
async getPersona(req) {
|
|
712
|
+
const row = personaStore.getPersona(req.id);
|
|
713
|
+
if (!row)
|
|
714
|
+
throw new Error(`Persona not found: ${req.id}`);
|
|
715
|
+
return personaRowToProto(row);
|
|
716
|
+
},
|
|
717
|
+
async updatePersona(req) {
|
|
718
|
+
const existing = personaStore.getPersona(req.id);
|
|
719
|
+
if (!existing)
|
|
720
|
+
throw new Error(`Persona not found: ${req.id}`);
|
|
721
|
+
// Only update toolConfig/mcpServers if the request provides non-empty values;
|
|
722
|
+
// otherwise keep the existing stored value.
|
|
723
|
+
const hasNewToolConfig = !!req.toolConfig &&
|
|
724
|
+
((req.toolConfig.allowedTools &&
|
|
725
|
+
req.toolConfig.allowedTools.length > 0) ||
|
|
726
|
+
(req.toolConfig.disallowedTools &&
|
|
727
|
+
req.toolConfig.disallowedTools.length > 0));
|
|
728
|
+
const toolConfigJson = hasNewToolConfig
|
|
729
|
+
? JSON.stringify({
|
|
730
|
+
allowedTools: [...(req.toolConfig?.allowedTools || [])],
|
|
731
|
+
disallowedTools: [...(req.toolConfig?.disallowedTools || [])],
|
|
732
|
+
})
|
|
733
|
+
: existing.toolConfig;
|
|
734
|
+
const hasNewMcpServers = Array.isArray(req.mcpServers) && req.mcpServers.length > 0;
|
|
735
|
+
const mcpServersJson = hasNewMcpServers
|
|
736
|
+
? JSON.stringify(req.mcpServers.map((s) => ({
|
|
737
|
+
name: s.name,
|
|
738
|
+
command: s.command,
|
|
739
|
+
args: [...s.args],
|
|
740
|
+
tools: [...s.tools],
|
|
741
|
+
})))
|
|
742
|
+
: existing.mcpServers;
|
|
743
|
+
// Treat empty string / 0 as "not set" and keep existing value
|
|
744
|
+
const name = req.name || existing.name;
|
|
745
|
+
if (name !== existing.name && personaStore.getPersonaByName(name)) {
|
|
746
|
+
throw new Error(`Persona with name "${name}" already exists`);
|
|
747
|
+
}
|
|
748
|
+
const description = req.description || existing.description;
|
|
749
|
+
const systemPrompt = req.systemPrompt || existing.systemPrompt;
|
|
750
|
+
const runtime = req.runtime || existing.runtime;
|
|
751
|
+
const model = req.model || existing.model;
|
|
752
|
+
const maxTurns = req.maxTurns === 0 ? existing.maxTurns : req.maxTurns;
|
|
753
|
+
personaStore.updatePersona(req.id, name, description, systemPrompt, toolConfigJson, runtime, model, maxTurns, mcpServersJson);
|
|
754
|
+
broadcast({ type: "persona_updated", payload: { personaId: req.id } });
|
|
755
|
+
const row = personaStore.getPersona(req.id);
|
|
756
|
+
return personaRowToProto(row);
|
|
757
|
+
},
|
|
758
|
+
async deletePersona(req) {
|
|
759
|
+
personaStore.deletePersona(req.id);
|
|
760
|
+
broadcast({ type: "persona_deleted", payload: { personaId: req.id } });
|
|
526
761
|
return create(grackle.EmptySchema, {});
|
|
527
762
|
},
|
|
528
763
|
// ─── Findings ────────────────────────────────────────────
|
|
529
764
|
async postFinding(req) {
|
|
530
765
|
const id = uuid().slice(0, 8);
|
|
531
766
|
findingStore.postFinding(id, req.projectId, req.taskId, req.sessionId, req.category, req.title, req.content, [...req.tags]);
|
|
532
|
-
broadcast({
|
|
767
|
+
broadcast({
|
|
768
|
+
type: "finding_posted",
|
|
769
|
+
payload: { projectId: req.projectId, findingId: id },
|
|
770
|
+
});
|
|
533
771
|
const rows = findingStore.queryFindings(req.projectId);
|
|
534
772
|
const row = rows.find((r) => r.id === id);
|
|
535
773
|
return findingRowToProto(row);
|
|
@@ -556,7 +794,8 @@ export function registerGrackleRoutes(router) {
|
|
|
556
794
|
throw new Error(`Task not found: ${req.taskId}`);
|
|
557
795
|
if (!task.branch)
|
|
558
796
|
throw new Error("Task has no branch");
|
|
559
|
-
const environmentId = task.environmentId ||
|
|
797
|
+
const environmentId = task.environmentId ||
|
|
798
|
+
projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
560
799
|
if (!environmentId)
|
|
561
800
|
throw new Error("No environment for task");
|
|
562
801
|
const conn = adapterManager.getConnection(environmentId);
|