@grackle-ai/server 0.14.10 → 0.15.1
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 +8 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +310 -29
- 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 +3 -3
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +43 -19
- package/dist/task-store.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +441 -69
- 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
|
-
import type
|
|
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,
|
|
1
|
+
{"version":3,"file":"grpc-service.d.ts","sourceRoot":"","sources":["../src/grpc-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA4M7E,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,CAm5BjE"}
|
package/dist/grpc-service.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
1
2
|
import { create } from "@bufbuild/protobuf";
|
|
2
3
|
import { grackle, powerline } from "@grackle-ai/common";
|
|
3
4
|
import { v4 as uuid } from "uuid";
|
|
@@ -10,12 +11,14 @@ import * as tokenBroker from "./token-broker.js";
|
|
|
10
11
|
import * as projectStore from "./project-store.js";
|
|
11
12
|
import * as taskStore from "./task-store.js";
|
|
12
13
|
import * as findingStore from "./finding-store.js";
|
|
14
|
+
import * as personaStore from "./persona-store.js";
|
|
13
15
|
import { broadcast } from "./ws-broadcast.js";
|
|
14
16
|
import { processEventStream } from "./event-processor.js";
|
|
15
17
|
import { join } from "node:path";
|
|
16
18
|
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, MAX_TASK_DEPTH, taskStatusToEnum, taskStatusToString, projectStatusToEnum, } from "@grackle-ai/common";
|
|
17
19
|
import { grackleHome } from "./paths.js";
|
|
18
20
|
import { safeParseJsonArray } from "./json-helpers.js";
|
|
21
|
+
import { logger } from "./logger.js";
|
|
19
22
|
import { slugify } from "./utils/slugify.js";
|
|
20
23
|
import { buildTaskSystemContext } from "./utils/system-context.js";
|
|
21
24
|
import { importGitHubIssues as executeGitHubImport } from "./github-import.js";
|
|
@@ -82,12 +85,87 @@ function taskRowToProto(row, childIds) {
|
|
|
82
85
|
sortOrder: row.sortOrder,
|
|
83
86
|
parentTaskId: row.parentTaskId,
|
|
84
87
|
depth: row.depth,
|
|
85
|
-
childTaskIds: childIds ?? taskStore.getChildren(row.id).map(c => c.id),
|
|
88
|
+
childTaskIds: childIds ?? taskStore.getChildren(row.id).map((c) => c.id),
|
|
86
89
|
canDecompose: row.canDecompose,
|
|
90
|
+
personaId: row.personaId,
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
function findingRowToProto(row) {
|
|
90
|
-
return create(grackle.FindingSchema, {
|
|
94
|
+
return create(grackle.FindingSchema, {
|
|
95
|
+
...row,
|
|
96
|
+
tags: safeParseJsonArray(row.tags),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Safely parse a JSON string, returning the fallback value on failure. */
|
|
100
|
+
function safeParseJson(value, fallback) {
|
|
101
|
+
if (!value) {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(value);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Convert a persona database row to a Persona proto message. */
|
|
112
|
+
function personaRowToProto(row) {
|
|
113
|
+
const toolConfig = safeParseJson(row.toolConfig, {});
|
|
114
|
+
const mcpServers = safeParseJson(row.mcpServers, []);
|
|
115
|
+
return create(grackle.PersonaSchema, {
|
|
116
|
+
id: row.id,
|
|
117
|
+
name: row.name,
|
|
118
|
+
description: row.description,
|
|
119
|
+
systemPrompt: row.systemPrompt,
|
|
120
|
+
toolConfig: create(grackle.ToolConfigSchema, {
|
|
121
|
+
allowedTools: Array.isArray(toolConfig.allowedTools)
|
|
122
|
+
? toolConfig.allowedTools.filter((t) => typeof t === "string")
|
|
123
|
+
: [],
|
|
124
|
+
disallowedTools: Array.isArray(toolConfig.disallowedTools)
|
|
125
|
+
? toolConfig.disallowedTools.filter((t) => typeof t === "string")
|
|
126
|
+
: [],
|
|
127
|
+
}),
|
|
128
|
+
runtime: row.runtime,
|
|
129
|
+
model: row.model,
|
|
130
|
+
maxTurns: row.maxTurns,
|
|
131
|
+
mcpServers: mcpServers
|
|
132
|
+
.filter((s) => typeof s === "object" &&
|
|
133
|
+
s !== null &&
|
|
134
|
+
typeof s.name === "string" &&
|
|
135
|
+
typeof s.command === "string")
|
|
136
|
+
.map((s) => create(grackle.McpServerConfigSchema, {
|
|
137
|
+
name: s.name,
|
|
138
|
+
command: s.command,
|
|
139
|
+
args: Array.isArray(s.args)
|
|
140
|
+
? s.args.filter((a) => typeof a === "string")
|
|
141
|
+
: [],
|
|
142
|
+
tools: Array.isArray(s.tools)
|
|
143
|
+
? s.tools.filter((t) => typeof t === "string")
|
|
144
|
+
: [],
|
|
145
|
+
})),
|
|
146
|
+
createdAt: row.createdAt,
|
|
147
|
+
updatedAt: row.updatedAt,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** Convert persona MCP server configs to a JSON string for the PowerLine SpawnRequest. */
|
|
151
|
+
function personaMcpServersToJson(row) {
|
|
152
|
+
const mcpServers = JSON.parse(row.mcpServers || "[]");
|
|
153
|
+
if (mcpServers.length === 0) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
return buildMcpServersJson(mcpServers);
|
|
157
|
+
}
|
|
158
|
+
/** Build a JSON string of MCP server configs for the PowerLine SpawnRequest. */
|
|
159
|
+
export function buildMcpServersJson(mcpServers) {
|
|
160
|
+
const obj = {};
|
|
161
|
+
for (const s of mcpServers) {
|
|
162
|
+
obj[s.name] = {
|
|
163
|
+
command: s.command,
|
|
164
|
+
args: s.args || [],
|
|
165
|
+
...(s.tools && s.tools.length > 0 ? { tools: s.tools } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return JSON.stringify(obj);
|
|
91
169
|
}
|
|
92
170
|
/** Register all Grackle gRPC service handlers on the given ConnectRPC router. */
|
|
93
171
|
export function registerGrackleRoutes(router) {
|
|
@@ -114,14 +192,19 @@ export function registerGrackleRoutes(router) {
|
|
|
114
192
|
try {
|
|
115
193
|
await adapter.disconnect(req.id);
|
|
116
194
|
}
|
|
117
|
-
catch {
|
|
195
|
+
catch {
|
|
196
|
+
/* best-effort */
|
|
197
|
+
}
|
|
118
198
|
}
|
|
119
199
|
}
|
|
120
200
|
adapterManager.removeConnection(req.id);
|
|
121
201
|
// Delete sessions referencing this environment (FK constraint)
|
|
122
202
|
sessionStore.deleteByEnvironment(req.id);
|
|
123
203
|
envRegistry.removeEnvironment(req.id);
|
|
124
|
-
broadcast({
|
|
204
|
+
broadcast({
|
|
205
|
+
type: "environment_removed",
|
|
206
|
+
payload: { environmentId: req.id },
|
|
207
|
+
});
|
|
125
208
|
return create(grackle.EmptySchema, {});
|
|
126
209
|
},
|
|
127
210
|
async *provisionEnvironment(req) {
|
|
@@ -210,22 +293,42 @@ export function registerGrackleRoutes(router) {
|
|
|
210
293
|
if (!conn) {
|
|
211
294
|
throw new Error(`Environment ${req.environmentId} not connected`);
|
|
212
295
|
}
|
|
296
|
+
// Resolve persona if specified
|
|
297
|
+
const persona = req.personaId
|
|
298
|
+
? personaStore.getPersona(req.personaId)
|
|
299
|
+
: undefined;
|
|
300
|
+
if (req.personaId && !persona) {
|
|
301
|
+
throw new Error(`Persona not found: ${req.personaId}`);
|
|
302
|
+
}
|
|
213
303
|
const sessionId = uuid();
|
|
214
|
-
const runtime = req.runtime || env.defaultRuntime;
|
|
215
|
-
const model = req.model ||
|
|
304
|
+
const runtime = req.runtime || persona?.runtime || env.defaultRuntime;
|
|
305
|
+
const model = req.model ||
|
|
306
|
+
persona?.model ||
|
|
307
|
+
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
308
|
+
DEFAULT_MODEL;
|
|
216
309
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
310
|
+
let systemContext = req.systemContext || "";
|
|
311
|
+
if (persona) {
|
|
312
|
+
systemContext =
|
|
313
|
+
persona.systemPrompt + (systemContext ? "\n\n" + systemContext : "");
|
|
314
|
+
}
|
|
217
315
|
sessionStore.createSession(sessionId, req.environmentId, runtime, req.prompt, model, logPath);
|
|
316
|
+
const mcpServersJson = persona ? personaMcpServersToJson(persona) : "";
|
|
218
317
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
219
318
|
sessionId,
|
|
220
319
|
runtime,
|
|
221
320
|
prompt: req.prompt,
|
|
222
321
|
model,
|
|
223
|
-
maxTurns: 0,
|
|
322
|
+
maxTurns: persona?.maxTurns || 0,
|
|
224
323
|
branch: req.branch || "",
|
|
225
324
|
worktreeBasePath: req.branch ? "/workspace" : "",
|
|
226
|
-
systemContext
|
|
325
|
+
systemContext,
|
|
326
|
+
mcpServersJson,
|
|
327
|
+
});
|
|
328
|
+
processEventStream(conn.client.spawn(powerlineReq), {
|
|
329
|
+
sessionId,
|
|
330
|
+
logPath,
|
|
227
331
|
});
|
|
228
|
-
processEventStream(conn.client.spawn(powerlineReq), { sessionId, logPath });
|
|
229
332
|
const row = sessionStore.getSession(sessionId);
|
|
230
333
|
return sessionRowToProto(row);
|
|
231
334
|
},
|
|
@@ -244,7 +347,10 @@ export function registerGrackleRoutes(router) {
|
|
|
244
347
|
runtime: session.runtime,
|
|
245
348
|
});
|
|
246
349
|
const logPath = session.logPath || join(grackleHome, LOGS_DIR, session.id);
|
|
247
|
-
processEventStream(conn.client.resume(powerlineReq), {
|
|
350
|
+
processEventStream(conn.client.resume(powerlineReq), {
|
|
351
|
+
sessionId: session.id,
|
|
352
|
+
logPath,
|
|
353
|
+
});
|
|
248
354
|
const row = sessionStore.getSession(session.id);
|
|
249
355
|
return sessionRowToProto(row);
|
|
250
356
|
},
|
|
@@ -374,7 +480,7 @@ export function registerGrackleRoutes(router) {
|
|
|
374
480
|
const rows = taskStore.listTasks(req.id);
|
|
375
481
|
const childIdsMap = taskStore.buildChildIdsMap(rows);
|
|
376
482
|
return create(grackle.TaskListSchema, {
|
|
377
|
-
tasks: rows.map(r => taskRowToProto(r, childIdsMap.get(r.id) ?? [])),
|
|
483
|
+
tasks: rows.map((r) => taskRowToProto(r, childIdsMap.get(r.id) ?? [])),
|
|
378
484
|
});
|
|
379
485
|
},
|
|
380
486
|
async createTask(req) {
|
|
@@ -394,10 +500,23 @@ export function registerGrackleRoutes(router) {
|
|
|
394
500
|
}
|
|
395
501
|
}
|
|
396
502
|
const id = uuid().slice(0, 8);
|
|
397
|
-
|
|
398
|
-
|
|
503
|
+
// Resolve environment: explicit > parent task's env > project default
|
|
504
|
+
let environmentId = req.environmentId;
|
|
505
|
+
if (!environmentId && req.parentTaskId) {
|
|
506
|
+
const parent = taskStore.getTask(req.parentTaskId);
|
|
507
|
+
if (parent?.environmentId) {
|
|
508
|
+
environmentId = parent.environmentId;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!environmentId) {
|
|
512
|
+
environmentId = project.defaultEnvironmentId;
|
|
513
|
+
}
|
|
514
|
+
taskStore.createTask(id, req.projectId, req.title, req.description, environmentId, [...req.dependsOn], slugify(project.name), req.parentTaskId, req.canDecompose, req.personaId);
|
|
399
515
|
const row = taskStore.getTask(id);
|
|
400
|
-
broadcast({
|
|
516
|
+
broadcast({
|
|
517
|
+
type: "task_created",
|
|
518
|
+
payload: { task: row ? { ...row } : null },
|
|
519
|
+
});
|
|
401
520
|
return taskRowToProto(row);
|
|
402
521
|
},
|
|
403
522
|
async getTask(req) {
|
|
@@ -418,7 +537,9 @@ export function registerGrackleRoutes(router) {
|
|
|
418
537
|
}
|
|
419
538
|
reqStatus = converted;
|
|
420
539
|
}
|
|
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
|
|
540
|
+
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
|
|
541
|
+
? [...req.dependsOn]
|
|
542
|
+
: safeParseJsonArray(existing.dependsOn), req.reviewNotes !== "" ? req.reviewNotes : existing.reviewNotes);
|
|
422
543
|
const row = taskStore.getTask(req.id);
|
|
423
544
|
return taskRowToProto(row);
|
|
424
545
|
},
|
|
@@ -441,27 +562,50 @@ export function registerGrackleRoutes(router) {
|
|
|
441
562
|
const conn = adapterManager.getConnection(environmentId);
|
|
442
563
|
if (!conn)
|
|
443
564
|
throw new Error(`Environment ${environmentId} not connected`);
|
|
565
|
+
// Resolve persona (StartTaskRequest override > task's stored persona)
|
|
566
|
+
const personaId = req.personaId || task.personaId;
|
|
567
|
+
const persona = personaId
|
|
568
|
+
? personaStore.getPersona(personaId)
|
|
569
|
+
: undefined;
|
|
570
|
+
if (personaId && !persona) {
|
|
571
|
+
throw new Error(`Persona not found: ${personaId}`);
|
|
572
|
+
}
|
|
444
573
|
const env = envRegistry.getEnvironment(environmentId);
|
|
445
574
|
const sessionId = uuid();
|
|
446
|
-
const runtime = req.runtime ||
|
|
447
|
-
|
|
575
|
+
const runtime = req.runtime ||
|
|
576
|
+
persona?.runtime ||
|
|
577
|
+
env?.defaultRuntime ||
|
|
578
|
+
DEFAULT_RUNTIME;
|
|
579
|
+
const model = req.model ||
|
|
580
|
+
persona?.model ||
|
|
581
|
+
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
582
|
+
DEFAULT_MODEL;
|
|
583
|
+
const maxTurns = persona?.maxTurns || 0;
|
|
448
584
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
449
|
-
|
|
585
|
+
let systemContext = buildTaskSystemContext(task.title, task.description, task.reviewNotes, task.canDecompose);
|
|
586
|
+
if (persona) {
|
|
587
|
+
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
588
|
+
}
|
|
450
589
|
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath);
|
|
451
590
|
taskStore.setTaskSession(task.id, sessionId);
|
|
452
591
|
taskStore.markTaskStarted(task.id);
|
|
453
|
-
broadcast({
|
|
592
|
+
broadcast({
|
|
593
|
+
type: "task_started",
|
|
594
|
+
payload: { taskId: task.id, sessionId, projectId: task.projectId },
|
|
595
|
+
});
|
|
596
|
+
const mcpServersJson = persona ? personaMcpServersToJson(persona) : "";
|
|
454
597
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
455
598
|
sessionId,
|
|
456
599
|
runtime,
|
|
457
600
|
prompt: task.title,
|
|
458
601
|
model,
|
|
459
|
-
maxTurns
|
|
602
|
+
maxTurns,
|
|
460
603
|
branch: task.branch,
|
|
461
604
|
worktreeBasePath: task.branch ? "/workspace" : "",
|
|
462
605
|
systemContext,
|
|
463
606
|
projectId: task.projectId,
|
|
464
607
|
taskId: task.id,
|
|
608
|
+
mcpServersJson,
|
|
465
609
|
});
|
|
466
610
|
processEventStream(conn.client.spawn(powerlineReq), {
|
|
467
611
|
sessionId,
|
|
@@ -479,7 +623,10 @@ export function registerGrackleRoutes(router) {
|
|
|
479
623
|
else if (sess?.status === "failed") {
|
|
480
624
|
taskStore.markTaskCompleted(task.id, "failed");
|
|
481
625
|
}
|
|
482
|
-
broadcast({
|
|
626
|
+
broadcast({
|
|
627
|
+
type: "task_updated",
|
|
628
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
629
|
+
});
|
|
483
630
|
}
|
|
484
631
|
},
|
|
485
632
|
});
|
|
@@ -498,11 +645,18 @@ export function registerGrackleRoutes(router) {
|
|
|
498
645
|
sessionId: "",
|
|
499
646
|
type: grackle.EventType.SYSTEM,
|
|
500
647
|
timestamp: new Date().toISOString(),
|
|
501
|
-
content: JSON.stringify({
|
|
648
|
+
content: JSON.stringify({
|
|
649
|
+
type: "task_unblocked",
|
|
650
|
+
taskId: t.id,
|
|
651
|
+
title: t.title,
|
|
652
|
+
}),
|
|
502
653
|
raw: "",
|
|
503
654
|
}));
|
|
504
655
|
}
|
|
505
|
-
broadcast({
|
|
656
|
+
broadcast({
|
|
657
|
+
type: "task_approved",
|
|
658
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
659
|
+
});
|
|
506
660
|
const row = taskStore.getTask(task.id);
|
|
507
661
|
return taskRowToProto(row);
|
|
508
662
|
},
|
|
@@ -511,25 +665,151 @@ export function registerGrackleRoutes(router) {
|
|
|
511
665
|
if (!task)
|
|
512
666
|
throw new Error(`Task not found: ${req.id}`);
|
|
513
667
|
taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), req.reviewNotes || "");
|
|
514
|
-
broadcast({
|
|
668
|
+
broadcast({
|
|
669
|
+
type: "task_rejected",
|
|
670
|
+
payload: { taskId: task.id, projectId: task.projectId },
|
|
671
|
+
});
|
|
515
672
|
const row = taskStore.getTask(task.id);
|
|
516
673
|
return taskRowToProto(row);
|
|
517
674
|
},
|
|
518
675
|
async deleteTask(req) {
|
|
519
676
|
const task = taskStore.getTask(req.id);
|
|
677
|
+
if (!task) {
|
|
678
|
+
throw new ConnectError(`Task not found: ${req.id}`, Code.NotFound);
|
|
679
|
+
}
|
|
520
680
|
const children = taskStore.getChildren(req.id);
|
|
521
681
|
if (children.length > 0) {
|
|
522
|
-
throw new
|
|
682
|
+
throw new ConnectError("Cannot delete task with children. Delete children first.", Code.FailedPrecondition);
|
|
683
|
+
}
|
|
684
|
+
// Kill active session before deleting the task
|
|
685
|
+
if (task.sessionId) {
|
|
686
|
+
const activeSession = sessionStore.getSession(task.sessionId);
|
|
687
|
+
if (activeSession && (activeSession.status === "running" || activeSession.status === "waiting_input")) {
|
|
688
|
+
const conn = adapterManager.getConnection(activeSession.environmentId);
|
|
689
|
+
if (conn) {
|
|
690
|
+
try {
|
|
691
|
+
await conn.client.kill(create(powerline.SessionIdSchema, { id: task.sessionId }));
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
logger.warn({ taskId: req.id, sessionId: task.sessionId, err }, "Failed to kill session during task deletion");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
sessionStore.updateSession(task.sessionId, "killed");
|
|
698
|
+
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
699
|
+
sessionId: task.sessionId,
|
|
700
|
+
type: grackle.EventType.STATUS,
|
|
701
|
+
timestamp: new Date().toISOString(),
|
|
702
|
+
content: "killed",
|
|
703
|
+
raw: "",
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const changes = taskStore.deleteTask(req.id);
|
|
708
|
+
if (changes === 0) {
|
|
709
|
+
logger.error({ taskId: req.id }, "deleteTask returned 0 changes despite task existing");
|
|
710
|
+
throw new ConnectError(`Failed to delete task ${req.id}: no rows affected`, Code.Internal);
|
|
711
|
+
}
|
|
712
|
+
broadcast({
|
|
713
|
+
type: "task_deleted",
|
|
714
|
+
payload: { taskId: req.id, projectId: task.projectId },
|
|
715
|
+
});
|
|
716
|
+
return create(grackle.EmptySchema, {});
|
|
717
|
+
},
|
|
718
|
+
// ─── Personas ───────────────────────────────────────────────
|
|
719
|
+
async listPersonas() {
|
|
720
|
+
const rows = personaStore.listPersonas();
|
|
721
|
+
return create(grackle.PersonaListSchema, {
|
|
722
|
+
personas: rows.map(personaRowToProto),
|
|
723
|
+
});
|
|
724
|
+
},
|
|
725
|
+
async createPersona(req) {
|
|
726
|
+
if (!req.name)
|
|
727
|
+
throw new Error("Persona name is required");
|
|
728
|
+
if (!req.systemPrompt)
|
|
729
|
+
throw new Error("Persona system_prompt is required");
|
|
730
|
+
// Enforce unique ID and unique name
|
|
731
|
+
let id = slugify(req.name) || uuid().slice(0, 8);
|
|
732
|
+
if (personaStore.getPersona(id)) {
|
|
733
|
+
id = `${id}-${uuid().slice(0, 4)}`;
|
|
523
734
|
}
|
|
524
|
-
|
|
525
|
-
|
|
735
|
+
if (personaStore.getPersonaByName(req.name)) {
|
|
736
|
+
throw new Error(`Persona with name "${req.name}" already exists`);
|
|
737
|
+
}
|
|
738
|
+
const toolConfigJson = JSON.stringify({
|
|
739
|
+
allowedTools: [...(req.toolConfig?.allowedTools || [])],
|
|
740
|
+
disallowedTools: [...(req.toolConfig?.disallowedTools || [])],
|
|
741
|
+
});
|
|
742
|
+
const mcpServersJson = JSON.stringify(req.mcpServers.map((s) => ({
|
|
743
|
+
name: s.name,
|
|
744
|
+
command: s.command,
|
|
745
|
+
args: [...s.args],
|
|
746
|
+
tools: [...s.tools],
|
|
747
|
+
})));
|
|
748
|
+
personaStore.createPersona(id, req.name, req.description, req.systemPrompt, toolConfigJson, req.runtime, req.model, req.maxTurns, mcpServersJson);
|
|
749
|
+
broadcast({ type: "persona_created", payload: { personaId: id } });
|
|
750
|
+
const row = personaStore.getPersona(id);
|
|
751
|
+
return personaRowToProto(row);
|
|
752
|
+
},
|
|
753
|
+
async getPersona(req) {
|
|
754
|
+
const row = personaStore.getPersona(req.id);
|
|
755
|
+
if (!row)
|
|
756
|
+
throw new Error(`Persona not found: ${req.id}`);
|
|
757
|
+
return personaRowToProto(row);
|
|
758
|
+
},
|
|
759
|
+
async updatePersona(req) {
|
|
760
|
+
const existing = personaStore.getPersona(req.id);
|
|
761
|
+
if (!existing)
|
|
762
|
+
throw new Error(`Persona not found: ${req.id}`);
|
|
763
|
+
// Only update toolConfig/mcpServers if the request provides non-empty values;
|
|
764
|
+
// otherwise keep the existing stored value.
|
|
765
|
+
const hasNewToolConfig = !!req.toolConfig &&
|
|
766
|
+
((req.toolConfig.allowedTools &&
|
|
767
|
+
req.toolConfig.allowedTools.length > 0) ||
|
|
768
|
+
(req.toolConfig.disallowedTools &&
|
|
769
|
+
req.toolConfig.disallowedTools.length > 0));
|
|
770
|
+
const toolConfigJson = hasNewToolConfig
|
|
771
|
+
? JSON.stringify({
|
|
772
|
+
allowedTools: [...(req.toolConfig?.allowedTools || [])],
|
|
773
|
+
disallowedTools: [...(req.toolConfig?.disallowedTools || [])],
|
|
774
|
+
})
|
|
775
|
+
: existing.toolConfig;
|
|
776
|
+
const hasNewMcpServers = Array.isArray(req.mcpServers) && req.mcpServers.length > 0;
|
|
777
|
+
const mcpServersJson = hasNewMcpServers
|
|
778
|
+
? JSON.stringify(req.mcpServers.map((s) => ({
|
|
779
|
+
name: s.name,
|
|
780
|
+
command: s.command,
|
|
781
|
+
args: [...s.args],
|
|
782
|
+
tools: [...s.tools],
|
|
783
|
+
})))
|
|
784
|
+
: existing.mcpServers;
|
|
785
|
+
// Treat empty string / 0 as "not set" and keep existing value
|
|
786
|
+
const name = req.name || existing.name;
|
|
787
|
+
if (name !== existing.name && personaStore.getPersonaByName(name)) {
|
|
788
|
+
throw new Error(`Persona with name "${name}" already exists`);
|
|
789
|
+
}
|
|
790
|
+
const description = req.description || existing.description;
|
|
791
|
+
const systemPrompt = req.systemPrompt || existing.systemPrompt;
|
|
792
|
+
const runtime = req.runtime || existing.runtime;
|
|
793
|
+
const model = req.model || existing.model;
|
|
794
|
+
const maxTurns = req.maxTurns === 0 ? existing.maxTurns : req.maxTurns;
|
|
795
|
+
personaStore.updatePersona(req.id, name, description, systemPrompt, toolConfigJson, runtime, model, maxTurns, mcpServersJson);
|
|
796
|
+
broadcast({ type: "persona_updated", payload: { personaId: req.id } });
|
|
797
|
+
const row = personaStore.getPersona(req.id);
|
|
798
|
+
return personaRowToProto(row);
|
|
799
|
+
},
|
|
800
|
+
async deletePersona(req) {
|
|
801
|
+
personaStore.deletePersona(req.id);
|
|
802
|
+
broadcast({ type: "persona_deleted", payload: { personaId: req.id } });
|
|
526
803
|
return create(grackle.EmptySchema, {});
|
|
527
804
|
},
|
|
528
805
|
// ─── Findings ────────────────────────────────────────────
|
|
529
806
|
async postFinding(req) {
|
|
530
807
|
const id = uuid().slice(0, 8);
|
|
531
808
|
findingStore.postFinding(id, req.projectId, req.taskId, req.sessionId, req.category, req.title, req.content, [...req.tags]);
|
|
532
|
-
broadcast({
|
|
809
|
+
broadcast({
|
|
810
|
+
type: "finding_posted",
|
|
811
|
+
payload: { projectId: req.projectId, findingId: id },
|
|
812
|
+
});
|
|
533
813
|
const rows = findingStore.queryFindings(req.projectId);
|
|
534
814
|
const row = rows.find((r) => r.id === id);
|
|
535
815
|
return findingRowToProto(row);
|
|
@@ -556,7 +836,8 @@ export function registerGrackleRoutes(router) {
|
|
|
556
836
|
throw new Error(`Task not found: ${req.taskId}`);
|
|
557
837
|
if (!task.branch)
|
|
558
838
|
throw new Error("Task has no branch");
|
|
559
|
-
const environmentId = task.environmentId ||
|
|
839
|
+
const environmentId = task.environmentId ||
|
|
840
|
+
projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
560
841
|
if (!environmentId)
|
|
561
842
|
throw new Error("No environment for task");
|
|
562
843
|
const conn = adapterManager.getConnection(environmentId);
|