@teammy/server 0.0.8 → 0.0.10
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/README.md +8 -6
- package/dist/line-diff-worker.js +32 -0
- package/dist/main.js +1248 -510
- package/dist/native-pi-entry.js +247 -0
- package/dist/runtime/README.md +21 -2
- package/dist/runtime/shared/system.md +2 -2
- package/dist/runtime/shared/tools.json +11 -3
- package/dist/runtime/workflows/directory-analysis/config.json +6 -7
- package/dist/runtime/workflows/directory-analysis/prompt.md +4 -2
- package/dist/runtime/workflows/file-summary/config.json +4 -6
- package/dist/runtime/workflows/file-summary/prompt.md +2 -2
- package/dist/runtime/workflows/task-bootstrap/config.json +7 -8
- package/dist/runtime/workflows/task-bootstrap/prompt.md +43 -7
- package/dist/web/assets/Dashboard-wKygMUMn.js +1 -0
- package/dist/web/assets/Files-C9kRlKdR.js +1 -0
- package/dist/web/assets/Markdown-DQn2KtR3.js +29 -0
- package/dist/web/assets/Sessions-HD72N7MF.js +1 -0
- package/dist/web/assets/{index-hjEc8Pme.css → index-BgzSGwum.css} +1 -1
- package/dist/web/assets/{index-f4fiI3WA.js → index-Db4kzLA8.js} +6 -6
- package/dist/web/index.html +3 -3
- package/package.json +2 -1
- package/dist/web/assets/Dashboard-CsQ-GV-q.js +0 -1
- package/dist/web/assets/Files-cG0tkAfP.js +0 -1
- package/dist/web/assets/Markdown-Bt-6EDRz.js +0 -29
- package/dist/web/assets/Sessions-B7ABJvRH.js +0 -1
package/dist/main.js
CHANGED
|
@@ -3,6 +3,14 @@ import { createHash as createHash4 } from "node:crypto";
|
|
|
3
3
|
|
|
4
4
|
// apps/server/files.ts
|
|
5
5
|
import { createHash as createHash3 } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
// packages/protocol/browse-order.ts
|
|
8
|
+
var names = new Intl.Collator("zh-CN", { numeric: true, sensitivity: "base" });
|
|
9
|
+
function compareNames(a, b) {
|
|
10
|
+
return names.compare(a, b) || (a < b ? -1 : a > b ? 1 : 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// apps/server/files.ts
|
|
6
14
|
import { join as join2 } from "node:path";
|
|
7
15
|
|
|
8
16
|
// packages/storage/local-blob-store.ts
|
|
@@ -110,10 +118,10 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
|
110
118
|
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;`
|
|
111
119
|
);
|
|
112
120
|
transaction(db, () => {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
121
|
+
const version2 = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
122
|
+
if (version2 > migrations2.length)
|
|
115
123
|
throw new Error("Database was created by a newer Teammy version.");
|
|
116
|
-
for (let index =
|
|
124
|
+
for (let index = version2; index < migrations2.length; index++) {
|
|
117
125
|
db.exec(migrations2[index]);
|
|
118
126
|
db.exec(`PRAGMA user_version = ${index + 1}`);
|
|
119
127
|
}
|
|
@@ -966,7 +974,7 @@ var FileRepository = class {
|
|
|
966
974
|
if (target > head)
|
|
967
975
|
throw new HubError(400, "INVALID_REVISION", "Requested revision has not been published.");
|
|
968
976
|
const prefix = path ? path + "/" : "";
|
|
969
|
-
|
|
977
|
+
let rows = this.hub.db.prepare(
|
|
970
978
|
`
|
|
971
979
|
WITH latest AS (
|
|
972
980
|
SELECT path, MAX(revision) revision FROM file_events
|
|
@@ -979,7 +987,7 @@ var FileRepository = class {
|
|
|
979
987
|
SELECT *,CASE WHEN instr(tail,'/')=0 THEN tail ELSE substr(tail,1,instr(tail,'/')-1) END name FROM live
|
|
980
988
|
)
|
|
981
989
|
SELECT name,MAX(instr(tail,'/')>0) directory,COUNT(*) count,MIN(hash) hash,MIN(size) size,MAX(revision) revision
|
|
982
|
-
FROM children
|
|
990
|
+
FROM children GROUP BY name
|
|
983
991
|
`
|
|
984
992
|
).all(
|
|
985
993
|
identity.workspaceId,
|
|
@@ -989,10 +997,15 @@ var FileRepository = class {
|
|
|
989
997
|
identity.workspaceId,
|
|
990
998
|
owner,
|
|
991
999
|
prefix,
|
|
992
|
-
prefix
|
|
993
|
-
|
|
994
|
-
|
|
1000
|
+
prefix
|
|
1001
|
+
);
|
|
1002
|
+
rows.sort(
|
|
1003
|
+
(a, b) => Number(b.directory) - Number(a.directory) || compareNames(String(a.name), String(b.name))
|
|
995
1004
|
);
|
|
1005
|
+
const offset = after ? rows.findIndex((row) => row.name === after) + 1 : 0;
|
|
1006
|
+
if (after && !offset)
|
|
1007
|
+
throw new HubError(400, "INVALID_CURSOR", "Directory cursor is not in this snapshot.");
|
|
1008
|
+
rows = rows.slice(offset, offset + limit + 1);
|
|
996
1009
|
return {
|
|
997
1010
|
revision: target,
|
|
998
1011
|
path,
|
|
@@ -1089,7 +1102,7 @@ function captureSnapshot(hub, identity, selection, options = {}) {
|
|
|
1089
1102
|
return snapshot;
|
|
1090
1103
|
}
|
|
1091
1104
|
var SnapshotReader = class _SnapshotReader {
|
|
1092
|
-
constructor(hub, identity, snapshot, budget =
|
|
1105
|
+
constructor(hub, identity, snapshot, budget = Number.POSITIVE_INFINITY, sharedBudget) {
|
|
1093
1106
|
this.hub = hub;
|
|
1094
1107
|
this.identity = identity;
|
|
1095
1108
|
this.snapshot = snapshot;
|
|
@@ -1099,8 +1112,6 @@ var SnapshotReader = class _SnapshotReader {
|
|
|
1099
1112
|
identity;
|
|
1100
1113
|
snapshot;
|
|
1101
1114
|
readFiles = /* @__PURE__ */ new Set();
|
|
1102
|
-
lineCounts = /* @__PURE__ */ new Map();
|
|
1103
|
-
readRanges = /* @__PURE__ */ new Map();
|
|
1104
1115
|
limitations = /* @__PURE__ */ new Set();
|
|
1105
1116
|
budget;
|
|
1106
1117
|
commonReader;
|
|
@@ -1116,21 +1127,16 @@ var SnapshotReader = class _SnapshotReader {
|
|
|
1116
1127
|
this.budget
|
|
1117
1128
|
);
|
|
1118
1129
|
}
|
|
1130
|
+
forSnapshot(snapshot) {
|
|
1131
|
+
return new _SnapshotReader(this.hub, this.identity, snapshot, 0, this.budget);
|
|
1132
|
+
}
|
|
1119
1133
|
afterIndex(after) {
|
|
1120
1134
|
if (!after) return 0;
|
|
1121
1135
|
const index = this.snapshot.files.findIndex((f) => f.path === after);
|
|
1122
1136
|
if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
|
|
1123
1137
|
return index + 1;
|
|
1124
1138
|
}
|
|
1125
|
-
|
|
1126
|
-
let end = 0;
|
|
1127
|
-
for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
|
|
1128
|
-
if (range[0] > end + 1) break;
|
|
1129
|
-
end = Math.max(end, range[1]);
|
|
1130
|
-
}
|
|
1131
|
-
return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
|
|
1132
|
-
}
|
|
1133
|
-
list(after = "", limit = 50) {
|
|
1139
|
+
list(after = "", limit = this.snapshot.files.length) {
|
|
1134
1140
|
const entries = this.snapshot.files.slice(
|
|
1135
1141
|
this.afterIndex(after),
|
|
1136
1142
|
this.afterIndex(after) + limit + 1
|
|
@@ -1143,12 +1149,10 @@ var SnapshotReader = class _SnapshotReader {
|
|
|
1143
1149
|
}
|
|
1144
1150
|
source(path) {
|
|
1145
1151
|
const file = this.snapshot.files.find((entry) => entry.path === path);
|
|
1146
|
-
if (!file
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
"Task source must be an actually read snapshot file."
|
|
1151
|
-
);
|
|
1152
|
+
if (!file) {
|
|
1153
|
+
this.limitations.add(`\u6765\u6E90\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${path}`);
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1152
1156
|
const row = this.hub.db.prepare(
|
|
1153
1157
|
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
1154
1158
|
).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
|
|
@@ -1156,45 +1160,35 @@ var SnapshotReader = class _SnapshotReader {
|
|
|
1156
1160
|
throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
|
|
1157
1161
|
return { ...file, syncedAt: String(row.created_at) };
|
|
1158
1162
|
}
|
|
1159
|
-
async
|
|
1163
|
+
async bytes(path) {
|
|
1160
1164
|
const entry = this.snapshot.files.find((f) => f.path === path);
|
|
1161
1165
|
if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
|
|
1162
|
-
if (entry.size > 256 * 1024)
|
|
1163
|
-
throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
|
|
1164
1166
|
const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
|
|
1165
1167
|
if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
1166
1168
|
throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1169
|
+
return bytes;
|
|
1170
|
+
}
|
|
1171
|
+
async text(path) {
|
|
1172
|
+
return new TextDecoder().decode(await this.bytes(path));
|
|
1173
|
+
}
|
|
1174
|
+
consume(size) {
|
|
1175
|
+
if (size > this.budget.remaining) {
|
|
1176
|
+
this.limitations.add("\u672C\u6B21\u5206\u6790\u7684\u7D2F\u8BA1\u6B63\u6587\u8BFB\u53D6\u91CF\u5DF2\u8FBE\u5230\u914D\u7F6E\u989D\u5EA6\u3002");
|
|
1177
|
+
throw new HubError(413, "AI_READ_BUDGET", "Workflow read allowance exceeded.");
|
|
1172
1178
|
}
|
|
1173
|
-
|
|
1174
|
-
throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
|
|
1175
|
-
this.lineCounts.set(path, text.split("\n").length);
|
|
1176
|
-
return text;
|
|
1179
|
+
this.budget.remaining -= size;
|
|
1177
1180
|
}
|
|
1178
|
-
async read(path, startLine = 1, limit
|
|
1181
|
+
async read(path, startLine = 1, limit) {
|
|
1179
1182
|
const text = await this.text(path), lines = text.split("\n");
|
|
1180
1183
|
if (startLine > lines.length)
|
|
1181
1184
|
throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
|
|
1182
|
-
const selected =
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
selected.push(line);
|
|
1188
|
-
size += cost;
|
|
1189
|
-
}
|
|
1190
|
-
if (!selected.length) {
|
|
1191
|
-
this.limitations.add("\u8BFB\u53D6\u9884\u7B97\u6216\u5355\u884C\u957F\u5EA6\u9650\u5236\u963B\u6B62\u4E86\u7EE7\u7EED\u8BFB\u53D6\u3002");
|
|
1192
|
-
throw new HubError(413, "AI_READ_BUDGET", "Read budget exceeded or line is too long.");
|
|
1193
|
-
}
|
|
1194
|
-
this.budget.remaining -= size;
|
|
1185
|
+
const selected = lines.slice(
|
|
1186
|
+
startLine - 1,
|
|
1187
|
+
limit === void 0 ? void 0 : startLine - 1 + limit
|
|
1188
|
+
);
|
|
1189
|
+
this.consume(Buffer.byteLength(selected.join("\n")));
|
|
1195
1190
|
this.readFiles.add(path);
|
|
1196
1191
|
const endLine = startLine + selected.length - 1;
|
|
1197
|
-
this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
|
|
1198
1192
|
return {
|
|
1199
1193
|
path,
|
|
1200
1194
|
startLine,
|
|
@@ -1204,55 +1198,55 @@ var SnapshotReader = class _SnapshotReader {
|
|
|
1204
1198
|
hasMore: endLine < lines.length
|
|
1205
1199
|
};
|
|
1206
1200
|
}
|
|
1207
|
-
async whole(path
|
|
1201
|
+
async whole(path) {
|
|
1208
1202
|
const text = await this.text(path);
|
|
1209
|
-
|
|
1210
|
-
throw new HubError(
|
|
1211
|
-
413,
|
|
1212
|
-
"AI_CONTEXT_LIMIT",
|
|
1213
|
-
"File is too large for this model budget; use directory analysis to read portions."
|
|
1214
|
-
);
|
|
1203
|
+
this.consume(Buffer.byteLength(text));
|
|
1215
1204
|
this.readFiles.add(path);
|
|
1216
|
-
this.readRanges.set(path, [[1, text.split("\n").length]]);
|
|
1217
1205
|
return text;
|
|
1218
1206
|
}
|
|
1219
|
-
async search(query, after = "",
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
for (const file of files) {
|
|
1224
|
-
if (file.size > 256 * 1024) {
|
|
1225
|
-
this.limitations.add("\u90E8\u5206\u5927\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
|
|
1226
|
-
continue;
|
|
1227
|
-
}
|
|
1228
|
-
if (scannedBytes + file.size > 1024 * 1024) {
|
|
1229
|
-
return {
|
|
1230
|
-
matches,
|
|
1231
|
-
nextCursor: files[files.indexOf(file) - 1]?.path ?? after,
|
|
1232
|
-
limited: true
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
scannedBytes += file.size;
|
|
1207
|
+
async search(query, after = "", files = this.snapshot.files) {
|
|
1208
|
+
const fingerprint = snapshotDigest(files) + ":" + this.snapshot.owner + ":" + this.snapshot.revision;
|
|
1209
|
+
let fileIndex = 0, startLine = 1;
|
|
1210
|
+
if (after) {
|
|
1236
1211
|
try {
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1212
|
+
const cursor = JSON.parse(Buffer.from(after, "base64url").toString("utf8"));
|
|
1213
|
+
fileIndex = files.findIndex((file) => file.path === cursor.path);
|
|
1214
|
+
if (fileIndex < 0 || cursor.query !== query || cursor.fingerprint !== fingerprint || !Number.isSafeInteger(cursor.line) || cursor.line < 1)
|
|
1215
|
+
throw new Error();
|
|
1216
|
+
startLine = cursor.line;
|
|
1217
|
+
} catch {
|
|
1218
|
+
throw new HubError(
|
|
1219
|
+
400,
|
|
1220
|
+
"AI_CURSOR_INVALID",
|
|
1221
|
+
"Search cursor does not match this query and snapshot."
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const matches = [];
|
|
1226
|
+
const needle = query.toLowerCase();
|
|
1227
|
+
for (let f = fileIndex; f < files.length; f++) {
|
|
1228
|
+
const file = files[f];
|
|
1229
|
+
const lines = (await this.text(file.path)).split("\n");
|
|
1230
|
+
for (let i = f === fileIndex ? startLine - 1 : 0; i < lines.length; i++) {
|
|
1231
|
+
if (!lines[i].toLowerCase().includes(needle)) continue;
|
|
1232
|
+
if (matches.length === 100) {
|
|
1233
|
+
return {
|
|
1234
|
+
matches,
|
|
1235
|
+
nextCursor: Buffer.from(
|
|
1236
|
+
JSON.stringify({
|
|
1237
|
+
fingerprint,
|
|
1238
|
+
query,
|
|
1239
|
+
path: file.path,
|
|
1240
|
+
line: i + 1
|
|
1241
|
+
})
|
|
1242
|
+
).toString("base64url"),
|
|
1243
|
+
limited: true
|
|
1244
|
+
};
|
|
1247
1245
|
}
|
|
1248
|
-
|
|
1246
|
+
matches.push({ path: file.path, line: i + 1, excerpt: lines[i] });
|
|
1249
1247
|
}
|
|
1250
1248
|
}
|
|
1251
|
-
return {
|
|
1252
|
-
matches,
|
|
1253
|
-
nextCursor: files.length === limit ? files.at(-1).path : null,
|
|
1254
|
-
limited: false
|
|
1255
|
-
};
|
|
1249
|
+
return { matches, nextCursor: null, limited: false };
|
|
1256
1250
|
}
|
|
1257
1251
|
};
|
|
1258
1252
|
|
|
@@ -1438,6 +1432,10 @@ CREATE TABLE ai_defaults (
|
|
|
1438
1432
|
scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
|
|
1439
1433
|
);
|
|
1440
1434
|
`;
|
|
1435
|
+
var aiRetryMigration = `
|
|
1436
|
+
ALTER TABLE ai_jobs ADD COLUMN auto_retry_count INTEGER NOT NULL DEFAULT 0;
|
|
1437
|
+
ALTER TABLE ai_jobs ADD COLUMN next_retry_at INTEGER;
|
|
1438
|
+
`;
|
|
1441
1439
|
|
|
1442
1440
|
// apps/server/store.ts
|
|
1443
1441
|
function digest2(token) {
|
|
@@ -1467,7 +1465,8 @@ var migrations = [
|
|
|
1467
1465
|
equalMembershipMigration,
|
|
1468
1466
|
taskBoardMigration,
|
|
1469
1467
|
taskMaintenanceMigration,
|
|
1470
|
-
sharedFilesMigration
|
|
1468
|
+
sharedFilesMigration,
|
|
1469
|
+
aiRetryMigration
|
|
1471
1470
|
];
|
|
1472
1471
|
var HubStore = class {
|
|
1473
1472
|
constructor(directory) {
|
|
@@ -1476,8 +1475,8 @@ var HubStore = class {
|
|
|
1476
1475
|
if (existsSync(path)) {
|
|
1477
1476
|
const existing = new DatabaseSync2(path, { readOnly: true });
|
|
1478
1477
|
try {
|
|
1479
|
-
const
|
|
1480
|
-
if (
|
|
1478
|
+
const version2 = Number(existing.prepare("PRAGMA user_version").get().user_version);
|
|
1479
|
+
if (version2 > 0 && version2 < 9)
|
|
1481
1480
|
throw new Error(
|
|
1482
1481
|
"Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
|
|
1483
1482
|
);
|
|
@@ -1572,10 +1571,11 @@ import { z as z6 } from "zod";
|
|
|
1572
1571
|
import { z as z5 } from "zod";
|
|
1573
1572
|
var TaskStatusSchema = z5.enum(["planned", "in_progress", "in_review", "completed"]);
|
|
1574
1573
|
var TaskFieldsSchema = z5.object({
|
|
1575
|
-
title: z5.string().trim()
|
|
1576
|
-
goal: z5.string().trim().
|
|
1574
|
+
title: z5.string().trim(),
|
|
1575
|
+
goal: z5.string().trim().nullable(),
|
|
1577
1576
|
status: TaskStatusSchema.nullable(),
|
|
1578
|
-
progress: z5.string().trim()
|
|
1577
|
+
progress: z5.string().trim(),
|
|
1578
|
+
needsConfirmation: z5.boolean().default(false)
|
|
1579
1579
|
}).strict();
|
|
1580
1580
|
var TaskSourceSchema = z5.object({
|
|
1581
1581
|
path: FilePathSchema,
|
|
@@ -1587,14 +1587,17 @@ var TaskCardSchema = TaskFieldsSchema.extend({
|
|
|
1587
1587
|
id: z5.uuid(),
|
|
1588
1588
|
owner: NameSchema,
|
|
1589
1589
|
jobId: z5.uuid(),
|
|
1590
|
-
sources: z5.array(TaskSourceSchema)
|
|
1591
|
-
syncedAt: z5.string(),
|
|
1590
|
+
sources: z5.array(TaskSourceSchema),
|
|
1591
|
+
syncedAt: z5.string().nullable(),
|
|
1592
1592
|
corrected: z5.boolean(),
|
|
1593
1593
|
needsReview: z5.boolean()
|
|
1594
1594
|
});
|
|
1595
1595
|
var TaskCorrectionSchema = z5.object({
|
|
1596
1596
|
title: TaskFieldsSchema.shape.title.optional(),
|
|
1597
|
-
status: TaskStatusSchema.nullable().optional()
|
|
1597
|
+
status: TaskStatusSchema.nullable().optional(),
|
|
1598
|
+
goal: TaskFieldsSchema.shape.goal.optional(),
|
|
1599
|
+
progress: TaskFieldsSchema.shape.progress.optional(),
|
|
1600
|
+
needsConfirmation: z5.boolean().optional()
|
|
1598
1601
|
}).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
|
|
1599
1602
|
var TaskBoardSchema = z5.object({
|
|
1600
1603
|
tasks: z5.array(TaskCardSchema),
|
|
@@ -1605,17 +1608,11 @@ var TaskBoardSchema = z5.object({
|
|
|
1605
1608
|
status: z5.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
|
|
1606
1609
|
error: z5.string().nullable(),
|
|
1607
1610
|
sourceChanged: z5.boolean(),
|
|
1608
|
-
partial: z5.boolean(),
|
|
1609
1611
|
uncertainties: z5.array(z5.string())
|
|
1610
1612
|
})
|
|
1611
1613
|
),
|
|
1612
1614
|
modelAvailable: z5.boolean()
|
|
1613
1615
|
});
|
|
1614
|
-
var TaskBootstrapRequestSchema = z5.object({ operationId: z5.uuid(), owner: NameSchema }).strict();
|
|
1615
|
-
var TaskBootstrapResponseSchema = z5.object({
|
|
1616
|
-
jobId: z5.uuid().nullable(),
|
|
1617
|
-
empty: z5.boolean()
|
|
1618
|
-
});
|
|
1619
1616
|
|
|
1620
1617
|
// packages/protocol/ai.ts
|
|
1621
1618
|
var AiProfileSchema = z6.object({
|
|
@@ -1624,13 +1621,8 @@ var AiProfileSchema = z6.object({
|
|
|
1624
1621
|
protocol: z6.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
|
|
1625
1622
|
baseUrl: ServerUrlSchema,
|
|
1626
1623
|
model: z6.string().trim().min(1).max(200),
|
|
1627
|
-
enabled: z6.boolean().default(true)
|
|
1628
|
-
|
|
1629
|
-
maxOutputTokens: z6.number().int().min(128).max(16e3).default(2048)
|
|
1630
|
-
}).strict().refine(
|
|
1631
|
-
(p) => p.maxOutputTokens < p.contextWindow,
|
|
1632
|
-
"Output budget must be smaller than context window."
|
|
1633
|
-
);
|
|
1624
|
+
enabled: z6.boolean().default(true)
|
|
1625
|
+
}).strict();
|
|
1634
1626
|
var AiPublicProfileSchema = AiProfileSchema.safeExtend({
|
|
1635
1627
|
revision: z6.number().int(),
|
|
1636
1628
|
keyConfigured: z6.boolean()
|
|
@@ -1671,7 +1663,7 @@ var AiJobCreateSchema = z6.object({
|
|
|
1671
1663
|
profileId: NameSchema.optional(),
|
|
1672
1664
|
workflow: z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
|
|
1673
1665
|
selection: AiSelectionSchema,
|
|
1674
|
-
question: z6.string().trim().
|
|
1666
|
+
question: z6.string().trim().default("")
|
|
1675
1667
|
}).strict();
|
|
1676
1668
|
var AiCitationSchema = z6.object({
|
|
1677
1669
|
path: FilePathSchema,
|
|
@@ -1681,26 +1673,31 @@ var AiCitationSchema = z6.object({
|
|
|
1681
1673
|
});
|
|
1682
1674
|
var AiCoverageSchema = z6.object({
|
|
1683
1675
|
totalFiles: z6.number().int(),
|
|
1676
|
+
// Access diagnostics only; reading a file does not establish semantic completeness.
|
|
1684
1677
|
readFiles: z6.array(FilePathSchema),
|
|
1685
|
-
|
|
1678
|
+
accessTracking: z6.literal("unavailable").optional(),
|
|
1686
1679
|
limitations: z6.array(z6.string())
|
|
1687
1680
|
});
|
|
1688
1681
|
var AiResultSchema = z6.object({
|
|
1689
|
-
summary: z6.string()
|
|
1690
|
-
findings: z6.array(z6.string()
|
|
1691
|
-
uncertainties: z6.array(z6.string()
|
|
1692
|
-
citations: z6.array(AiCitationSchema)
|
|
1682
|
+
summary: z6.string(),
|
|
1683
|
+
findings: z6.array(z6.string()),
|
|
1684
|
+
uncertainties: z6.array(z6.string()).default([]),
|
|
1685
|
+
citations: z6.array(AiCitationSchema),
|
|
1693
1686
|
coverage: AiCoverageSchema
|
|
1694
1687
|
});
|
|
1695
1688
|
var TaskAnalysisResultSchema = z6.object({
|
|
1696
1689
|
kind: z6.literal("task-bootstrap"),
|
|
1690
|
+
// Absent for historical full-list results; new workflows publish explicit patches.
|
|
1691
|
+
updateMode: z6.literal("patch").optional(),
|
|
1692
|
+
archive: z6.array(z6.uuid()).optional(),
|
|
1697
1693
|
tasks: z6.array(
|
|
1698
1694
|
TaskFieldsSchema.extend({
|
|
1699
1695
|
taskId: z6.uuid().nullable().optional(),
|
|
1700
|
-
sources: z6.array(TaskSourceSchema)
|
|
1696
|
+
sources: z6.array(TaskSourceSchema)
|
|
1701
1697
|
})
|
|
1702
|
-
)
|
|
1703
|
-
|
|
1698
|
+
),
|
|
1699
|
+
// Read historical result notes without asking new task workflows to generate them.
|
|
1700
|
+
uncertainties: z6.array(z6.string()).optional(),
|
|
1704
1701
|
coverage: AiCoverageSchema
|
|
1705
1702
|
});
|
|
1706
1703
|
var AiWorkflowResultSchema = z6.union([AiResultSchema, TaskAnalysisResultSchema]);
|
|
@@ -1788,11 +1785,18 @@ var AiConfig = class {
|
|
|
1788
1785
|
return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
|
|
1789
1786
|
}
|
|
1790
1787
|
list() {
|
|
1791
|
-
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) =>
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1788
|
+
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => {
|
|
1789
|
+
const {
|
|
1790
|
+
contextWindow: _context,
|
|
1791
|
+
maxOutputTokens: _output,
|
|
1792
|
+
...profile
|
|
1793
|
+
} = JSON.parse(String(row.config_json));
|
|
1794
|
+
return {
|
|
1795
|
+
...AiProfileSchema.parse(profile),
|
|
1796
|
+
revision: Number(row.revision),
|
|
1797
|
+
keyConfigured: !!row.secret
|
|
1798
|
+
};
|
|
1799
|
+
});
|
|
1796
1800
|
}
|
|
1797
1801
|
get(id) {
|
|
1798
1802
|
const profile = this.list().find((p) => p.id === id);
|
|
@@ -1837,8 +1841,8 @@ var AiDefaults = class {
|
|
|
1837
1841
|
this.hub = hub;
|
|
1838
1842
|
}
|
|
1839
1843
|
hub;
|
|
1840
|
-
read(
|
|
1841
|
-
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(
|
|
1844
|
+
read(scope2) {
|
|
1845
|
+
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope2);
|
|
1842
1846
|
return {
|
|
1843
1847
|
profileId: row?.profile_id ? String(row.profile_id) : null,
|
|
1844
1848
|
revision: Number(row?.revision ?? 0)
|
|
@@ -1861,10 +1865,10 @@ var AiDefaults = class {
|
|
|
1861
1865
|
project(projectId2) {
|
|
1862
1866
|
return this.view(projectId2);
|
|
1863
1867
|
}
|
|
1864
|
-
save(
|
|
1868
|
+
save(scope2, value) {
|
|
1865
1869
|
const input = AiDefaultUpdateSchema.parse(value);
|
|
1866
1870
|
transaction(this.hub.db, () => {
|
|
1867
|
-
if (this.read(
|
|
1871
|
+
if (this.read(scope2).revision !== input.expectedRevision)
|
|
1868
1872
|
throw new HubError(
|
|
1869
1873
|
409,
|
|
1870
1874
|
"AI_CONFIG_CONFLICT",
|
|
@@ -1874,7 +1878,7 @@ var AiDefaults = class {
|
|
|
1874
1878
|
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1875
1879
|
this.hub.db.prepare(
|
|
1876
1880
|
`INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
|
|
1877
|
-
).run(
|
|
1881
|
+
).run(scope2, input.profileId, input.expectedRevision + 1);
|
|
1878
1882
|
});
|
|
1879
1883
|
}
|
|
1880
1884
|
saveSite(identity, value) {
|
|
@@ -1905,31 +1909,57 @@ import { resolve as resolve2 } from "node:path";
|
|
|
1905
1909
|
import { fileURLToPath } from "node:url";
|
|
1906
1910
|
import { z as z7 } from "zod";
|
|
1907
1911
|
var WorkflowNameSchema = z7.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
|
|
1908
|
-
var ToolNameSchema = z7.enum([
|
|
1909
|
-
|
|
1912
|
+
var ToolNameSchema = z7.enum([
|
|
1913
|
+
"list_files",
|
|
1914
|
+
"read_file",
|
|
1915
|
+
"search_files",
|
|
1916
|
+
"inspect_changes",
|
|
1917
|
+
"query_tasks",
|
|
1918
|
+
"read",
|
|
1919
|
+
"bash",
|
|
1920
|
+
"grep",
|
|
1921
|
+
"find",
|
|
1922
|
+
"ls"
|
|
1923
|
+
]);
|
|
1924
|
+
var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1) }).strict();
|
|
1910
1925
|
var RuntimeConfigSchema = z7.object({
|
|
1911
1926
|
version: z7.number().int().positive(),
|
|
1927
|
+
execution: z7.enum(["snapshot-tools", "native-sandbox"]).optional(),
|
|
1912
1928
|
thinkingLevel: z7.enum(["off", "minimal", "low", "medium", "high"]),
|
|
1913
|
-
tools: z7.array(ToolNameSchema).
|
|
1914
|
-
maxRounds: z7.number().int().min(1).max(
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
maxReadBytes: z7.number().int().min(1024).max(2e6),
|
|
1929
|
+
tools: z7.array(ToolNameSchema).refine((v) => new Set(v).size === v.length),
|
|
1930
|
+
maxRounds: z7.number().int().min(1).max(384),
|
|
1931
|
+
timeoutMs: z7.number().int().min(1e3).max(21e5),
|
|
1932
|
+
requestTimeoutMs: z7.number().int().min(1e3).max(3e5),
|
|
1933
|
+
maxReadBytes: z7.number().int().positive().max(1024 * 1024 * 1024).optional(),
|
|
1919
1934
|
settleSeconds: z7.number().int().min(0).max(3600).optional()
|
|
1920
|
-
}).strict()
|
|
1935
|
+
}).strict().refine((config) => {
|
|
1936
|
+
const allowed = config.execution === "native-sandbox" ? ["read", "bash", "grep", "find", "ls", "query_tasks"] : ["list_files", "read_file", "search_files", "inspect_changes", "query_tasks"];
|
|
1937
|
+
return config.tools.every((tool) => allowed.includes(tool));
|
|
1938
|
+
}, "Native file tools require the isolated runtime.");
|
|
1921
1939
|
var WorkflowRuntimeSchema = z7.object({
|
|
1922
1940
|
workflow: WorkflowNameSchema,
|
|
1923
|
-
system: z7.string().min(1)
|
|
1924
|
-
prompt: z7.string().min(1)
|
|
1941
|
+
system: z7.string().min(1),
|
|
1942
|
+
prompt: z7.string().min(1),
|
|
1925
1943
|
config: RuntimeConfigSchema,
|
|
1926
1944
|
toolText: z7.object({
|
|
1927
1945
|
list_files: ToolTextSchema,
|
|
1928
1946
|
read_file: ToolTextSchema,
|
|
1929
|
-
search_files: ToolTextSchema
|
|
1947
|
+
search_files: ToolTextSchema,
|
|
1948
|
+
inspect_changes: ToolTextSchema.optional(),
|
|
1949
|
+
query_tasks: ToolTextSchema.optional()
|
|
1930
1950
|
}).strict(),
|
|
1931
1951
|
fingerprint: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
1932
1952
|
}).strict();
|
|
1953
|
+
function withoutLegacyToolLimit(value) {
|
|
1954
|
+
const { maxToolCalls: _removed, ...config } = value;
|
|
1955
|
+
return config;
|
|
1956
|
+
}
|
|
1957
|
+
function parseWorkflowRuntime(value) {
|
|
1958
|
+
return WorkflowRuntimeSchema.parse({
|
|
1959
|
+
...value,
|
|
1960
|
+
config: withoutLegacyToolLimit(value.config)
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1933
1963
|
function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
|
|
1934
1964
|
try {
|
|
1935
1965
|
WorkflowNameSchema.parse(workflow);
|
|
@@ -1939,15 +1969,21 @@ function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
|
|
|
1939
1969
|
workflow,
|
|
1940
1970
|
system: read("shared/system.md"),
|
|
1941
1971
|
prompt: read(`workflows/${workflow}/prompt.md`),
|
|
1942
|
-
config: RuntimeConfigSchema.parse(
|
|
1972
|
+
config: RuntimeConfigSchema.parse(
|
|
1973
|
+
withoutLegacyToolLimit(JSON.parse(read(`workflows/${workflow}/config.json`)))
|
|
1974
|
+
),
|
|
1943
1975
|
toolText: JSON.parse(read("shared/tools.json"))
|
|
1944
1976
|
};
|
|
1945
|
-
|
|
1977
|
+
const native = resource.config.execution === "native-sandbox";
|
|
1978
|
+
if (workflow !== "file-summary" && !resource.config.tools.includes(native ? "read" : "read_file"))
|
|
1946
1979
|
throw new Error("Reader required");
|
|
1947
1980
|
if (workflow === "file-summary" && resource.config.tools.length)
|
|
1948
1981
|
throw new Error("Direct workflow has no tools");
|
|
1949
1982
|
if (workflow === "task-bootstrap" && resource.config.settleSeconds === void 0)
|
|
1950
1983
|
throw new Error("Settle interval required");
|
|
1984
|
+
for (const tool of resource.config.tools)
|
|
1985
|
+
if (!["read", "bash", "grep", "find", "ls"].includes(tool) && !resource.toolText[tool])
|
|
1986
|
+
throw new Error("Missing tool description");
|
|
1951
1987
|
return WorkflowRuntimeSchema.parse({
|
|
1952
1988
|
...resource,
|
|
1953
1989
|
fingerprint: createHash6("sha256").update(JSON.stringify(resource)).digest("hex")
|
|
@@ -2002,35 +2038,49 @@ function applyTaskResult(hub, job, result) {
|
|
|
2002
2038
|
"AI_SOURCE_CHANGED",
|
|
2003
2039
|
"Workspace changed during analysis; analyse the new revision."
|
|
2004
2040
|
);
|
|
2005
|
-
const
|
|
2041
|
+
const patch = result.updateMode === "patch";
|
|
2042
|
+
const previous = hub.db.prepare(
|
|
2043
|
+
"SELECT * FROM task_cards WHERE workspace_id=? AND owner=?" + (patch ? "" : " AND active=1")
|
|
2044
|
+
).all(job.workspace, job.snapshot.owner);
|
|
2045
|
+
const previousById = new Map(previous.map((row) => [String(row.id), row]));
|
|
2046
|
+
const frozenIds = new Set(job.taskContext?.tasks.map((task) => task.taskId));
|
|
2006
2047
|
const used = /* @__PURE__ */ new Set();
|
|
2007
2048
|
const explicit = new Set(result.tasks.flatMap((task) => task.taskId ? [task.taskId] : []));
|
|
2049
|
+
const archive = new Set(result.archive ?? []);
|
|
2050
|
+
if (patch) {
|
|
2051
|
+
const ids = [...explicit, ...archive];
|
|
2052
|
+
if (explicit.size !== result.tasks.filter((task) => task.taskId).length || archive.size !== (result.archive ?? []).length || ids.some(
|
|
2053
|
+
(id) => explicit.has(id) && archive.has(id) || !previousById.has(id) || !frozenIds.has(id)
|
|
2054
|
+
) || result.tasks.some((task) => task.taskId === void 0))
|
|
2055
|
+
throw new HubError(
|
|
2056
|
+
422,
|
|
2057
|
+
"AI_TASK_ID_INVALID",
|
|
2058
|
+
"Task changes must reference unique identities in the frozen member context."
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2008
2061
|
for (const task of result.tasks) {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
);
|
|
2012
|
-
let old = task.taskId ? previous.find((row) => row.id === task.taskId) : void 0;
|
|
2013
|
-
if (task.taskId && (!old || used.has(task.taskId) || !job.taskContext?.tasks.some((item) => item.taskId === task.taskId)))
|
|
2062
|
+
let old = task.taskId ? previousById.get(task.taskId) : void 0;
|
|
2063
|
+
if (task.taskId && (!old || used.has(task.taskId) || !frozenIds.has(task.taskId)))
|
|
2014
2064
|
throw new HubError(
|
|
2015
2065
|
422,
|
|
2016
2066
|
"AI_TASK_ID_INVALID",
|
|
2017
2067
|
"Task identity is outside the frozen member context."
|
|
2018
2068
|
);
|
|
2019
|
-
if (task.taskId === void 0)
|
|
2020
|
-
old =
|
|
2021
|
-
(row) => paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
|
|
2069
|
+
if (!patch && task.taskId === void 0)
|
|
2070
|
+
old = previous.find(
|
|
2071
|
+
(row) => !used.has(String(row.id)) && !explicit.has(String(row.id)) && paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
|
|
2022
2072
|
);
|
|
2023
|
-
if (!old && task.taskId === void 0) {
|
|
2073
|
+
if (!patch && !old && task.taskId === void 0 && task.sources.length) {
|
|
2024
2074
|
const primary = task.sources[0].path;
|
|
2025
|
-
const oldGroup = previous.filter((row) => sources(row)[0]
|
|
2026
|
-
const newGroup = result.tasks.filter((item) => item.sources[0]
|
|
2075
|
+
const oldGroup = previous.filter((row) => sources(row)[0]?.path === primary);
|
|
2076
|
+
const newGroup = result.tasks.filter((item) => item.sources[0]?.path === primary);
|
|
2027
2077
|
if (oldGroup.length === 1 && newGroup.length === 1 && !used.has(String(oldGroup[0].id)) && !explicit.has(String(oldGroup[0].id)))
|
|
2028
2078
|
old = oldGroup[0];
|
|
2029
2079
|
}
|
|
2030
2080
|
const id = old ? String(old.id) : randomUUID2();
|
|
2031
2081
|
used.add(id);
|
|
2032
2082
|
const { sources: source, taskId: _taskId, ...content } = task;
|
|
2033
|
-
const syncedAt = source.map((item) => item.syncedAt).sort().at(-1);
|
|
2083
|
+
const syncedAt = source.map((item) => item.syncedAt).sort().at(-1) ?? "";
|
|
2034
2084
|
hub.db.prepare(
|
|
2035
2085
|
`INSERT INTO task_cards(id,workspace_id,owner,job_id,fields_json,sources_json,synced_at)
|
|
2036
2086
|
VALUES(?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET job_id=excluded.job_id,fields_json=excluded.fields_json,
|
|
@@ -2046,11 +2096,8 @@ function applyTaskResult(hub, job, result) {
|
|
|
2046
2096
|
);
|
|
2047
2097
|
}
|
|
2048
2098
|
for (const old of previous)
|
|
2049
|
-
if (!used.has(String(old.id))) {
|
|
2050
|
-
const
|
|
2051
|
-
(source) => job.snapshot.files.some((file) => file.path === source.path)
|
|
2052
|
-
);
|
|
2053
|
-
const retain = Boolean(old.correction_json) || result.coverage.partial && hasSource;
|
|
2099
|
+
if (patch ? archive.has(String(old.id)) : !used.has(String(old.id))) {
|
|
2100
|
+
const retain = Boolean(old.correction_json);
|
|
2054
2101
|
hub.db.prepare("UPDATE task_cards SET active=?,needs_review=? WHERE id=?").run(retain ? 1 : 0, retain ? 1 : 0, old.id);
|
|
2055
2102
|
}
|
|
2056
2103
|
hub.db.prepare("UPDATE task_board_members SET applied_job_id=? WHERE workspace_id=? AND owner=?").run(job.id, job.workspace, job.snapshot.owner);
|
|
@@ -2068,9 +2115,9 @@ var TaskStore = class {
|
|
|
2068
2115
|
owner: row.owner,
|
|
2069
2116
|
jobId: row.job_id,
|
|
2070
2117
|
sources: sources(row),
|
|
2071
|
-
syncedAt: row.synced_at,
|
|
2118
|
+
syncedAt: row.synced_at || null,
|
|
2072
2119
|
corrected: Boolean(row.correction_json),
|
|
2073
|
-
needsReview: Boolean(row.needs_review)
|
|
2120
|
+
needsReview: Boolean(row.needs_review && row.correction_json)
|
|
2074
2121
|
});
|
|
2075
2122
|
}
|
|
2076
2123
|
board(session) {
|
|
@@ -2097,8 +2144,7 @@ var TaskStore = class {
|
|
|
2097
2144
|
status: row.status ?? (row.has_files ? "waiting" : "empty"),
|
|
2098
2145
|
error: row.error ?? null,
|
|
2099
2146
|
sourceChanged: snapshot ? !taskSnapshotCurrent(this.hub, session.workspaceId, snapshot) : false,
|
|
2100
|
-
|
|
2101
|
-
uncertainties: result ? [...result.uncertainties, ...result.coverage.limitations] : []
|
|
2147
|
+
uncertainties: result ? [...result.uncertainties ?? [], ...result.coverage.limitations] : []
|
|
2102
2148
|
};
|
|
2103
2149
|
}),
|
|
2104
2150
|
modelAvailable: new AiDefaults(this.hub).project(session.workspaceId).available
|
|
@@ -2119,26 +2165,34 @@ var TaskStore = class {
|
|
|
2119
2165
|
};
|
|
2120
2166
|
|
|
2121
2167
|
// apps/server/ai/task-context.ts
|
|
2168
|
+
function changedPaths(beforeFiles, afterFiles) {
|
|
2169
|
+
const before = new Map(beforeFiles.map((file) => [file.path, file.sha256]));
|
|
2170
|
+
const after = new Map(afterFiles.map((file) => [file.path, file.sha256]));
|
|
2171
|
+
return {
|
|
2172
|
+
added: afterFiles.filter((file) => !before.has(file.path)).map((file) => file.path),
|
|
2173
|
+
modified: afterFiles.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
|
|
2174
|
+
deleted: [...before.keys()].filter((path) => !after.has(path))
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2122
2177
|
function captureTaskContext(hub, workspace, snapshot) {
|
|
2123
2178
|
const applied = hub.db.prepare(
|
|
2124
2179
|
`SELECT j.snapshot_json FROM task_board_members b JOIN ai_jobs j ON j.id=b.applied_job_id
|
|
2125
2180
|
WHERE b.workspace_id=? AND b.owner=?`
|
|
2126
2181
|
).get(workspace, snapshot.owner);
|
|
2127
2182
|
const previous = applied ? JSON.parse(String(applied.snapshot_json)) : null;
|
|
2128
|
-
const before = new Map(previous?.files.map((file) => [file.path, file.sha256]));
|
|
2129
|
-
const after = new Map(snapshot.files.map((file) => [file.path, file.sha256]));
|
|
2130
2183
|
return {
|
|
2131
2184
|
previousRevision: previous?.revision ?? null,
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
modified: snapshot.files.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
|
|
2135
|
-
deleted: [...before.keys()].filter((path) => !after.has(path))
|
|
2136
|
-
},
|
|
2185
|
+
baseline: previous,
|
|
2186
|
+
changes: changedPaths(previous?.files ?? [], snapshot.files),
|
|
2137
2187
|
tasks: hub.db.prepare(
|
|
2138
|
-
"SELECT id,fields_json,sources_json FROM task_cards WHERE workspace_id=? AND owner=?
|
|
2188
|
+
"SELECT id,fields_json,correction_json,sources_json,active FROM task_cards WHERE workspace_id=? AND owner=? ORDER BY id"
|
|
2139
2189
|
).all(workspace, snapshot.owner).map((row) => ({
|
|
2140
|
-
...
|
|
2190
|
+
...TaskFieldsSchema.parse({
|
|
2191
|
+
...JSON.parse(String(row.fields_json)),
|
|
2192
|
+
...row.correction_json ? JSON.parse(String(row.correction_json)) : {}
|
|
2193
|
+
}),
|
|
2141
2194
|
taskId: String(row.id),
|
|
2195
|
+
active: Boolean(row.active),
|
|
2142
2196
|
sources: JSON.parse(String(row.sources_json))
|
|
2143
2197
|
}))
|
|
2144
2198
|
};
|
|
@@ -2146,6 +2200,7 @@ function captureTaskContext(hub, workspace, snapshot) {
|
|
|
2146
2200
|
|
|
2147
2201
|
// apps/server/ai/jobs.ts
|
|
2148
2202
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2203
|
+
var retryDelays = [5e3, 15e3, 45e3];
|
|
2149
2204
|
var AiJobs = class {
|
|
2150
2205
|
constructor(hub) {
|
|
2151
2206
|
this.hub = hub;
|
|
@@ -2388,7 +2443,7 @@ var AiJobs = class {
|
|
|
2388
2443
|
this.own(session, row);
|
|
2389
2444
|
if (["queued", "running"].includes(String(row.status))) {
|
|
2390
2445
|
this.hub.db.prepare(
|
|
2391
|
-
"UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL WHERE id=?"
|
|
2446
|
+
"UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
|
|
2392
2447
|
).run(now(), id);
|
|
2393
2448
|
this.hub.db.prepare(
|
|
2394
2449
|
"UPDATE ai_attempts SET status='cancelled',ended_at=? WHERE job_id=? AND status='running'"
|
|
@@ -2408,20 +2463,49 @@ var AiJobs = class {
|
|
|
2408
2463
|
if (!["failed", "cancelled"].includes(String(row.status)))
|
|
2409
2464
|
throw new HubError(409, "AI_TASK_STATE", "Only failed or cancelled tasks can be retried.");
|
|
2410
2465
|
this.hub.db.prepare(
|
|
2411
|
-
"UPDATE ai_jobs SET status='queued',updated_at=?,error=NULL,result_json=NULL,recovery_count=0 WHERE id=?"
|
|
2466
|
+
"UPDATE ai_jobs SET status='queued',updated_at=?,error=NULL,result_json=NULL,recovery_count=0,auto_retry_count=0,next_retry_at=NULL WHERE id=?"
|
|
2412
2467
|
).run(now(), id);
|
|
2413
2468
|
this.event(id, Number(row.attempt_count), "queued", "\u4F7F\u7528\u539F\u8F93\u5165\u5FEB\u7167\u91CD\u8BD5\u3002");
|
|
2414
2469
|
return id;
|
|
2415
2470
|
});
|
|
2416
2471
|
return this.get(session.workspaceId, id);
|
|
2417
2472
|
}
|
|
2473
|
+
cancelStaleTaskJobs() {
|
|
2474
|
+
return transaction(this.hub.db, () => {
|
|
2475
|
+
const rows = this.hub.db.prepare(
|
|
2476
|
+
"SELECT * FROM ai_jobs WHERE status IN ('queued','running') AND json_extract(request_json,'$.workflow')='task-bootstrap'"
|
|
2477
|
+
).all();
|
|
2478
|
+
for (const row of rows) {
|
|
2479
|
+
if (taskSnapshotCurrent(
|
|
2480
|
+
this.hub,
|
|
2481
|
+
String(row.workspace_id),
|
|
2482
|
+
JSON.parse(String(row.snapshot_json))
|
|
2483
|
+
))
|
|
2484
|
+
continue;
|
|
2485
|
+
this.hub.db.prepare(
|
|
2486
|
+
"UPDATE ai_jobs SET status='cancelled',error='AI_SOURCE_CHANGED',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
|
|
2487
|
+
).run(now(), row.id);
|
|
2488
|
+
this.hub.db.prepare(
|
|
2489
|
+
"UPDATE ai_attempts SET status='cancelled',ended_at=?,error='AI_SOURCE_CHANGED' WHERE job_id=? AND status='running'"
|
|
2490
|
+
).run(now(), row.id);
|
|
2491
|
+
this.event(
|
|
2492
|
+
String(row.id),
|
|
2493
|
+
Number(row.attempt_count),
|
|
2494
|
+
"cancelled",
|
|
2495
|
+
"\u8D44\u6599\u5DF2\u66F4\u65B0\uFF0C\u53D6\u6D88\u65E7\u7248\u672C\u5206\u6790\uFF0C\u7B49\u5F85\u65B0\u7248\u672C\u6574\u7406\u3002"
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2418
2500
|
claim(at = Date.now()) {
|
|
2501
|
+
this.cancelStaleTaskJobs();
|
|
2419
2502
|
return transaction(this.hub.db, () => {
|
|
2420
2503
|
for (const old of this.hub.db.prepare("SELECT * FROM ai_jobs WHERE status='running' AND lease_until<=?").all(at)) {
|
|
2421
|
-
const
|
|
2504
|
+
const retry = Number(old.auto_retry_count) < retryDelays.length;
|
|
2505
|
+
const status = retry ? "queued" : "failed";
|
|
2422
2506
|
this.hub.db.prepare(
|
|
2423
|
-
"UPDATE ai_jobs SET status=?,run_token=NULL,lease_until=NULL,error=?,updated_at=?,recovery_count=recovery_count+1 WHERE id=?"
|
|
2424
|
-
).run(status, "AI_WORKER_INTERRUPTED", now(), old.id);
|
|
2507
|
+
"UPDATE ai_jobs SET status=?,run_token=NULL,lease_until=NULL,error=?,updated_at=?,recovery_count=recovery_count+1,auto_retry_count=auto_retry_count+?,next_retry_at=NULL WHERE id=?"
|
|
2508
|
+
).run(status, "AI_WORKER_INTERRUPTED", now(), retry ? 1 : 0, old.id);
|
|
2425
2509
|
this.hub.db.prepare(
|
|
2426
2510
|
"UPDATE ai_attempts SET status='interrupted',ended_at=?,error='AI_WORKER_INTERRUPTED' WHERE job_id=? AND status='running'"
|
|
2427
2511
|
).run(now(), old.id);
|
|
@@ -2434,11 +2518,13 @@ var AiJobs = class {
|
|
|
2434
2518
|
}
|
|
2435
2519
|
if (this.hub.db.prepare("SELECT 1 FROM ai_jobs WHERE status='running'").get())
|
|
2436
2520
|
return void 0;
|
|
2437
|
-
const row = this.hub.db.prepare(
|
|
2521
|
+
const row = this.hub.db.prepare(
|
|
2522
|
+
"SELECT * FROM ai_jobs WHERE status='queued' AND (next_retry_at IS NULL OR next_retry_at<=?) ORDER BY created_at,id LIMIT 1"
|
|
2523
|
+
).get(at);
|
|
2438
2524
|
if (!row) return void 0;
|
|
2439
2525
|
const token = randomUUID3(), attempt = Number(row.attempt_count) + 1, date = now();
|
|
2440
2526
|
this.hub.db.prepare(
|
|
2441
|
-
"UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL WHERE id=?"
|
|
2527
|
+
"UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL,next_retry_at=NULL WHERE id=?"
|
|
2442
2528
|
).run(attempt, token, at + 3e4, date, row.id);
|
|
2443
2529
|
this.hub.db.prepare("INSERT INTO ai_attempts(job_id,number,status,started_at) VALUES(?,?,?,?)").run(row.id, attempt, "running", date);
|
|
2444
2530
|
this.event(String(row.id), attempt, "running", "\u5F00\u59CB\u5206\u6790\u3002");
|
|
@@ -2452,12 +2538,16 @@ var AiJobs = class {
|
|
|
2452
2538
|
profile: JSON.parse(String(row.profile_json)),
|
|
2453
2539
|
snapshot: JSON.parse(String(row.snapshot_json)),
|
|
2454
2540
|
promptVersion: String(row.prompt_version),
|
|
2455
|
-
runtime: row.runtime_json ?
|
|
2541
|
+
runtime: row.runtime_json ? parseWorkflowRuntime(JSON.parse(String(row.runtime_json))) : null,
|
|
2456
2542
|
taskContext: row.task_context_json ? JSON.parse(String(row.task_context_json)) : null
|
|
2457
2543
|
};
|
|
2458
2544
|
});
|
|
2459
2545
|
}
|
|
2460
2546
|
heartbeat(job) {
|
|
2547
|
+
if (job.request.workflow === "task-bootstrap" && !taskSnapshotCurrent(this.hub, job.workspace, job.snapshot)) {
|
|
2548
|
+
this.cancelStaleTaskJobs();
|
|
2549
|
+
return false;
|
|
2550
|
+
}
|
|
2461
2551
|
return this.hub.db.prepare(
|
|
2462
2552
|
"UPDATE ai_jobs SET lease_until=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2463
2553
|
).run(Date.now() + 3e4, job.id, job.token, Date.now()).changes === 1;
|
|
@@ -2478,20 +2568,40 @@ var AiJobs = class {
|
|
|
2478
2568
|
}
|
|
2479
2569
|
finish(job, result, error, usage) {
|
|
2480
2570
|
return transaction(this.hub.db, () => {
|
|
2481
|
-
const
|
|
2571
|
+
const row = this.hub.db.prepare(
|
|
2572
|
+
"SELECT auto_retry_count,runtime_json FROM ai_jobs WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2573
|
+
).get(job.id, job.token, Date.now());
|
|
2574
|
+
if (!row) return false;
|
|
2575
|
+
const obsolete = error === "AI_SOURCE_CHANGED" || error === "AI_TASK_SUPERSEDED";
|
|
2576
|
+
const retryCount = Number(row.auto_retry_count);
|
|
2577
|
+
const retry = !result && !obsolete && retryCount < retryDelays.length;
|
|
2578
|
+
const attemptStatus = result ? "succeeded" : obsolete ? "cancelled" : "failed";
|
|
2579
|
+
const status = retry ? "queued" : attemptStatus;
|
|
2482
2580
|
if (result) {
|
|
2483
2581
|
AiWorkflowResultSchema.parse(result);
|
|
2484
2582
|
const taskResult = "kind" in result;
|
|
2485
2583
|
if (job.request.workflow === "task-bootstrap" !== taskResult)
|
|
2486
2584
|
throw new HubError(422, "AI_RESULT_INVALID", "Result does not match the workflow.");
|
|
2585
|
+
if (taskResult && row.runtime_json) {
|
|
2586
|
+
const runtime = JSON.parse(String(row.runtime_json));
|
|
2587
|
+
const expectsPatch = runtime.config.version >= 7;
|
|
2588
|
+
if (expectsPatch !== (result.updateMode === "patch"))
|
|
2589
|
+
throw new HubError(
|
|
2590
|
+
422,
|
|
2591
|
+
"AI_RESULT_INVALID",
|
|
2592
|
+
"Task result does not match its frozen output contract."
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2487
2595
|
}
|
|
2488
2596
|
const changed = this.hub.db.prepare(
|
|
2489
|
-
"UPDATE ai_jobs SET status=?,result_json=?,error=?,run_token=NULL,lease_until=NULL,updated_at=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2597
|
+
"UPDATE ai_jobs SET status=?,result_json=?,error=?,run_token=NULL,lease_until=NULL,updated_at=?,auto_retry_count=auto_retry_count+?,next_retry_at=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2490
2598
|
).run(
|
|
2491
2599
|
status,
|
|
2492
2600
|
result ? JSON.stringify(result) : null,
|
|
2493
2601
|
error,
|
|
2494
2602
|
now(),
|
|
2603
|
+
retry ? 1 : 0,
|
|
2604
|
+
retry ? Date.now() + retryDelays[retryCount] : null,
|
|
2495
2605
|
job.id,
|
|
2496
2606
|
job.token,
|
|
2497
2607
|
Date.now()
|
|
@@ -2500,8 +2610,27 @@ var AiJobs = class {
|
|
|
2500
2610
|
if (result && "kind" in result) applyTaskResult(this.hub, job, result);
|
|
2501
2611
|
this.hub.db.prepare(
|
|
2502
2612
|
"UPDATE ai_attempts SET status=?,ended_at=?,error=?,usage_json=? WHERE job_id=? AND number=?"
|
|
2503
|
-
).run(
|
|
2504
|
-
|
|
2613
|
+
).run(
|
|
2614
|
+
attemptStatus,
|
|
2615
|
+
now(),
|
|
2616
|
+
error,
|
|
2617
|
+
usage ? JSON.stringify(usage) : null,
|
|
2618
|
+
job.id,
|
|
2619
|
+
job.attempt
|
|
2620
|
+
);
|
|
2621
|
+
this.event(
|
|
2622
|
+
job.id,
|
|
2623
|
+
job.attempt,
|
|
2624
|
+
attemptStatus,
|
|
2625
|
+
result ? "\u5206\u6790\u5B8C\u6210\u3002" : error ?? "\u5206\u6790\u5931\u8D25\u3002"
|
|
2626
|
+
);
|
|
2627
|
+
if (retry)
|
|
2628
|
+
this.event(
|
|
2629
|
+
job.id,
|
|
2630
|
+
job.attempt,
|
|
2631
|
+
"queued",
|
|
2632
|
+
`\u5C06\u5728 ${retryDelays[retryCount] / 1e3} \u79D2\u540E\u81EA\u52A8\u91CD\u8BD5\uFF08${retryCount + 1}/3\uFF09\u3002`
|
|
2633
|
+
);
|
|
2505
2634
|
}
|
|
2506
2635
|
return changed === 1;
|
|
2507
2636
|
});
|
|
@@ -2509,23 +2638,97 @@ var AiJobs = class {
|
|
|
2509
2638
|
};
|
|
2510
2639
|
|
|
2511
2640
|
// apps/server/ai/pi-runner.ts
|
|
2512
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2513
|
-
import { tmpdir } from "node:os";
|
|
2514
|
-
import { join as
|
|
2641
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
|
|
2642
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
2643
|
+
import { join as join7 } from "node:path";
|
|
2515
2644
|
import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi-ai";
|
|
2516
2645
|
|
|
2517
|
-
// apps/server/ai/tools.ts
|
|
2646
|
+
// apps/server/ai/task-tools.ts
|
|
2518
2647
|
import { Type } from "@earendil-works/pi-ai";
|
|
2519
2648
|
import { z as z8 } from "zod";
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2649
|
+
var scope = z8.enum(["member", "project"]).optional();
|
|
2650
|
+
var version = z8.enum(["current", "baseline"]).optional();
|
|
2651
|
+
var filter = {
|
|
2652
|
+
path: z8.string().optional(),
|
|
2653
|
+
query: z8.string().optional(),
|
|
2654
|
+
pattern: z8.string().optional()
|
|
2655
|
+
};
|
|
2656
|
+
function taskTools(explorer, signal, progress, runtime) {
|
|
2657
|
+
function tool(name, schema, execute) {
|
|
2658
|
+
const text = runtime.toolText[name];
|
|
2659
|
+
return {
|
|
2660
|
+
name,
|
|
2661
|
+
...text,
|
|
2662
|
+
parameters: Type.Unsafe(z8.toJSONSchema(schema)),
|
|
2663
|
+
executionMode: "sequential",
|
|
2664
|
+
async execute(_id, params) {
|
|
2665
|
+
signal.throwIfAborted();
|
|
2666
|
+
const parsed = schema.parse(params);
|
|
2667
|
+
progress(name, `${text.label}\u3002`);
|
|
2668
|
+
const value = await execute(parsed);
|
|
2669
|
+
signal.throwIfAborted();
|
|
2670
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }], details: {} };
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
return [
|
|
2675
|
+
tool(
|
|
2676
|
+
"list_files",
|
|
2677
|
+
z8.object({ ...filter, scope, version }).strict(),
|
|
2678
|
+
(p) => explorer.list(p, p.scope, p.version)
|
|
2679
|
+
),
|
|
2680
|
+
tool(
|
|
2681
|
+
"read_file",
|
|
2682
|
+
z8.object({
|
|
2683
|
+
path: FilePathSchema,
|
|
2684
|
+
scope,
|
|
2685
|
+
version,
|
|
2686
|
+
startLine: z8.number().int().positive().optional(),
|
|
2687
|
+
limit: z8.number().int().positive().optional()
|
|
2688
|
+
}).strict(),
|
|
2689
|
+
(p) => explorer.reader(p.scope, p.version).read(p.path, p.startLine, p.limit)
|
|
2690
|
+
),
|
|
2691
|
+
tool(
|
|
2692
|
+
"search_files",
|
|
2693
|
+
z8.object({
|
|
2694
|
+
path: filter.path,
|
|
2695
|
+
pattern: filter.pattern,
|
|
2696
|
+
query: z8.string().min(1),
|
|
2697
|
+
after: z8.string().optional(),
|
|
2698
|
+
scope,
|
|
2699
|
+
version
|
|
2700
|
+
}).strict(),
|
|
2701
|
+
(p) => explorer.search(p.query, p.after, { path: p.path, pattern: p.pattern }, p.scope, p.version)
|
|
2702
|
+
),
|
|
2703
|
+
tool(
|
|
2704
|
+
"inspect_changes",
|
|
2705
|
+
z8.object({
|
|
2706
|
+
...filter,
|
|
2707
|
+
scope,
|
|
2708
|
+
version,
|
|
2709
|
+
startLine: z8.number().int().positive().optional(),
|
|
2710
|
+
endLine: z8.number().int().positive().optional()
|
|
2711
|
+
}).strict(),
|
|
2712
|
+
(p) => explorer.inspect(p, p.scope)
|
|
2713
|
+
),
|
|
2714
|
+
tool(
|
|
2715
|
+
"query_tasks",
|
|
2716
|
+
z8.object({
|
|
2717
|
+
id: z8.uuid().optional(),
|
|
2718
|
+
source: z8.string().optional(),
|
|
2719
|
+
query: z8.string().optional(),
|
|
2720
|
+
visibility: z8.enum(["active", "archived", "all"]).optional()
|
|
2721
|
+
}).strict(),
|
|
2722
|
+
(p) => explorer.queryTasks(p)
|
|
2723
|
+
)
|
|
2724
|
+
].filter((tool2) => runtime.config.tools.some((name) => name === tool2.name));
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// apps/server/ai/tools.ts
|
|
2728
|
+
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
2729
|
+
import { z as z9 } from "zod";
|
|
2730
|
+
function analysisTools(reader, signal, progress, runtime, explorer) {
|
|
2731
|
+
if (explorer) return taskTools(explorer, signal, progress, runtime);
|
|
2529
2732
|
const reply = (value) => ({
|
|
2530
2733
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
2531
2734
|
details: {}
|
|
@@ -2534,44 +2737,44 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2534
2737
|
{
|
|
2535
2738
|
name: "list_files",
|
|
2536
2739
|
...runtime.toolText.list_files,
|
|
2537
|
-
parameters:
|
|
2740
|
+
parameters: Type2.Object(
|
|
2538
2741
|
{
|
|
2539
|
-
after:
|
|
2540
|
-
scope:
|
|
2742
|
+
after: Type2.Optional(Type2.String()),
|
|
2743
|
+
scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")]))
|
|
2541
2744
|
},
|
|
2542
2745
|
{ additionalProperties: false }
|
|
2543
2746
|
),
|
|
2544
2747
|
executionMode: "sequential",
|
|
2545
2748
|
async execute(_id, params) {
|
|
2546
|
-
|
|
2547
|
-
const p =
|
|
2548
|
-
after:
|
|
2549
|
-
scope:
|
|
2749
|
+
signal.throwIfAborted();
|
|
2750
|
+
const p = z9.object({
|
|
2751
|
+
after: z9.string().default(""),
|
|
2752
|
+
scope: z9.enum(["member", "project"]).default("member")
|
|
2550
2753
|
}).strict().parse(params);
|
|
2551
2754
|
progress("list", "\u6D4F\u89C8\u6587\u4EF6\u6E05\u5355\u3002");
|
|
2552
|
-
return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after
|
|
2755
|
+
return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after));
|
|
2553
2756
|
}
|
|
2554
2757
|
},
|
|
2555
2758
|
{
|
|
2556
2759
|
name: "read_file",
|
|
2557
2760
|
...runtime.toolText.read_file,
|
|
2558
|
-
parameters:
|
|
2761
|
+
parameters: Type2.Object(
|
|
2559
2762
|
{
|
|
2560
|
-
path:
|
|
2561
|
-
scope:
|
|
2562
|
-
startLine:
|
|
2563
|
-
limit:
|
|
2763
|
+
path: Type2.String(),
|
|
2764
|
+
scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")])),
|
|
2765
|
+
startLine: Type2.Optional(Type2.Integer({ minimum: 1 })),
|
|
2766
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1 }))
|
|
2564
2767
|
},
|
|
2565
2768
|
{ additionalProperties: false }
|
|
2566
2769
|
),
|
|
2567
2770
|
executionMode: "sequential",
|
|
2568
2771
|
async execute(_id, params) {
|
|
2569
|
-
|
|
2570
|
-
const p =
|
|
2772
|
+
signal.throwIfAborted();
|
|
2773
|
+
const p = z9.object({
|
|
2571
2774
|
path: FilePathSchema,
|
|
2572
|
-
scope:
|
|
2573
|
-
startLine:
|
|
2574
|
-
limit:
|
|
2775
|
+
scope: z9.enum(["member", "project"]).default("member"),
|
|
2776
|
+
startLine: z9.number().int().min(1).default(1),
|
|
2777
|
+
limit: z9.number().int().min(1).optional()
|
|
2575
2778
|
}).strict().parse(params);
|
|
2576
2779
|
progress("read", `\u8BFB\u53D6 ${p.path}\uFF0C\u7B2C ${p.startLine} \u884C\u8D77\u3002`);
|
|
2577
2780
|
return reply(
|
|
@@ -2586,17 +2789,17 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2586
2789
|
{
|
|
2587
2790
|
name: "search_files",
|
|
2588
2791
|
...runtime.toolText.search_files,
|
|
2589
|
-
parameters:
|
|
2792
|
+
parameters: Type2.Object(
|
|
2590
2793
|
{
|
|
2591
|
-
query:
|
|
2592
|
-
after:
|
|
2794
|
+
query: Type2.String({ minLength: 1 }),
|
|
2795
|
+
after: Type2.Optional(Type2.String())
|
|
2593
2796
|
},
|
|
2594
2797
|
{ additionalProperties: false }
|
|
2595
2798
|
),
|
|
2596
2799
|
executionMode: "sequential",
|
|
2597
2800
|
async execute(_id, params) {
|
|
2598
|
-
|
|
2599
|
-
const p =
|
|
2801
|
+
signal.throwIfAborted();
|
|
2802
|
+
const p = z9.object({ query: z9.string().min(1), after: z9.string().default("") }).strict().parse(params);
|
|
2600
2803
|
progress("search", "\u641C\u7D22\u5FEB\u7167\u6B63\u6587\u3002");
|
|
2601
2804
|
return reply(await reader.search(p.query, p.after));
|
|
2602
2805
|
}
|
|
@@ -2606,10 +2809,19 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2606
2809
|
}
|
|
2607
2810
|
|
|
2608
2811
|
// apps/server/ai/models.ts
|
|
2609
|
-
import { createModels, createProvider, Type as
|
|
2812
|
+
import { createModels, createProvider, Type as Type3 } from "@earendil-works/pi-ai";
|
|
2610
2813
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
2611
2814
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
2612
2815
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
2816
|
+
function withoutOutputLimits(payload) {
|
|
2817
|
+
const {
|
|
2818
|
+
max_tokens: _chat,
|
|
2819
|
+
max_completion_tokens: _completion,
|
|
2820
|
+
max_output_tokens: _response,
|
|
2821
|
+
...rest
|
|
2822
|
+
} = payload;
|
|
2823
|
+
return rest;
|
|
2824
|
+
}
|
|
2613
2825
|
function modelAccess(profile, key, reasoning = false) {
|
|
2614
2826
|
const model = {
|
|
2615
2827
|
id: profile.model,
|
|
@@ -2619,8 +2831,8 @@ function modelAccess(profile, key, reasoning = false) {
|
|
|
2619
2831
|
baseUrl: profile.protocol === "anthropic-messages" ? profile.baseUrl.replace(/\/v1\/?$/, "") : profile.baseUrl,
|
|
2620
2832
|
reasoning,
|
|
2621
2833
|
input: ["text"],
|
|
2622
|
-
contextWindow:
|
|
2623
|
-
maxTokens:
|
|
2834
|
+
contextWindow: 0,
|
|
2835
|
+
maxTokens: Number.MAX_SAFE_INTEGER,
|
|
2624
2836
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2625
2837
|
...profile.protocol === "openai-completions" ? {
|
|
2626
2838
|
compat: {
|
|
@@ -2666,11 +2878,11 @@ async function modelCall(profile, key, context, signal, extra = {}, thinkingLeve
|
|
|
2666
2878
|
const { models, model } = modelAccess(profile, key, thinkingLevel !== "off");
|
|
2667
2879
|
const options = {
|
|
2668
2880
|
...extra,
|
|
2669
|
-
maxTokens: profile.maxOutputTokens,
|
|
2670
2881
|
maxRetries: 0,
|
|
2671
|
-
timeoutMs: extra.timeoutMs ??
|
|
2882
|
+
timeoutMs: extra.timeoutMs ?? 3e5,
|
|
2672
2883
|
signal,
|
|
2673
|
-
transport: "sse"
|
|
2884
|
+
transport: "sse",
|
|
2885
|
+
onPayload: async (payload, model2) => withoutOutputLimits(await extra.onPayload?.(payload, model2) ?? payload)
|
|
2674
2886
|
};
|
|
2675
2887
|
const stream = thinkingLevel === "off" ? models.stream(model, context, options) : models.streamSimple(model, context, { ...options, reasoning: thinkingLevel });
|
|
2676
2888
|
let streamed = false;
|
|
@@ -2700,13 +2912,13 @@ async function probeModel(profile, key, mode, signal) {
|
|
|
2700
2912
|
{
|
|
2701
2913
|
name: "ping",
|
|
2702
2914
|
description: "Get the verification marker.",
|
|
2703
|
-
parameters:
|
|
2915
|
+
parameters: Type3.Object({ value: Type3.String() })
|
|
2704
2916
|
}
|
|
2705
2917
|
]
|
|
2706
2918
|
} : {}
|
|
2707
2919
|
};
|
|
2708
2920
|
const first = await modelCall(
|
|
2709
|
-
|
|
2921
|
+
profile,
|
|
2710
2922
|
key,
|
|
2711
2923
|
context,
|
|
2712
2924
|
signal,
|
|
@@ -2762,8 +2974,522 @@ async function probeModel(profile, key, mode, signal) {
|
|
|
2762
2974
|
return { ok: true, mode, streamed, inputTokens, outputTokens };
|
|
2763
2975
|
}
|
|
2764
2976
|
|
|
2977
|
+
// apps/server/ai/native-sandbox.ts
|
|
2978
|
+
import { spawn } from "node:child_process";
|
|
2979
|
+
import { lstat, mkdtemp, readlink, rm } from "node:fs/promises";
|
|
2980
|
+
import { tmpdir } from "node:os";
|
|
2981
|
+
import { basename, dirname as dirname4, join as join6 } from "node:path";
|
|
2982
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2983
|
+
|
|
2984
|
+
// apps/server/ai/agent-channel.ts
|
|
2985
|
+
import { createInterface } from "node:readline";
|
|
2986
|
+
var AgentChannel = class {
|
|
2987
|
+
constructor(input, output) {
|
|
2988
|
+
this.output = output;
|
|
2989
|
+
this.lines = createInterface({ input, crlfDelay: Infinity });
|
|
2990
|
+
this.iterator = this.lines[Symbol.asyncIterator]();
|
|
2991
|
+
output.on("error", () => {
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
output;
|
|
2995
|
+
lines;
|
|
2996
|
+
iterator;
|
|
2997
|
+
writes = Promise.resolve();
|
|
2998
|
+
async *messages() {
|
|
2999
|
+
for await (const line of this.iterator) if (line) yield JSON.parse(line);
|
|
3000
|
+
}
|
|
3001
|
+
send(value) {
|
|
3002
|
+
const write = this.writes.then(
|
|
3003
|
+
() => new Promise((resolve4, reject) => {
|
|
3004
|
+
this.output.write(
|
|
3005
|
+
JSON.stringify(value) + "\n",
|
|
3006
|
+
(error) => error ? reject(error) : resolve4()
|
|
3007
|
+
);
|
|
3008
|
+
})
|
|
3009
|
+
);
|
|
3010
|
+
this.writes = write.catch(() => {
|
|
3011
|
+
});
|
|
3012
|
+
return write;
|
|
3013
|
+
}
|
|
3014
|
+
close() {
|
|
3015
|
+
this.lines.close();
|
|
3016
|
+
this.output.end();
|
|
3017
|
+
}
|
|
3018
|
+
};
|
|
3019
|
+
|
|
3020
|
+
// apps/server/ai/agent-workspace.ts
|
|
3021
|
+
import { mkdir as mkdir2, writeFile } from "node:fs/promises";
|
|
3022
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
3023
|
+
|
|
3024
|
+
// apps/server/ai/line-diff.ts
|
|
3025
|
+
import { Worker } from "node:worker_threads";
|
|
3026
|
+
async function lineChanges(before, after, signal) {
|
|
3027
|
+
signal?.throwIfAborted();
|
|
3028
|
+
const worker = new Worker(new URL("./line-diff-worker.js", import.meta.url), {
|
|
3029
|
+
workerData: { before, after }
|
|
3030
|
+
});
|
|
3031
|
+
let abort;
|
|
3032
|
+
try {
|
|
3033
|
+
return await new Promise((resolve4, reject) => {
|
|
3034
|
+
abort = () => reject(signal.reason);
|
|
3035
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3036
|
+
worker.once("message", resolve4);
|
|
3037
|
+
worker.once("error", reject);
|
|
3038
|
+
worker.once(
|
|
3039
|
+
"exit",
|
|
3040
|
+
(code) => reject(new Error(`Line comparison exited without a result (${code}).`))
|
|
3041
|
+
);
|
|
3042
|
+
if (signal?.aborted) abort();
|
|
3043
|
+
});
|
|
3044
|
+
} finally {
|
|
3045
|
+
if (abort) signal?.removeEventListener("abort", abort);
|
|
3046
|
+
await worker.terminate();
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// apps/server/ai/task-explorer.ts
|
|
3051
|
+
import { matchesGlob } from "node:path";
|
|
3052
|
+
var displayCount = 40;
|
|
3053
|
+
function abbreviated(items) {
|
|
3054
|
+
const shown = items.slice(0, displayCount), omitted = items.length - shown.length;
|
|
3055
|
+
return {
|
|
3056
|
+
items: shown,
|
|
3057
|
+
total: items.length,
|
|
3058
|
+
omitted,
|
|
3059
|
+
...omitted ? { note: `\u2026\u2026\u7B49 ${omitted} \u9879\uFF1B\u53EF\u7F29\u5C0F\u67E5\u8BE2\u8303\u56F4\u3002` } : {}
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function matchesFile(path, filter2) {
|
|
3063
|
+
const prefix = filter2.path?.replace(/\/$/, "") ?? "";
|
|
3064
|
+
return (!prefix || path === prefix || path.startsWith(prefix + "/")) && (!filter2.query || path.toLowerCase().includes(filter2.query.toLowerCase())) && (!filter2.pattern || matchesGlob(path, filter2.pattern));
|
|
3065
|
+
}
|
|
3066
|
+
var TaskExplorer = class {
|
|
3067
|
+
constructor(current, context, signal) {
|
|
3068
|
+
this.current = current;
|
|
3069
|
+
this.context = context;
|
|
3070
|
+
this.signal = signal;
|
|
3071
|
+
this.baseline = context?.baseline ? current.forSnapshot(context.baseline) : void 0;
|
|
3072
|
+
}
|
|
3073
|
+
current;
|
|
3074
|
+
context;
|
|
3075
|
+
signal;
|
|
3076
|
+
baseline;
|
|
3077
|
+
ranges = /* @__PURE__ */ new Map();
|
|
3078
|
+
reader(scope2 = "member", version2 = "current") {
|
|
3079
|
+
const reader = version2 === "current" ? this.current : this.baseline;
|
|
3080
|
+
if (!reader)
|
|
3081
|
+
throw new HubError(
|
|
3082
|
+
404,
|
|
3083
|
+
"AI_BASELINE_UNAVAILABLE",
|
|
3084
|
+
"There is no successful baseline snapshot."
|
|
3085
|
+
);
|
|
3086
|
+
return scope2 === "project" ? reader.projectReader() : reader;
|
|
3087
|
+
}
|
|
3088
|
+
files(scope2, version2) {
|
|
3089
|
+
const snapshot = version2 === "current" ? this.current.snapshot : this.context?.baseline;
|
|
3090
|
+
return (scope2 === "project" ? snapshot?.project?.files : snapshot?.files) ?? [];
|
|
3091
|
+
}
|
|
3092
|
+
list(filter2 = {}, scope2 = "member", version2 = "current") {
|
|
3093
|
+
this.reader(scope2, version2);
|
|
3094
|
+
const files = this.files(scope2, version2).filter((file) => matchesFile(file.path, filter2));
|
|
3095
|
+
const prefix = filter2.path?.replace(/\/$/, "") ?? "";
|
|
3096
|
+
const directories = /* @__PURE__ */ new Map();
|
|
3097
|
+
for (const file of files) {
|
|
3098
|
+
const relative = prefix && file.path.startsWith(prefix + "/") ? file.path.slice(prefix.length + 1) : file.path;
|
|
3099
|
+
if (!relative.includes("/")) continue;
|
|
3100
|
+
const path = (prefix ? prefix + "/" : "") + relative.split("/")[0];
|
|
3101
|
+
directories.set(path, (directories.get(path) ?? 0) + 1);
|
|
3102
|
+
}
|
|
3103
|
+
return {
|
|
3104
|
+
scope: scope2,
|
|
3105
|
+
version: version2,
|
|
3106
|
+
files: abbreviated(files.map(({ path, size }) => ({ path, size }))),
|
|
3107
|
+
directories: abbreviated([...directories].map(([path, files2]) => ({ path, files: files2 })))
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
async search(query, after = "", filter2 = {}, scope2 = "member", version2 = "current") {
|
|
3111
|
+
const reader = this.reader(scope2, version2);
|
|
3112
|
+
return reader.search(
|
|
3113
|
+
query,
|
|
3114
|
+
after,
|
|
3115
|
+
reader.snapshot.files.filter((file) => matchesFile(file.path, filter2))
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
changes(filter2 = {}, scope2 = "member") {
|
|
3119
|
+
const changes = changedPaths(this.files(scope2, "baseline"), this.files(scope2, "current"));
|
|
3120
|
+
return Object.entries(changes).flatMap(([kind, paths2]) => paths2.map((path) => ({ path, kind }))).filter((file) => matchesFile(file.path, filter2)).sort((a, b) => a.path.localeCompare(b.path));
|
|
3121
|
+
}
|
|
3122
|
+
overview() {
|
|
3123
|
+
return {
|
|
3124
|
+
owner: this.current.snapshot.owner,
|
|
3125
|
+
revision: this.current.snapshot.revision,
|
|
3126
|
+
mode: this.context?.baseline ? "incremental" : "cold_start",
|
|
3127
|
+
baselineRevision: this.context?.previousRevision ?? null,
|
|
3128
|
+
inventory: this.list(),
|
|
3129
|
+
projectReferences: this.current.snapshot.project ? this.list({}, "project") : null,
|
|
3130
|
+
changes: {
|
|
3131
|
+
member: abbreviated(this.changes()),
|
|
3132
|
+
project: abbreviated(this.changes({}, "project"))
|
|
3133
|
+
},
|
|
3134
|
+
previousTasks: {
|
|
3135
|
+
active: this.context?.tasks.filter((task) => task.active !== false).length ?? 0,
|
|
3136
|
+
archived: this.context?.tasks.filter((task) => task.active === false).length ?? 0
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
queryTasks(filter2 = {}) {
|
|
3141
|
+
const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
|
|
3142
|
+
return abbreviated(
|
|
3143
|
+
(this.context?.tasks ?? []).filter(
|
|
3144
|
+
(task) => (!filter2.id || task.taskId === filter2.id) && (visibility === "all" || (visibility === "active" ? task.active !== false : task.active === false)) && (!filter2.source || task.sources.some((source) => matchesFile(source.path, { path: filter2.source }))) && (!filter2.query || [
|
|
3145
|
+
task.title,
|
|
3146
|
+
task.goal ?? "",
|
|
3147
|
+
task.progress,
|
|
3148
|
+
...task.sources.map((source) => source.path)
|
|
3149
|
+
].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
|
|
3150
|
+
)
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
async inspect(filter2 = {}, scope2 = "member") {
|
|
3154
|
+
const changes = this.changes(filter2, scope2);
|
|
3155
|
+
const response = abbreviated(changes);
|
|
3156
|
+
const items = [];
|
|
3157
|
+
for (const change of response.items) {
|
|
3158
|
+
this.signal.throwIfAborted();
|
|
3159
|
+
const before = this.files(scope2, "baseline").find((file) => file.path === change.path);
|
|
3160
|
+
const after = this.files(scope2, "current").find((file) => file.path === change.path);
|
|
3161
|
+
const key = `${before?.sha256 ?? ""}:${after?.sha256 ?? ""}`;
|
|
3162
|
+
try {
|
|
3163
|
+
let pending = this.ranges.get(key);
|
|
3164
|
+
if (!pending) {
|
|
3165
|
+
pending = (async () => lineChanges(
|
|
3166
|
+
before ? await this.reader(scope2, "baseline").text(change.path) : "",
|
|
3167
|
+
after ? await this.reader(scope2).text(change.path) : "",
|
|
3168
|
+
this.signal
|
|
3169
|
+
))();
|
|
3170
|
+
this.ranges.set(key, pending);
|
|
3171
|
+
}
|
|
3172
|
+
const ranges = await pending;
|
|
3173
|
+
this.signal.throwIfAborted();
|
|
3174
|
+
const selected = ranges.filter((range) => {
|
|
3175
|
+
const location = filter2.version === "baseline" ? range.before : range.after;
|
|
3176
|
+
return (filter2.startLine === void 0 || location.startLine + Math.max(1, location.count) - 1 >= filter2.startLine) && (filter2.endLine === void 0 || location.startLine <= filter2.endLine);
|
|
3177
|
+
});
|
|
3178
|
+
items.push({ ...change, available: true, ranges: abbreviated(selected) });
|
|
3179
|
+
} catch (error) {
|
|
3180
|
+
this.signal.throwIfAborted();
|
|
3181
|
+
items.push({
|
|
3182
|
+
...change,
|
|
3183
|
+
available: false,
|
|
3184
|
+
reason: error instanceof HubError ? error.code : "DIFF_UNAVAILABLE"
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
return { scope: scope2, ...response, items };
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
|
|
3192
|
+
// apps/server/ai/agent-workspace.ts
|
|
3193
|
+
async function materializeWorkspace(root, reader, signal) {
|
|
3194
|
+
const owner = FileOwnerSchema.parse(reader.snapshot.owner);
|
|
3195
|
+
await mkdir2(join5(root, owner), { recursive: true });
|
|
3196
|
+
async function copy(source, prefix, shared = false) {
|
|
3197
|
+
for (const file of source.snapshot.files) {
|
|
3198
|
+
signal.throwIfAborted();
|
|
3199
|
+
const path = FilePathSchema.parse(file.path);
|
|
3200
|
+
if (shared && !sharedPathAllowed(path, DEFAULT_SHARED_PATHS)) continue;
|
|
3201
|
+
const target = join5(root, prefix, path);
|
|
3202
|
+
await mkdir2(dirname3(target), { recursive: true });
|
|
3203
|
+
await writeFile(target, await source.bytes(path), { flag: "wx", mode: 292 });
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
await copy(reader, owner);
|
|
3207
|
+
if (reader.snapshot.project) await copy(reader.projectReader(), "", true);
|
|
3208
|
+
}
|
|
3209
|
+
function memberSourcePath(path, owner) {
|
|
3210
|
+
const relative = path.replace(/^\/workspaces\//, "").replace(/^\.\//, "");
|
|
3211
|
+
return relative.startsWith(owner + "/") ? relative.slice(owner.length + 1) : null;
|
|
3212
|
+
}
|
|
3213
|
+
async function taskInput(reader, context, signal) {
|
|
3214
|
+
const owner = reader.snapshot.owner;
|
|
3215
|
+
const lines = [
|
|
3216
|
+
`\u672C\u8F6E\u7EF4\u62A4\u6210\u5458 ${owner} \u7684\u4EFB\u52A1\u6E05\u5355\u3002`,
|
|
3217
|
+
`\u6A21\u5F0F\uFF1A${context?.baseline ? "\u589E\u91CF\u7EF4\u62A4" : "\u9996\u6B21\u6574\u7406"}\uFF1B\u5F53\u524D\u8D44\u6599\uFF1A/workspaces/${owner}/\u3002`,
|
|
3218
|
+
"\u516C\u5171\u8BF4\u660E\u53CA\u6A21\u677F\uFF08\u5982\u6709\uFF09\uFF1A/workspaces/README.md\u3001/workspaces/_template/\u3002",
|
|
3219
|
+
context?.baseline ? "\u5386\u53F2\u57FA\u7EBF\uFF1A/baseline/workspaces/\u3002" : "\u672C\u8F6E\u6CA1\u6709\u5386\u53F2\u57FA\u7EBF\u3002"
|
|
3220
|
+
];
|
|
3221
|
+
if (context?.baseline) {
|
|
3222
|
+
const explorer = new TaskExplorer(reader, context, signal);
|
|
3223
|
+
const changes = [
|
|
3224
|
+
...explorer.changes().map((change) => ({ ...change, scope: "member" })),
|
|
3225
|
+
...explorer.changes({}, "project").map((change) => ({ ...change, scope: "project" }))
|
|
3226
|
+
];
|
|
3227
|
+
lines.push("\u672C\u8F6E\u53D8\u5316\uFF1A");
|
|
3228
|
+
for (const change of changes.slice(0, 20)) {
|
|
3229
|
+
signal.throwIfAborted();
|
|
3230
|
+
const path = change.scope === "member" ? `${owner}/${change.path}` : change.path;
|
|
3231
|
+
let detail = "";
|
|
3232
|
+
if (change.kind === "modified") {
|
|
3233
|
+
const diff = await explorer.inspect({ path: change.path }, change.scope);
|
|
3234
|
+
const item = diff.items[0];
|
|
3235
|
+
if (item?.available) {
|
|
3236
|
+
detail = "\uFF1B\u53D8\u5316\u884C\uFF08\u65E7\u2192\u65B0\uFF09" + item.ranges.items.slice(0, 3).map(
|
|
3237
|
+
(range) => `${range.before.startLine}+${range.before.count}\u2192${range.after.startLine}+${range.after.count}`
|
|
3238
|
+
).join("\u3001");
|
|
3239
|
+
if (item.ranges.total > 3) detail += `\uFF0C\u53E6\u6709 ${item.ranges.total - 3} \u5904`;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
lines.push(`- ${change.kind} ${JSON.stringify(path)}${detail}`);
|
|
3243
|
+
}
|
|
3244
|
+
if (changes.length > 20) lines.push(`\u2026\u2026\u53E6\u6709 ${changes.length - 20} \u4E2A\u6587\u4EF6\u53D1\u751F\u53D8\u5316\u3002`);
|
|
3245
|
+
if (!changes.length) lines.push("- \u65E0\u6587\u4EF6\u53D8\u5316\u3002");
|
|
3246
|
+
}
|
|
3247
|
+
lines.push("\u5DF2\u6709\u4EFB\u52A1\u53EF\u901A\u8FC7 query_tasks \u67E5\u8BE2\u3002");
|
|
3248
|
+
return lines.join("\n");
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// apps/server/ai/agent-protocol.ts
|
|
3252
|
+
import { z as z10 } from "zod";
|
|
3253
|
+
var TaskQuerySchema = z10.object({
|
|
3254
|
+
id: z10.uuid().optional(),
|
|
3255
|
+
source: z10.string().optional(),
|
|
3256
|
+
query: z10.string().optional(),
|
|
3257
|
+
visibility: z10.enum(["active", "archived", "all"]).optional()
|
|
3258
|
+
}).strict();
|
|
3259
|
+
|
|
3260
|
+
// apps/server/ai/task-query.ts
|
|
3261
|
+
function queryTasks(context, owner, input) {
|
|
3262
|
+
const filter2 = TaskQuerySchema.parse(input);
|
|
3263
|
+
const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
|
|
3264
|
+
return abbreviated(
|
|
3265
|
+
(context?.tasks ?? []).map((task) => ({
|
|
3266
|
+
...task,
|
|
3267
|
+
sources: task.sources.map((source) => ({ ...source, path: `${owner}/${source.path}` }))
|
|
3268
|
+
})).filter(
|
|
3269
|
+
(task) => (!filter2.id || task.taskId === filter2.id) && (visibility === "all" || (visibility === "active" ? task.active !== false : task.active === false)) && (!filter2.source || task.sources.some((source) => matchesFile(source.path, { path: filter2.source }))) && (!filter2.query || [
|
|
3270
|
+
task.title,
|
|
3271
|
+
task.goal ?? "",
|
|
3272
|
+
task.progress,
|
|
3273
|
+
...task.sources.map((source) => source.path)
|
|
3274
|
+
].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
|
|
3275
|
+
)
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// apps/server/ai/native-sandbox.ts
|
|
3280
|
+
async function sandboxCommand(root, baseline) {
|
|
3281
|
+
if (process.platform !== "linux")
|
|
3282
|
+
throw new HubError(
|
|
3283
|
+
503,
|
|
3284
|
+
"AI_SANDBOX_UNAVAILABLE",
|
|
3285
|
+
"Native analysis requires a Linux worker with Bubblewrap."
|
|
3286
|
+
);
|
|
3287
|
+
let dependencies = dirname4(fileURLToPath2(import.meta.resolve("@earendil-works/pi-coding-agent")));
|
|
3288
|
+
while (basename(dependencies) !== "node_modules") {
|
|
3289
|
+
const parent = dirname4(dependencies);
|
|
3290
|
+
if (parent === dependencies) throw new Error("Pi dependency root not found.");
|
|
3291
|
+
dependencies = parent;
|
|
3292
|
+
}
|
|
3293
|
+
const args = [
|
|
3294
|
+
"--unshare-all",
|
|
3295
|
+
"--unshare-user",
|
|
3296
|
+
"--die-with-parent",
|
|
3297
|
+
"--new-session",
|
|
3298
|
+
"--cap-drop",
|
|
3299
|
+
"ALL",
|
|
3300
|
+
"--ro-bind",
|
|
3301
|
+
"/usr",
|
|
3302
|
+
"/usr"
|
|
3303
|
+
];
|
|
3304
|
+
for (const path of ["/bin", "/sbin", "/lib", "/lib64"]) {
|
|
3305
|
+
const stat = await lstat(path).catch(() => null);
|
|
3306
|
+
if (!stat) continue;
|
|
3307
|
+
if (stat.isSymbolicLink()) args.push("--symlink", await readlink(path), path);
|
|
3308
|
+
else args.push("--ro-bind", path, path);
|
|
3309
|
+
}
|
|
3310
|
+
if (await lstat("/etc/alternatives").catch(() => null))
|
|
3311
|
+
args.push("--ro-bind", "/etc/alternatives", "/etc/alternatives");
|
|
3312
|
+
args.push(
|
|
3313
|
+
"--proc",
|
|
3314
|
+
"/proc",
|
|
3315
|
+
"--dev",
|
|
3316
|
+
"/dev",
|
|
3317
|
+
"--tmpfs",
|
|
3318
|
+
"/tmp",
|
|
3319
|
+
"--dir",
|
|
3320
|
+
"/home/agent",
|
|
3321
|
+
"--setenv",
|
|
3322
|
+
"HOME",
|
|
3323
|
+
"/home/agent",
|
|
3324
|
+
"--setenv",
|
|
3325
|
+
"PATH",
|
|
3326
|
+
"/opt/node-bin:/usr/local/bin:/usr/bin:/bin",
|
|
3327
|
+
"--setenv",
|
|
3328
|
+
"LANG",
|
|
3329
|
+
"C.UTF-8",
|
|
3330
|
+
"--setenv",
|
|
3331
|
+
"SHELL",
|
|
3332
|
+
"/bin/bash",
|
|
3333
|
+
"--setenv",
|
|
3334
|
+
"PI_SKIP_VERSION_CHECK",
|
|
3335
|
+
"1",
|
|
3336
|
+
"--ro-bind",
|
|
3337
|
+
process.execPath,
|
|
3338
|
+
"/opt/node-bin/node",
|
|
3339
|
+
"--ro-bind",
|
|
3340
|
+
dependencies,
|
|
3341
|
+
"/opt/teammy/node_modules",
|
|
3342
|
+
"--ro-bind",
|
|
3343
|
+
fileURLToPath2(new URL("./native-pi-entry.js", import.meta.url)),
|
|
3344
|
+
"/opt/teammy/native-pi-entry.mjs",
|
|
3345
|
+
"--ro-bind",
|
|
3346
|
+
join6(root, "workspaces"),
|
|
3347
|
+
"/workspaces"
|
|
3348
|
+
);
|
|
3349
|
+
if (baseline) args.push("--ro-bind", join6(root, "baseline/workspaces"), "/baseline/workspaces");
|
|
3350
|
+
args.push("--chdir", "/workspaces", "/opt/node-bin/node", "/opt/teammy/native-pi-entry.mjs");
|
|
3351
|
+
return { command: "bwrap", args };
|
|
3352
|
+
}
|
|
3353
|
+
async function runNativeAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, context = null) {
|
|
3354
|
+
signal.throwIfAborted();
|
|
3355
|
+
const root = await mkdtemp(join6(tmpdir(), "teammy-agent-"));
|
|
3356
|
+
try {
|
|
3357
|
+
await materializeWorkspace(join6(root, "workspaces"), reader, signal);
|
|
3358
|
+
if (context?.baseline)
|
|
3359
|
+
await materializeWorkspace(
|
|
3360
|
+
join6(root, "baseline/workspaces"),
|
|
3361
|
+
reader.forSnapshot(context.baseline),
|
|
3362
|
+
signal
|
|
3363
|
+
);
|
|
3364
|
+
const { command, args } = await sandboxCommand(root, Boolean(context?.baseline));
|
|
3365
|
+
signal.throwIfAborted();
|
|
3366
|
+
const child = spawn(command, args, {
|
|
3367
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3368
|
+
detached: true,
|
|
3369
|
+
env: { PATH: "/usr/bin:/bin" }
|
|
3370
|
+
});
|
|
3371
|
+
const channel = new AgentChannel(child.stdout, child.stdin);
|
|
3372
|
+
const active = new AbortController();
|
|
3373
|
+
const requestSignal = AbortSignal.any([signal, active.signal]);
|
|
3374
|
+
let failure, text, rounds = 0;
|
|
3375
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
3376
|
+
const stop = () => {
|
|
3377
|
+
active.abort();
|
|
3378
|
+
if (child.pid) {
|
|
3379
|
+
try {
|
|
3380
|
+
process.kill(-child.pid, "SIGKILL");
|
|
3381
|
+
} catch {
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
const closed = new Promise((resolve4) => {
|
|
3386
|
+
child.once("error", () => {
|
|
3387
|
+
failure ??= new HubError(
|
|
3388
|
+
503,
|
|
3389
|
+
"AI_SANDBOX_UNAVAILABLE",
|
|
3390
|
+
"Could not start Bubblewrap; check the Linux worker setup."
|
|
3391
|
+
);
|
|
3392
|
+
});
|
|
3393
|
+
child.once("close", (code) => {
|
|
3394
|
+
if (code !== 0 && !signal.aborted)
|
|
3395
|
+
failure ??= new HubError(
|
|
3396
|
+
503,
|
|
3397
|
+
"AI_SANDBOX_FAILED",
|
|
3398
|
+
"Sandbox exited unsuccessfully; check worker isolation support."
|
|
3399
|
+
);
|
|
3400
|
+
active.abort();
|
|
3401
|
+
resolve4();
|
|
3402
|
+
});
|
|
3403
|
+
});
|
|
3404
|
+
child.stderr.resume();
|
|
3405
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
3406
|
+
const pending = /* @__PURE__ */ new Set();
|
|
3407
|
+
const handle = async (message) => {
|
|
3408
|
+
if (message.type === "progress") {
|
|
3409
|
+
if (runtime.config.tools.some((tool) => tool === message.tool))
|
|
3410
|
+
progress("tool", `\u4F7F\u7528 ${message.tool}\u3002`);
|
|
3411
|
+
} else if (message.type === "result") {
|
|
3412
|
+
if (typeof message.text !== "string" || text !== void 0)
|
|
3413
|
+
throw new Error("Invalid result.");
|
|
3414
|
+
text = message.text;
|
|
3415
|
+
} else if (message.type === "failed") {
|
|
3416
|
+
throw new HubError(422, "AI_RESULT_INVALID", "Agent did not complete analysis.");
|
|
3417
|
+
} else if (message.type === "request") {
|
|
3418
|
+
let value;
|
|
3419
|
+
if (message.method === "model") {
|
|
3420
|
+
if (++rounds > runtime.config.maxRounds)
|
|
3421
|
+
throw new HubError(413, "AI_ROUND_LIMIT", "Analysis exceeded its model round budget.");
|
|
3422
|
+
progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${rounds} \u8F6E\u3002`);
|
|
3423
|
+
const response = await modelCall(
|
|
3424
|
+
profile,
|
|
3425
|
+
key,
|
|
3426
|
+
message.input,
|
|
3427
|
+
requestSignal,
|
|
3428
|
+
{ timeoutMs: runtime.config.requestTimeoutMs },
|
|
3429
|
+
runtime.config.thinkingLevel
|
|
3430
|
+
);
|
|
3431
|
+
value = response.message;
|
|
3432
|
+
usage.inputTokens += response.message.usage.input;
|
|
3433
|
+
usage.outputTokens += response.message.usage.output;
|
|
3434
|
+
} else if (message.method === "query_tasks" && runtime.config.tools.includes("query_tasks")) {
|
|
3435
|
+
value = queryTasks(context, reader.snapshot.owner, message.input);
|
|
3436
|
+
} else throw new HubError(403, "AI_TOOL_UNAVAILABLE", "Tool is unavailable for this job.");
|
|
3437
|
+
await channel.send({ type: "response", id: message.id, value });
|
|
3438
|
+
} else throw new Error("Invalid sandbox message.");
|
|
3439
|
+
};
|
|
3440
|
+
try {
|
|
3441
|
+
const { model } = modelAccess(
|
|
3442
|
+
{ ...profile, baseUrl: "http://model.invalid" },
|
|
3443
|
+
"",
|
|
3444
|
+
runtime.config.thinkingLevel !== "off"
|
|
3445
|
+
);
|
|
3446
|
+
await channel.send({ type: "start", config: { model, runtime, systemPrompt, input } });
|
|
3447
|
+
if (signal.aborted) stop();
|
|
3448
|
+
for await (const message of channel.messages()) {
|
|
3449
|
+
const task = handle(message).catch((error) => {
|
|
3450
|
+
failure ??= error;
|
|
3451
|
+
stop();
|
|
3452
|
+
});
|
|
3453
|
+
pending.add(task);
|
|
3454
|
+
void task.finally(() => pending.delete(task));
|
|
3455
|
+
}
|
|
3456
|
+
await closed;
|
|
3457
|
+
await Promise.all(pending);
|
|
3458
|
+
signal.throwIfAborted();
|
|
3459
|
+
if (failure) throw failure;
|
|
3460
|
+
if (text === void 0) throw new HubError(422, "AI_RESULT_EMPTY", "No analysis result.");
|
|
3461
|
+
return { text, usage };
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
stop();
|
|
3464
|
+
await closed;
|
|
3465
|
+
signal.throwIfAborted();
|
|
3466
|
+
throw failure ?? error;
|
|
3467
|
+
} finally {
|
|
3468
|
+
signal.removeEventListener("abort", stop);
|
|
3469
|
+
stop();
|
|
3470
|
+
channel.close();
|
|
3471
|
+
await closed;
|
|
3472
|
+
await Promise.allSettled(pending);
|
|
3473
|
+
}
|
|
3474
|
+
} finally {
|
|
3475
|
+
await rm(root, { recursive: true, force: true });
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
|
|
2765
3479
|
// apps/server/ai/pi-runner.ts
|
|
2766
|
-
async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input) {
|
|
3480
|
+
async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, explorer, context = null) {
|
|
3481
|
+
if (runtime.config.execution === "native-sandbox")
|
|
3482
|
+
return runNativeAgent(
|
|
3483
|
+
profile,
|
|
3484
|
+
reader,
|
|
3485
|
+
key,
|
|
3486
|
+
signal,
|
|
3487
|
+
progress,
|
|
3488
|
+
runtime,
|
|
3489
|
+
systemPrompt,
|
|
3490
|
+
typeof input === "string" ? input : JSON.stringify(input),
|
|
3491
|
+
context
|
|
3492
|
+
);
|
|
2767
3493
|
const {
|
|
2768
3494
|
createAgentSession,
|
|
2769
3495
|
createExtensionRuntime,
|
|
@@ -2771,7 +3497,7 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2771
3497
|
SessionManager,
|
|
2772
3498
|
SettingsManager
|
|
2773
3499
|
} = await import("@earendil-works/pi-coding-agent");
|
|
2774
|
-
const cwd = await
|
|
3500
|
+
const cwd = await mkdtemp2(join7(tmpdir2(), "teammy-ai-run-"));
|
|
2775
3501
|
try {
|
|
2776
3502
|
const { models, model } = modelAccess(profile, key, runtime.config.thinkingLevel !== "off");
|
|
2777
3503
|
const modelRuntime = await ModelRuntime.create({
|
|
@@ -2812,29 +3538,29 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2812
3538
|
cacheWarming: "off"
|
|
2813
3539
|
}),
|
|
2814
3540
|
tools: runtime.config.tools,
|
|
2815
|
-
customTools: analysisTools(reader, signal, progress, runtime)
|
|
3541
|
+
customTools: analysisTools(reader, signal, progress, runtime, explorer)
|
|
2816
3542
|
});
|
|
2817
3543
|
let requests = 0, budgetError;
|
|
2818
3544
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
2819
|
-
session.agent.streamFunction = (_model,
|
|
3545
|
+
session.agent.streamFunction = (_model, context2, options) => {
|
|
2820
3546
|
signal.throwIfAborted();
|
|
2821
|
-
if (++requests > runtime.config.maxRounds
|
|
3547
|
+
if (++requests > runtime.config.maxRounds) {
|
|
2822
3548
|
budgetError = new HubError(
|
|
2823
3549
|
413,
|
|
2824
|
-
"
|
|
2825
|
-
"Analysis exceeded its
|
|
3550
|
+
"AI_ROUND_LIMIT",
|
|
3551
|
+
"Analysis exceeded its model round budget."
|
|
2826
3552
|
);
|
|
2827
3553
|
throw budgetError;
|
|
2828
3554
|
}
|
|
2829
3555
|
progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${requests} \u8F6E\u3002`);
|
|
2830
|
-
return models.streamSimple(model,
|
|
3556
|
+
return models.streamSimple(model, context2, {
|
|
2831
3557
|
...options,
|
|
2832
3558
|
...runtime.config.thinkingLevel !== "off" ? { reasoning: runtime.config.thinkingLevel } : {},
|
|
2833
3559
|
signal: options?.signal ? AbortSignal.any([signal, options.signal]) : signal,
|
|
2834
3560
|
maxRetries: 0,
|
|
2835
|
-
maxTokens: profile.maxOutputTokens,
|
|
2836
3561
|
timeoutMs: runtime.config.requestTimeoutMs,
|
|
2837
|
-
transport: "sse"
|
|
3562
|
+
transport: "sse",
|
|
3563
|
+
onPayload: async (payload, model2) => withoutOutputLimits(await options?.onPayload?.(payload, model2) ?? payload)
|
|
2838
3564
|
});
|
|
2839
3565
|
};
|
|
2840
3566
|
const unsubscribe = session.subscribe((event) => {
|
|
@@ -2861,25 +3587,25 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2861
3587
|
session.dispose();
|
|
2862
3588
|
}
|
|
2863
3589
|
} finally {
|
|
2864
|
-
await
|
|
3590
|
+
await rm2(cwd, { recursive: true, force: true });
|
|
2865
3591
|
}
|
|
2866
3592
|
}
|
|
2867
3593
|
|
|
2868
3594
|
// apps/server/ai/results.ts
|
|
2869
|
-
import { z as
|
|
2870
|
-
var SummaryOutputSchema =
|
|
2871
|
-
summary:
|
|
2872
|
-
findings:
|
|
2873
|
-
uncertainties:
|
|
2874
|
-
citations:
|
|
2875
|
-
|
|
2876
|
-
path:
|
|
2877
|
-
startLine:
|
|
2878
|
-
endLine:
|
|
3595
|
+
import { z as z11 } from "zod";
|
|
3596
|
+
var SummaryOutputSchema = z11.object({
|
|
3597
|
+
summary: z11.string(),
|
|
3598
|
+
findings: z11.array(z11.string()),
|
|
3599
|
+
uncertainties: z11.array(z11.string()).optional(),
|
|
3600
|
+
citations: z11.array(
|
|
3601
|
+
z11.object({
|
|
3602
|
+
path: z11.string(),
|
|
3603
|
+
startLine: z11.number().int().positive(),
|
|
3604
|
+
endLine: z11.number().int().positive()
|
|
2879
3605
|
}).strict()
|
|
2880
|
-
)
|
|
3606
|
+
)
|
|
2881
3607
|
}).strict();
|
|
2882
|
-
function validateResult(text, reader) {
|
|
3608
|
+
function validateResult(text, reader, native = false) {
|
|
2883
3609
|
let output;
|
|
2884
3610
|
try {
|
|
2885
3611
|
output = SummaryOutputSchema.parse(
|
|
@@ -2892,29 +3618,29 @@ function validateResult(text, reader) {
|
|
|
2892
3618
|
"Analysis did not produce the required JSON result."
|
|
2893
3619
|
);
|
|
2894
3620
|
}
|
|
2895
|
-
const citations = output.citations.
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
return { ...c, sha256: file.sha256 };
|
|
3621
|
+
const citations = output.citations.flatMap((c) => {
|
|
3622
|
+
const path = native ? memberSourcePath(c.path, reader.snapshot.owner) : c.path;
|
|
3623
|
+
const file = reader.snapshot.files.find((f) => f.path === path);
|
|
3624
|
+
if (c.endLine < c.startLine)
|
|
3625
|
+
throw new HubError(422, "AI_CITATION_INVALID", "Citation line range must be ordered.");
|
|
3626
|
+
if (!file) {
|
|
3627
|
+
reader.limitations.add(`\u5F15\u7528\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${c.path}`);
|
|
3628
|
+
return [];
|
|
3629
|
+
}
|
|
3630
|
+
return [{ ...c, path: file.path, sha256: file.sha256 }];
|
|
2906
3631
|
});
|
|
2907
3632
|
return AiResultSchema.parse({
|
|
2908
3633
|
...output,
|
|
2909
3634
|
citations,
|
|
2910
|
-
|
|
3635
|
+
uncertainties: output.uncertainties ?? [],
|
|
3636
|
+
coverage: coverage(reader, native)
|
|
2911
3637
|
});
|
|
2912
3638
|
}
|
|
2913
|
-
function coverage(reader) {
|
|
3639
|
+
function coverage(reader, native = false) {
|
|
2914
3640
|
return {
|
|
2915
3641
|
totalFiles: reader.snapshot.files.length,
|
|
2916
3642
|
readFiles: [...reader.readFiles],
|
|
2917
|
-
|
|
3643
|
+
...native ? { accessTracking: "unavailable" } : {},
|
|
2918
3644
|
limitations: [...reader.limitations]
|
|
2919
3645
|
};
|
|
2920
3646
|
}
|
|
@@ -2931,25 +3657,23 @@ async function analyzeDirectory(job, reader, key, signal, progress, runtime) {
|
|
|
2931
3657
|
workflowPrompt(runtime, SummaryOutputSchema),
|
|
2932
3658
|
{
|
|
2933
3659
|
owner: job.snapshot.owner,
|
|
3660
|
+
workspace: `/workspaces/${job.snapshot.owner}/`,
|
|
2934
3661
|
scope: job.snapshot.path || "/",
|
|
2935
3662
|
revision: job.snapshot.revision,
|
|
2936
3663
|
totalFiles: job.snapshot.files.length,
|
|
2937
3664
|
question: job.request.question
|
|
2938
3665
|
}
|
|
2939
3666
|
);
|
|
2940
|
-
return {
|
|
3667
|
+
return {
|
|
3668
|
+
result: validateResult(output.text, reader, runtime.config.execution === "native-sandbox"),
|
|
3669
|
+
usage: output.usage
|
|
3670
|
+
};
|
|
2941
3671
|
}
|
|
2942
3672
|
|
|
2943
3673
|
// apps/server/ai/workflows/file-summary.ts
|
|
2944
3674
|
async function summarizeFile(job, reader, key, signal, runtime) {
|
|
2945
3675
|
const path = job.snapshot.path;
|
|
2946
|
-
const body = await reader.whole(
|
|
2947
|
-
path,
|
|
2948
|
-
Math.min(
|
|
2949
|
-
runtime.config.maxReadBytes,
|
|
2950
|
-
Math.max(1024, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
|
|
2951
|
-
)
|
|
2952
|
-
);
|
|
3676
|
+
const body = await reader.whole(path);
|
|
2953
3677
|
const context = {
|
|
2954
3678
|
systemPrompt: workflowPrompt(runtime, SummaryOutputSchema),
|
|
2955
3679
|
messages: [
|
|
@@ -2964,13 +3688,6 @@ async function summarizeFile(job, reader, key, signal, runtime) {
|
|
|
2964
3688
|
}
|
|
2965
3689
|
]
|
|
2966
3690
|
};
|
|
2967
|
-
if (Buffer.byteLength(JSON.stringify(context)) + job.profile.maxOutputTokens > job.profile.contextWindow) {
|
|
2968
|
-
throw new HubError(
|
|
2969
|
-
413,
|
|
2970
|
-
"AI_CONTEXT_OR_ROUND_LIMIT",
|
|
2971
|
-
"File exceeds the analysis context budget."
|
|
2972
|
-
);
|
|
2973
|
-
}
|
|
2974
3691
|
const { message } = await modelCall(
|
|
2975
3692
|
job.profile,
|
|
2976
3693
|
key,
|
|
@@ -2988,61 +3705,81 @@ async function summarizeFile(job, reader, key, signal, runtime) {
|
|
|
2988
3705
|
}
|
|
2989
3706
|
|
|
2990
3707
|
// apps/server/ai/workflows/task-bootstrap.ts
|
|
2991
|
-
import { z as
|
|
2992
|
-
var
|
|
2993
|
-
tasks:
|
|
3708
|
+
import { z as z12 } from "zod";
|
|
3709
|
+
var LegacyTaskOutputSchema = z12.object({
|
|
3710
|
+
tasks: z12.array(
|
|
3711
|
+
TaskFieldsSchema.extend({
|
|
3712
|
+
taskId: z12.uuid().nullable().optional(),
|
|
3713
|
+
sources: z12.array(z12.string())
|
|
3714
|
+
})
|
|
3715
|
+
),
|
|
3716
|
+
uncertainties: z12.array(z12.string()).optional()
|
|
3717
|
+
}).strict();
|
|
3718
|
+
var LegacyPatchOutputSchema = z12.object({
|
|
3719
|
+
upsert: z12.array(
|
|
3720
|
+
TaskFieldsSchema.extend({ taskId: z12.uuid().nullable(), sources: z12.array(z12.string()) })
|
|
3721
|
+
),
|
|
3722
|
+
archive: z12.array(z12.uuid()),
|
|
3723
|
+
uncertainties: z12.array(z12.string()).optional()
|
|
3724
|
+
}).strict();
|
|
3725
|
+
var TaskOutputSchema = z12.object({
|
|
3726
|
+
upsert: z12.array(
|
|
2994
3727
|
TaskFieldsSchema.extend({
|
|
2995
|
-
taskId:
|
|
2996
|
-
sources:
|
|
3728
|
+
taskId: z12.uuid().nullable(),
|
|
3729
|
+
sources: z12.array(z12.string()),
|
|
3730
|
+
needsConfirmation: z12.boolean()
|
|
2997
3731
|
})
|
|
2998
|
-
)
|
|
2999
|
-
|
|
3732
|
+
),
|
|
3733
|
+
archive: z12.array(z12.uuid())
|
|
3000
3734
|
}).strict();
|
|
3001
|
-
function validateTasks(text, reader, context) {
|
|
3735
|
+
function validateTasks(text, reader, context, patch = true, native = false) {
|
|
3002
3736
|
let output;
|
|
3003
3737
|
try {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3738
|
+
const json = JSON.parse(text.trim().replace(/^```(?:json)?\s*\n([\s\S]*)\n```$/, "$1"));
|
|
3739
|
+
if (patch) {
|
|
3740
|
+
const parsed = native ? TaskOutputSchema.parse(json) : LegacyPatchOutputSchema.parse(json);
|
|
3741
|
+
output = {
|
|
3742
|
+
tasks: parsed.upsert,
|
|
3743
|
+
archive: parsed.archive,
|
|
3744
|
+
uncertainties: native ? void 0 : parsed.uncertainties
|
|
3745
|
+
};
|
|
3746
|
+
} else output = LegacyTaskOutputSchema.parse(json);
|
|
3007
3747
|
} catch {
|
|
3008
|
-
throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce
|
|
3748
|
+
throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce valid task changes.");
|
|
3009
3749
|
}
|
|
3010
|
-
if (!reader.readFiles.size)
|
|
3011
|
-
throw new HubError(
|
|
3012
|
-
422,
|
|
3013
|
-
"AI_TASK_SOURCE_INVALID",
|
|
3014
|
-
"Read workspace material before deciding whether it contains tasks."
|
|
3015
|
-
);
|
|
3016
|
-
const keys = output.tasks.map((task) => JSON.stringify([task.title, [...task.sources].sort()]));
|
|
3017
|
-
if (new Set(keys).size !== keys.length)
|
|
3018
|
-
throw new HubError(422, "AI_TASK_DUPLICATE", "Task list contains identical entries.");
|
|
3019
3750
|
const previous = new Map(context?.tasks.map((task) => [task.taskId, task]));
|
|
3020
3751
|
const used = /* @__PURE__ */ new Set();
|
|
3021
|
-
for (const
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
return current && old.sources.some((source) => source.path === path || source.sha256 === current.sha256);
|
|
3027
|
-
}))
|
|
3752
|
+
for (const id of [
|
|
3753
|
+
...output.tasks.flatMap((task) => task.taskId ? [task.taskId] : []),
|
|
3754
|
+
...output.archive ?? []
|
|
3755
|
+
]) {
|
|
3756
|
+
if (!previous.has(id) || used.has(id))
|
|
3028
3757
|
throw new HubError(
|
|
3029
3758
|
422,
|
|
3030
3759
|
"AI_TASK_ID_INVALID",
|
|
3031
|
-
"Existing task identity must be unique and
|
|
3760
|
+
"Existing task identity must be unique and belong to this member."
|
|
3032
3761
|
);
|
|
3033
|
-
used.add(
|
|
3762
|
+
used.add(id);
|
|
3034
3763
|
}
|
|
3035
3764
|
return TaskAnalysisResultSchema.parse({
|
|
3036
3765
|
kind: "task-bootstrap",
|
|
3766
|
+
...patch ? { updateMode: "patch", archive: output.archive } : {},
|
|
3037
3767
|
tasks: output.tasks.map((task) => ({
|
|
3038
3768
|
...task,
|
|
3039
|
-
sources: task.sources.
|
|
3769
|
+
sources: [...new Set(task.sources)].flatMap((path) => {
|
|
3770
|
+
const relative = native ? memberSourcePath(path, reader.snapshot.owner) : path;
|
|
3771
|
+
const source = reader.source(relative ?? path);
|
|
3772
|
+
return source ? [source] : [];
|
|
3773
|
+
})
|
|
3040
3774
|
})),
|
|
3041
|
-
uncertainties: output.uncertainties,
|
|
3042
|
-
coverage: coverage(reader)
|
|
3775
|
+
...!native ? { uncertainties: output.uncertainties ?? [] } : {},
|
|
3776
|
+
coverage: coverage(reader, native)
|
|
3043
3777
|
});
|
|
3044
3778
|
}
|
|
3045
3779
|
async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
|
|
3780
|
+
const patch = runtime.config.version >= 7;
|
|
3781
|
+
const native = runtime.config.execution === "native-sandbox";
|
|
3782
|
+
const explorer = patch && !native ? new TaskExplorer(reader, job.taskContext, signal) : void 0;
|
|
3046
3783
|
const output = await runAgent(
|
|
3047
3784
|
job.profile,
|
|
3048
3785
|
reader,
|
|
@@ -3050,32 +3787,34 @@ async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
|
|
|
3050
3787
|
signal,
|
|
3051
3788
|
progress,
|
|
3052
3789
|
runtime,
|
|
3053
|
-
workflowPrompt(
|
|
3054
|
-
|
|
3790
|
+
workflowPrompt(
|
|
3791
|
+
runtime,
|
|
3792
|
+
native ? TaskOutputSchema : patch ? LegacyPatchOutputSchema : LegacyTaskOutputSchema
|
|
3793
|
+
),
|
|
3794
|
+
native ? await taskInput(reader, job.taskContext, signal) : explorer?.overview() ?? {
|
|
3055
3795
|
owner: job.snapshot.owner,
|
|
3056
3796
|
revision: job.snapshot.revision,
|
|
3057
|
-
inventory: reader.list(
|
|
3058
|
-
previous: job.taskContext
|
|
3059
|
-
|
|
3060
|
-
|
|
3797
|
+
inventory: reader.list(),
|
|
3798
|
+
previous: job.taskContext ? {
|
|
3799
|
+
previousRevision: job.taskContext.previousRevision,
|
|
3800
|
+
changes: job.taskContext.changes,
|
|
3801
|
+
tasks: job.taskContext.tasks.filter((task) => task.active !== false)
|
|
3802
|
+
} : null,
|
|
3803
|
+
projectReferences: job.snapshot.project ? reader.projectReader().list() : null
|
|
3804
|
+
},
|
|
3805
|
+
explorer,
|
|
3806
|
+
job.taskContext
|
|
3061
3807
|
);
|
|
3062
|
-
return {
|
|
3808
|
+
return {
|
|
3809
|
+
result: validateTasks(output.text, reader, job.taskContext, patch, native),
|
|
3810
|
+
usage: output.usage
|
|
3811
|
+
};
|
|
3063
3812
|
}
|
|
3064
3813
|
|
|
3065
3814
|
// apps/server/ai/bootstrap.ts
|
|
3066
|
-
function requestTaskBootstrap(hub, session, owner, operationId) {
|
|
3067
|
-
requireAnalysisAccess(hub, session, owner);
|
|
3068
|
-
if (!new FileRepository(hub).manifest(session, owner, void 0, "", 1).files.length && !hub.db.prepare("SELECT 1 FROM task_board_members WHERE workspace_id=? AND owner=?").get(session.workspaceId, owner))
|
|
3069
|
-
return { jobId: null, empty: true };
|
|
3070
|
-
const { job } = new AiJobs(hub).create(session, {
|
|
3071
|
-
operationId,
|
|
3072
|
-
workflow: "task-bootstrap",
|
|
3073
|
-
question: "",
|
|
3074
|
-
selection: { owner, kind: "directory", path: "" }
|
|
3075
|
-
});
|
|
3076
|
-
return { jobId: job.id, empty: false };
|
|
3077
|
-
}
|
|
3078
3815
|
function scheduleTaskBootstraps(hub, at = Date.now()) {
|
|
3816
|
+
const jobs = new AiJobs(hub);
|
|
3817
|
+
jobs.cancelStaleTaskJobs();
|
|
3079
3818
|
const candidates = hub.db.prepare(
|
|
3080
3819
|
`SELECT m.workspace_id,m.member_id,MAX(r.created_at,COALESCE(pr.created_at,r.created_at)) created_at
|
|
3081
3820
|
FROM members m JOIN projects p ON p.id=m.workspace_id JOIN users u ON u.id=m.member_id
|
|
@@ -3091,7 +3830,7 @@ function scheduleTaskBootstraps(hub, at = Date.now()) {
|
|
|
3091
3830
|
).all();
|
|
3092
3831
|
if (!candidates.length) return 0;
|
|
3093
3832
|
const runtime = loadWorkflow("task-bootstrap");
|
|
3094
|
-
const defaults = new AiDefaults(hub)
|
|
3833
|
+
const defaults = new AiDefaults(hub);
|
|
3095
3834
|
let queued = 0;
|
|
3096
3835
|
for (const row of candidates) {
|
|
3097
3836
|
if (at - Date.parse(String(row.created_at)) < runtime.config.settleSeconds * 1e3) continue;
|
|
@@ -3162,9 +3901,10 @@ async function runAiCycle(hub, stop) {
|
|
|
3162
3901
|
job,
|
|
3163
3902
|
{
|
|
3164
3903
|
kind: "task-bootstrap",
|
|
3904
|
+
...runtime.config.version >= 7 ? { updateMode: "patch", archive: [] } : {},
|
|
3165
3905
|
tasks: [],
|
|
3166
|
-
uncertainties: [],
|
|
3167
|
-
coverage: { totalFiles: 0, readFiles: [],
|
|
3906
|
+
...runtime.config.execution !== "native-sandbox" ? { uncertainties: [] } : {},
|
|
3907
|
+
coverage: { totalFiles: 0, readFiles: [], limitations: [] }
|
|
3168
3908
|
},
|
|
3169
3909
|
null,
|
|
3170
3910
|
{ inputTokens: 0, outputTokens: 0 }
|
|
@@ -3181,10 +3921,7 @@ async function runAiCycle(hub, stop) {
|
|
|
3181
3921
|
hub,
|
|
3182
3922
|
{ workspaceId: job.workspace, memberId: job.requester },
|
|
3183
3923
|
job.snapshot,
|
|
3184
|
-
|
|
3185
|
-
runtime.config.maxReadBytes,
|
|
3186
|
-
Math.max(2048, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
|
|
3187
|
-
)
|
|
3924
|
+
runtime.config.maxReadBytes
|
|
3188
3925
|
);
|
|
3189
3926
|
const key = config.secret(profile.id);
|
|
3190
3927
|
const output = job.request.workflow === "file-summary" ? await summarizeFile(job, reader, key, signal, runtime) : await (job.request.workflow === "task-bootstrap" ? bootstrapTasks : analyzeDirectory)(
|
|
@@ -3243,11 +3980,11 @@ async function runAiWorker(directory, once = false) {
|
|
|
3243
3980
|
}
|
|
3244
3981
|
|
|
3245
3982
|
// apps/server/main.ts
|
|
3246
|
-
import { fileURLToPath as
|
|
3983
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3247
3984
|
import { parseArgs } from "node:util";
|
|
3248
3985
|
|
|
3249
3986
|
// apps/server/ai/job-routes.ts
|
|
3250
|
-
import { z as
|
|
3987
|
+
import { z as z13 } from "zod";
|
|
3251
3988
|
|
|
3252
3989
|
// apps/server/web-auth.ts
|
|
3253
3990
|
function cookieToken(request) {
|
|
@@ -3293,41 +4030,29 @@ function authorizeRead(store, request) {
|
|
|
3293
4030
|
// apps/server/ai/job-routes.ts
|
|
3294
4031
|
function registerAiJobRoutes(server, hub) {
|
|
3295
4032
|
const jobs = new AiJobs(hub);
|
|
3296
|
-
const id = (params) =>
|
|
4033
|
+
const id = (params) => z13.object({ id: z13.uuid() }).parse(params).id;
|
|
3297
4034
|
server.get("/ai/jobs", async (req) => {
|
|
3298
|
-
const session = authorizeWeb(hub, req), query =
|
|
3299
|
-
after:
|
|
4035
|
+
const session = authorizeWeb(hub, req), query = z13.object({
|
|
4036
|
+
after: z13.string().regex(/^\d{0,15}$/).default("")
|
|
3300
4037
|
}).parse(req.query);
|
|
3301
4038
|
return jobs.list(session.workspaceId, query.after);
|
|
3302
4039
|
});
|
|
3303
|
-
server.post("/ai/jobs", async (req) => {
|
|
3304
|
-
webMutation(req);
|
|
3305
|
-
return jobs.create(authorizeWeb(hub, req), AiJobCreateSchema.parse(req.body));
|
|
3306
|
-
});
|
|
3307
4040
|
server.get(
|
|
3308
4041
|
"/ai/jobs/:id",
|
|
3309
4042
|
async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
|
|
3310
4043
|
);
|
|
3311
4044
|
server.get("/ai/jobs/:id/events", async (req) => {
|
|
3312
|
-
const query =
|
|
4045
|
+
const query = z13.object({ after: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
|
|
3313
4046
|
return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
|
|
3314
4047
|
});
|
|
3315
4048
|
server.post("/ai/jobs/:id/cancel", async (req) => {
|
|
3316
4049
|
webMutation(req);
|
|
3317
4050
|
return jobs.cancel(authorizeWeb(hub, req), id(req.params));
|
|
3318
4051
|
});
|
|
3319
|
-
server.post("/ai/jobs/:id/retry", async (req) => {
|
|
3320
|
-
webMutation(req);
|
|
3321
|
-
return jobs.retry(
|
|
3322
|
-
authorizeWeb(hub, req),
|
|
3323
|
-
id(req.params),
|
|
3324
|
-
z11.object({ operationId: z11.uuid() }).strict().parse(req.body).operationId
|
|
3325
|
-
);
|
|
3326
|
-
});
|
|
3327
4052
|
server.get("/ai/jobs/:id/source", async (req) => {
|
|
3328
4053
|
const session = authorizeWeb(hub, req), jobId = id(req.params);
|
|
3329
4054
|
jobs.get(session.workspaceId, jobId);
|
|
3330
|
-
const query =
|
|
4055
|
+
const query = z13.object({ path: z13.string().max(1024), startLine: z13.coerce.number().int().min(1).default(1) }).parse(req.query);
|
|
3331
4056
|
const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
|
|
3332
4057
|
if (!row) throw new HubError(404, "AI_JOB_NOT_FOUND", "Task not found.");
|
|
3333
4058
|
return new SnapshotReader(hub, session, JSON.parse(String(row.snapshot_json))).read(
|
|
@@ -3339,16 +4064,11 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3339
4064
|
}
|
|
3340
4065
|
|
|
3341
4066
|
// apps/server/ai/task-routes.ts
|
|
3342
|
-
import { z as
|
|
4067
|
+
import { z as z14 } from "zod";
|
|
3343
4068
|
function registerTaskRoutes(server, hub) {
|
|
3344
4069
|
const store = new TaskStore(hub);
|
|
3345
|
-
const id = (params) =>
|
|
4070
|
+
const id = (params) => z14.object({ id: z14.uuid() }).parse(params).id;
|
|
3346
4071
|
server.get("/tasks", async (req) => store.board(authorizeWeb(hub, req)));
|
|
3347
|
-
server.post("/tasks/bootstrap", async (req) => {
|
|
3348
|
-
webMutation(req);
|
|
3349
|
-
const session = authorizeWeb(hub, req), input = TaskBootstrapRequestSchema.parse(req.body);
|
|
3350
|
-
return requestTaskBootstrap(hub, session, input.owner, input.operationId);
|
|
3351
|
-
});
|
|
3352
4072
|
server.patch("/tasks/:id/correction", async (req) => {
|
|
3353
4073
|
webMutation(req);
|
|
3354
4074
|
return store.correct(
|
|
@@ -3417,7 +4137,7 @@ function registerAiRoutes(server, store) {
|
|
|
3417
4137
|
profile,
|
|
3418
4138
|
config.secret(profile.id),
|
|
3419
4139
|
mode,
|
|
3420
|
-
AbortSignal.any([controller.signal, AbortSignal.timeout(
|
|
4140
|
+
AbortSignal.any([controller.signal, AbortSignal.timeout(3e5)])
|
|
3421
4141
|
);
|
|
3422
4142
|
} catch (e) {
|
|
3423
4143
|
if (e instanceof HubError) throw e;
|
|
@@ -3434,11 +4154,11 @@ function registerAiRoutes(server, store) {
|
|
|
3434
4154
|
}
|
|
3435
4155
|
|
|
3436
4156
|
// apps/server/static.ts
|
|
3437
|
-
import { join as
|
|
4157
|
+
import { join as join8 } from "node:path";
|
|
3438
4158
|
import fastifyStatic from "@fastify/static";
|
|
3439
4159
|
function registerWeb(server, root) {
|
|
3440
4160
|
server.register(fastifyStatic, {
|
|
3441
|
-
root:
|
|
4161
|
+
root: join8(root, "assets"),
|
|
3442
4162
|
prefix: "/assets/",
|
|
3443
4163
|
immutable: true,
|
|
3444
4164
|
maxAge: "1y",
|
|
@@ -3476,20 +4196,20 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
3476
4196
|
import Fastify from "fastify";
|
|
3477
4197
|
|
|
3478
4198
|
// packages/protocol/index.ts
|
|
3479
|
-
import { z as
|
|
4199
|
+
import { z as z15 } from "zod";
|
|
3480
4200
|
var PROTOCOL_VERSION = 1;
|
|
3481
|
-
var HealthSchema =
|
|
3482
|
-
var ServerInfoSchema =
|
|
3483
|
-
name:
|
|
3484
|
-
version:
|
|
3485
|
-
protocolVersion:
|
|
3486
|
-
capabilities:
|
|
4201
|
+
var HealthSchema = z15.object({ status: z15.literal("ok") });
|
|
4202
|
+
var ServerInfoSchema = z15.object({
|
|
4203
|
+
name: z15.literal("teammy"),
|
|
4204
|
+
version: z15.string().min(1),
|
|
4205
|
+
protocolVersion: z15.literal(PROTOCOL_VERSION),
|
|
4206
|
+
capabilities: z15.array(z15.string())
|
|
3487
4207
|
});
|
|
3488
|
-
var ErrorResponseSchema =
|
|
3489
|
-
error:
|
|
3490
|
-
code:
|
|
3491
|
-
message:
|
|
3492
|
-
requestId:
|
|
4208
|
+
var ErrorResponseSchema = z15.object({
|
|
4209
|
+
error: z15.object({
|
|
4210
|
+
code: z15.string(),
|
|
4211
|
+
message: z15.string(),
|
|
4212
|
+
requestId: z15.string()
|
|
3493
4213
|
})
|
|
3494
4214
|
});
|
|
3495
4215
|
|
|
@@ -3497,43 +4217,43 @@ var ErrorResponseSchema = z13.object({
|
|
|
3497
4217
|
import { ZodError } from "zod";
|
|
3498
4218
|
|
|
3499
4219
|
// packages/protocol/devices.ts
|
|
3500
|
-
import { z as
|
|
3501
|
-
var CredentialSchema =
|
|
3502
|
-
var EnrollmentSchema =
|
|
3503
|
-
operationId:
|
|
4220
|
+
import { z as z16 } from "zod";
|
|
4221
|
+
var CredentialSchema = z16.string().regex(/^tmy_[a-f0-9]{64}$/);
|
|
4222
|
+
var EnrollmentSchema = z16.object({
|
|
4223
|
+
operationId: z16.uuid(),
|
|
3504
4224
|
projectId: NameSchema,
|
|
3505
4225
|
memberId: NameSchema,
|
|
3506
4226
|
deviceId: DeviceIdSchema,
|
|
3507
4227
|
credential: CredentialSchema
|
|
3508
4228
|
}).strict();
|
|
3509
|
-
var EffectivePolicySchema =
|
|
3510
|
-
checkIntervalSeconds:
|
|
3511
|
-
fileSettleSeconds:
|
|
4229
|
+
var EffectivePolicySchema = z16.object({
|
|
4230
|
+
checkIntervalSeconds: z16.number().min(1).max(86400),
|
|
4231
|
+
fileSettleSeconds: z16.number().min(0).max(86400)
|
|
3512
4232
|
}).strict();
|
|
3513
|
-
var SyncSummarySchema =
|
|
3514
|
-
checkedAt:
|
|
3515
|
-
published:
|
|
3516
|
-
appliedFiles:
|
|
3517
|
-
pendingFiles:
|
|
3518
|
-
sessionSources:
|
|
3519
|
-
errors:
|
|
4233
|
+
var SyncSummarySchema = z16.object({
|
|
4234
|
+
checkedAt: z16.string(),
|
|
4235
|
+
published: z16.boolean(),
|
|
4236
|
+
appliedFiles: z16.number().int().nonnegative(),
|
|
4237
|
+
pendingFiles: z16.number().int().nonnegative(),
|
|
4238
|
+
sessionSources: z16.number().int().nonnegative(),
|
|
4239
|
+
errors: z16.array(z16.string().max(1e3)).max(20)
|
|
3520
4240
|
}).strict();
|
|
3521
|
-
var DeviceStatusSchema =
|
|
3522
|
-
clientVersion:
|
|
4241
|
+
var DeviceStatusSchema = z16.object({
|
|
4242
|
+
clientVersion: z16.string().max(64),
|
|
3523
4243
|
receivedRevision: RevisionSchema.nullable(),
|
|
3524
4244
|
appliedRevision: RevisionSchema.nullable(),
|
|
3525
4245
|
effectivePolicy: EffectivePolicySchema,
|
|
3526
|
-
overrides:
|
|
3527
|
-
configError:
|
|
3528
|
-
reportError:
|
|
4246
|
+
overrides: z16.array(z16.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
|
|
4247
|
+
configError: z16.string().max(2e3).nullable(),
|
|
4248
|
+
reportError: z16.string().max(2e3).nullable(),
|
|
3529
4249
|
lastSync: SyncSummarySchema.nullable()
|
|
3530
4250
|
}).strict();
|
|
3531
|
-
var WebDevicesSchema =
|
|
3532
|
-
devices:
|
|
3533
|
-
|
|
4251
|
+
var WebDevicesSchema = z16.object({
|
|
4252
|
+
devices: z16.array(
|
|
4253
|
+
z16.object({
|
|
3534
4254
|
memberId: NameSchema,
|
|
3535
|
-
bound:
|
|
3536
|
-
lastSeenAt:
|
|
4255
|
+
bound: z16.boolean(),
|
|
4256
|
+
lastSeenAt: z16.string().nullable(),
|
|
3537
4257
|
status: DeviceStatusSchema.nullable()
|
|
3538
4258
|
})
|
|
3539
4259
|
)
|
|
@@ -3571,7 +4291,7 @@ function registerControlRoutes(server, store) {
|
|
|
3571
4291
|
}
|
|
3572
4292
|
|
|
3573
4293
|
// apps/server/project-routes.ts
|
|
3574
|
-
import { z as
|
|
4294
|
+
import { z as z17 } from "zod";
|
|
3575
4295
|
function registerProjectRoutes(server, store) {
|
|
3576
4296
|
const project = new ProjectRepository(store), site = new SiteRepository(store);
|
|
3577
4297
|
server.get("/api/v1/projects", async (request) => ({
|
|
@@ -3618,7 +4338,7 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3618
4338
|
});
|
|
3619
4339
|
server.patch("/web/members/:id", async (request) => {
|
|
3620
4340
|
webMutation(request);
|
|
3621
|
-
const { id } =
|
|
4341
|
+
const { id } = z17.object({ id: NameSchema }).parse(request.params);
|
|
3622
4342
|
return project.updateMember(
|
|
3623
4343
|
authorizeWeb(store, request),
|
|
3624
4344
|
id,
|
|
@@ -3627,13 +4347,13 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3627
4347
|
});
|
|
3628
4348
|
server.post("/web/members/:id/device-reset", async (request) => {
|
|
3629
4349
|
webMutation(request);
|
|
3630
|
-
const { id } =
|
|
4350
|
+
const { id } = z17.object({ id: NameSchema }).parse(request.params);
|
|
3631
4351
|
return project.resetDevice(authorizeWeb(store, request), id);
|
|
3632
4352
|
});
|
|
3633
4353
|
}
|
|
3634
4354
|
|
|
3635
4355
|
// apps/server/site-routes.ts
|
|
3636
|
-
import { z as
|
|
4356
|
+
import { z as z18 } from "zod";
|
|
3637
4357
|
|
|
3638
4358
|
// apps/server/devices.ts
|
|
3639
4359
|
var DeviceRepository = class {
|
|
@@ -3709,7 +4429,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3709
4429
|
"/api/v1/web/session",
|
|
3710
4430
|
{ onRequest: async (request) => webMutation(request) },
|
|
3711
4431
|
async (request, reply) => {
|
|
3712
|
-
const { id } =
|
|
4432
|
+
const { id } = z18.object({ id: NameSchema }).strict().parse(request.body);
|
|
3713
4433
|
const { token, session } = site.login(id);
|
|
3714
4434
|
const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
|
|
3715
4435
|
reply.header(
|
|
@@ -3723,7 +4443,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3723
4443
|
server.patch("/api/v1/web/session", async (request) => {
|
|
3724
4444
|
webMutation(request);
|
|
3725
4445
|
const identity = site.identity(cookieToken(request));
|
|
3726
|
-
const { displayName } =
|
|
4446
|
+
const { displayName } = z18.object({ displayName: z18.string().trim().max(120) }).strict().parse(request.body);
|
|
3727
4447
|
return site.renameSelf(identity, displayName);
|
|
3728
4448
|
});
|
|
3729
4449
|
server.delete("/api/v1/web/session", async (request, reply) => {
|
|
@@ -3764,71 +4484,71 @@ function registerSiteRoutes(server, store) {
|
|
|
3764
4484
|
}
|
|
3765
4485
|
|
|
3766
4486
|
// apps/server/session-routes.ts
|
|
3767
|
-
import { z as
|
|
4487
|
+
import { z as z20 } from "zod";
|
|
3768
4488
|
|
|
3769
4489
|
// packages/protocol/sessions.ts
|
|
3770
|
-
import { z as
|
|
3771
|
-
var AgentSchema =
|
|
3772
|
-
var SessionSnapshotSchema =
|
|
3773
|
-
formatVersion:
|
|
3774
|
-
adapterVersion:
|
|
4490
|
+
import { z as z19 } from "zod";
|
|
4491
|
+
var AgentSchema = z19.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
|
|
4492
|
+
var SessionSnapshotSchema = z19.object({
|
|
4493
|
+
formatVersion: z19.literal(1),
|
|
4494
|
+
adapterVersion: z19.literal(1),
|
|
3775
4495
|
agent: AgentSchema,
|
|
3776
|
-
nativeId:
|
|
3777
|
-
cwd:
|
|
3778
|
-
messages:
|
|
3779
|
-
|
|
3780
|
-
id:
|
|
3781
|
-
role:
|
|
3782
|
-
text:
|
|
3783
|
-
source:
|
|
4496
|
+
nativeId: z19.string().min(1).max(512),
|
|
4497
|
+
cwd: z19.string().min(1).max(4096),
|
|
4498
|
+
messages: z19.array(
|
|
4499
|
+
z19.object({
|
|
4500
|
+
id: z19.string().min(1).max(1024),
|
|
4501
|
+
role: z19.enum(["user", "assistant"]),
|
|
4502
|
+
text: z19.string().min(1).max(2 * 1024 * 1024),
|
|
4503
|
+
source: z19.string().min(1).max(1024)
|
|
3784
4504
|
}).strict()
|
|
3785
4505
|
).max(5e4)
|
|
3786
4506
|
}).strict().refine(
|
|
3787
4507
|
(s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
|
|
3788
4508
|
"Duplicate message IDs."
|
|
3789
4509
|
);
|
|
3790
|
-
var SessionCommitSchema =
|
|
3791
|
-
operationId:
|
|
4510
|
+
var SessionCommitSchema = z19.object({
|
|
4511
|
+
operationId: z19.uuid(),
|
|
3792
4512
|
sessionKey: HashSchema,
|
|
3793
4513
|
parentRevision: RevisionSchema,
|
|
3794
4514
|
blob: BlobRefSchema
|
|
3795
4515
|
}).strict();
|
|
3796
|
-
var SessionInfoSchema =
|
|
4516
|
+
var SessionInfoSchema = z19.object({
|
|
3797
4517
|
owner: NameSchema,
|
|
3798
4518
|
sessionKey: HashSchema,
|
|
3799
4519
|
agent: AgentSchema,
|
|
3800
|
-
nativeId:
|
|
3801
|
-
cwd:
|
|
3802
|
-
title:
|
|
4520
|
+
nativeId: z19.string(),
|
|
4521
|
+
cwd: z19.string(),
|
|
4522
|
+
title: z19.string(),
|
|
3803
4523
|
revision: RevisionSchema,
|
|
3804
|
-
updatedAt:
|
|
3805
|
-
messageCount:
|
|
4524
|
+
updatedAt: z19.string(),
|
|
4525
|
+
messageCount: z19.number().int().nonnegative(),
|
|
3806
4526
|
blob: BlobRefSchema
|
|
3807
4527
|
});
|
|
3808
|
-
var SessionListSchema =
|
|
3809
|
-
sessions:
|
|
4528
|
+
var SessionListSchema = z19.object({
|
|
4529
|
+
sessions: z19.array(SessionInfoSchema),
|
|
3810
4530
|
nextCursor: RevisionSchema.nullable()
|
|
3811
4531
|
});
|
|
3812
|
-
var SessionStatsSchema =
|
|
3813
|
-
totalSessions:
|
|
3814
|
-
totalMessages:
|
|
3815
|
-
memberCount:
|
|
3816
|
-
updatedAt:
|
|
4532
|
+
var SessionStatsSchema = z19.object({
|
|
4533
|
+
totalSessions: z19.number().int().nonnegative(),
|
|
4534
|
+
totalMessages: z19.number().int().nonnegative(),
|
|
4535
|
+
memberCount: z19.number().int().nonnegative(),
|
|
4536
|
+
updatedAt: z19.string().nullable()
|
|
3817
4537
|
});
|
|
3818
|
-
var SessionScopeSchema =
|
|
4538
|
+
var SessionScopeSchema = z19.object({
|
|
3819
4539
|
owner: NameSchema.optional(),
|
|
3820
4540
|
sessionKey: HashSchema.optional()
|
|
3821
4541
|
}).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
|
|
3822
|
-
var SessionSearchSchema =
|
|
3823
|
-
matches:
|
|
3824
|
-
|
|
4542
|
+
var SessionSearchSchema = z19.object({
|
|
4543
|
+
matches: z19.array(
|
|
4544
|
+
z19.object({
|
|
3825
4545
|
owner: NameSchema,
|
|
3826
4546
|
sessionKey: HashSchema,
|
|
3827
4547
|
revision: RevisionSchema,
|
|
3828
|
-
messageId:
|
|
3829
|
-
role:
|
|
3830
|
-
snippet:
|
|
3831
|
-
title:
|
|
4548
|
+
messageId: z19.string(),
|
|
4549
|
+
role: z19.enum(["user", "assistant"]),
|
|
4550
|
+
snippet: z19.string(),
|
|
4551
|
+
title: z19.string()
|
|
3832
4552
|
})
|
|
3833
4553
|
),
|
|
3834
4554
|
nextCursor: RevisionSchema.nullable()
|
|
@@ -3989,31 +4709,46 @@ var SessionRepository = class {
|
|
|
3989
4709
|
).get(identity.workspaceId)
|
|
3990
4710
|
);
|
|
3991
4711
|
}
|
|
3992
|
-
list(identity, after, limit, owner) {
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
4712
|
+
list(identity, after, limit, owner, order = "created") {
|
|
4713
|
+
const byTitle = order === "title";
|
|
4714
|
+
let rows = this.hub.db.prepare(
|
|
4715
|
+
`SELECT s.id, s.title, s.native_id, r.info_json FROM sessions s JOIN session_revisions r USING(workspace_id, owner, session_key, revision)
|
|
4716
|
+
WHERE s.workspace_id=? ${byTitle ? "" : "AND s.id>?"} ${owner ? "AND s.owner=?" : ""} ${byTitle ? "" : "ORDER BY s.id LIMIT ?"}`
|
|
4717
|
+
).all(
|
|
4718
|
+
identity.workspaceId,
|
|
4719
|
+
...byTitle ? [] : [after],
|
|
4720
|
+
...owner ? [owner] : [],
|
|
4721
|
+
...byTitle ? [] : [limit + 1]
|
|
4722
|
+
);
|
|
4723
|
+
if (byTitle) {
|
|
4724
|
+
rows.sort(
|
|
4725
|
+
(a, b) => compareNames(String(a.title || a.native_id), String(b.title || b.native_id)) || Number(a.id) - Number(b.id)
|
|
4726
|
+
);
|
|
4727
|
+
const offset = after ? rows.findIndex((row) => row.id === after) + 1 : 0;
|
|
4728
|
+
if (after && !offset)
|
|
4729
|
+
throw new HubError(400, "INVALID_CURSOR", "Session cursor is not in this scope.");
|
|
4730
|
+
rows = rows.slice(offset, offset + limit + 1);
|
|
4731
|
+
}
|
|
3997
4732
|
return {
|
|
3998
4733
|
sessions: rows.slice(0, limit).map((row) => SessionInfoSchema.parse(JSON.parse(String(row.info_json)))),
|
|
3999
4734
|
nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
|
|
4000
4735
|
};
|
|
4001
4736
|
}
|
|
4002
|
-
search(identity, query, after, limit,
|
|
4003
|
-
SessionScopeSchema.parse(
|
|
4737
|
+
search(identity, query, after, limit, scope2 = {}) {
|
|
4738
|
+
SessionScopeSchema.parse(scope2);
|
|
4004
4739
|
const lower = query.toLowerCase();
|
|
4005
4740
|
const indexed = [...lower].length >= 3;
|
|
4006
4741
|
const rows = this.hub.db.prepare(
|
|
4007
4742
|
`SELECT m.*,s.title,s.native_id FROM session_messages m JOIN sessions s USING(workspace_id,owner,session_key)
|
|
4008
4743
|
WHERE m.workspace_id=? AND m.id>? AND instr(m.search_text, ?)>0
|
|
4009
|
-
${
|
|
4744
|
+
${scope2.owner ? "AND m.owner=?" : ""} ${scope2.sessionKey ? "AND m.session_key=?" : ""}
|
|
4010
4745
|
${indexed ? "AND m.id IN (SELECT rowid FROM session_fts WHERE session_fts MATCH ?)" : ""} ORDER BY m.id LIMIT ?`
|
|
4011
4746
|
).all(
|
|
4012
4747
|
identity.workspaceId,
|
|
4013
4748
|
after,
|
|
4014
4749
|
lower,
|
|
4015
|
-
...
|
|
4016
|
-
...
|
|
4750
|
+
...scope2.owner ? [scope2.owner] : [],
|
|
4751
|
+
...scope2.sessionKey ? [scope2.sessionKey] : [],
|
|
4017
4752
|
...indexed ? [`"${lower.replaceAll('"', '""')}"`] : [],
|
|
4018
4753
|
limit + 1
|
|
4019
4754
|
);
|
|
@@ -4045,9 +4780,9 @@ function registerSessionRoutes(server, store) {
|
|
|
4045
4780
|
authorize(request);
|
|
4046
4781
|
}
|
|
4047
4782
|
};
|
|
4048
|
-
const pagination =
|
|
4049
|
-
after:
|
|
4050
|
-
limit:
|
|
4783
|
+
const pagination = z20.object({
|
|
4784
|
+
after: z20.coerce.number().pipe(RevisionSchema).default(0),
|
|
4785
|
+
limit: z20.coerce.number().int().min(1).max(100).default(50)
|
|
4051
4786
|
});
|
|
4052
4787
|
server.post(
|
|
4053
4788
|
"/sessions/commits",
|
|
@@ -4059,15 +4794,18 @@ function registerSessionRoutes(server, store) {
|
|
|
4059
4794
|
)
|
|
4060
4795
|
);
|
|
4061
4796
|
server.get("/sessions", async (request) => {
|
|
4062
|
-
const q = pagination.extend({
|
|
4063
|
-
|
|
4797
|
+
const q = pagination.extend({
|
|
4798
|
+
owner: NameSchema.optional(),
|
|
4799
|
+
order: z20.enum(["created", "title"]).default("created")
|
|
4800
|
+
}).parse(request.query);
|
|
4801
|
+
return sessions.list(authorizeRead(store, request), q.after, q.limit, q.owner, q.order);
|
|
4064
4802
|
});
|
|
4065
4803
|
server.get("/sessions/stats", async (request) => sessions.stats(authorizeRead(store, request)));
|
|
4066
4804
|
server.get("/sessions/search", async (request) => {
|
|
4067
4805
|
const q = pagination.extend({
|
|
4068
4806
|
owner: NameSchema.optional(),
|
|
4069
4807
|
sessionKey: HashSchema.optional(),
|
|
4070
|
-
q:
|
|
4808
|
+
q: z20.string().min(1).max(500).refine((s) => !s.includes("\0"))
|
|
4071
4809
|
}).parse(request.query);
|
|
4072
4810
|
return sessions.search(
|
|
4073
4811
|
authorizeRead(store, request),
|
|
@@ -4078,14 +4816,14 @@ function registerSessionRoutes(server, store) {
|
|
|
4078
4816
|
);
|
|
4079
4817
|
});
|
|
4080
4818
|
server.get("/sessions/:owner/:key", async (request) => {
|
|
4081
|
-
const p =
|
|
4082
|
-
const q =
|
|
4819
|
+
const p = z20.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
|
|
4820
|
+
const q = z20.object({ revision: z20.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
|
|
4083
4821
|
return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
|
|
4084
4822
|
});
|
|
4085
4823
|
}
|
|
4086
4824
|
|
|
4087
4825
|
// apps/server/file-routes.ts
|
|
4088
|
-
import { z as
|
|
4826
|
+
import { z as z21 } from "zod";
|
|
4089
4827
|
function registerFileRoutes(server, store) {
|
|
4090
4828
|
const files = new FileRepository(store);
|
|
4091
4829
|
const shared = new SharedFilesRepository(store);
|
|
@@ -4116,13 +4854,13 @@ function registerFileRoutes(server, store) {
|
|
|
4116
4854
|
missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
|
|
4117
4855
|
}));
|
|
4118
4856
|
server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
|
|
4119
|
-
const { hash: hash2 } =
|
|
4857
|
+
const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
|
|
4120
4858
|
if (!Buffer.isBuffer(request.body))
|
|
4121
4859
|
throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
|
|
4122
4860
|
return files.upload(request.headers.authorization, device(request), hash2, request.body);
|
|
4123
4861
|
});
|
|
4124
4862
|
server.get("/blobs/:hash", async (request, reply) => {
|
|
4125
|
-
const { hash: hash2 } =
|
|
4863
|
+
const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
|
|
4126
4864
|
return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
|
|
4127
4865
|
});
|
|
4128
4866
|
server.post(
|
|
@@ -4139,11 +4877,11 @@ function registerFileRoutes(server, store) {
|
|
|
4139
4877
|
return files.commit(request.headers.authorization, device(request), batch, configRevision);
|
|
4140
4878
|
});
|
|
4141
4879
|
server.get("/files/:owner/manifest", async (request) => {
|
|
4142
|
-
const { owner } =
|
|
4143
|
-
const query =
|
|
4144
|
-
revision:
|
|
4145
|
-
after:
|
|
4146
|
-
limit:
|
|
4880
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4881
|
+
const query = z21.object({
|
|
4882
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4883
|
+
after: z21.string().max(1024).default(""),
|
|
4884
|
+
limit: z21.coerce.number().int().min(1).max(500).default(200)
|
|
4147
4885
|
}).parse(request.query);
|
|
4148
4886
|
return files.manifest(
|
|
4149
4887
|
authorizeRead(store, request),
|
|
@@ -4154,28 +4892,28 @@ function registerFileRoutes(server, store) {
|
|
|
4154
4892
|
);
|
|
4155
4893
|
});
|
|
4156
4894
|
server.get("/files/:owner/entry", async (request) => {
|
|
4157
|
-
const { owner } =
|
|
4158
|
-
const q =
|
|
4895
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4896
|
+
const q = z21.object({
|
|
4159
4897
|
path: FilePathSchema,
|
|
4160
|
-
revision:
|
|
4898
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
|
|
4161
4899
|
}).parse(request.query);
|
|
4162
4900
|
return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
|
|
4163
4901
|
});
|
|
4164
4902
|
server.get("/files/:owner/tree", async (request) => {
|
|
4165
|
-
const { owner } =
|
|
4166
|
-
const q =
|
|
4167
|
-
path:
|
|
4168
|
-
revision:
|
|
4169
|
-
after:
|
|
4170
|
-
limit:
|
|
4903
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4904
|
+
const q = z21.object({
|
|
4905
|
+
path: z21.union([z21.literal(""), FilePathSchema]).default(""),
|
|
4906
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4907
|
+
after: z21.string().max(255).default(""),
|
|
4908
|
+
limit: z21.coerce.number().int().min(1).max(200).default(100)
|
|
4171
4909
|
}).parse(request.query);
|
|
4172
4910
|
return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
|
|
4173
4911
|
});
|
|
4174
4912
|
server.get("/files/:owner/changes", async (request) => {
|
|
4175
|
-
const { owner } =
|
|
4176
|
-
const query =
|
|
4177
|
-
after:
|
|
4178
|
-
limit:
|
|
4913
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4914
|
+
const query = z21.object({
|
|
4915
|
+
after: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
4916
|
+
limit: z21.coerce.number().int().min(1).max(100).default(100)
|
|
4179
4917
|
}).parse(request.query);
|
|
4180
4918
|
return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
|
|
4181
4919
|
});
|
|
@@ -4244,14 +4982,14 @@ function buildServer(options) {
|
|
|
4244
4982
|
registerSiteRoutes(server, store);
|
|
4245
4983
|
registerProjectRoutes(server, store);
|
|
4246
4984
|
registerAiRoutes(server, store);
|
|
4247
|
-
const projectRoutes = async (
|
|
4248
|
-
registerProjectScopeRoutes(
|
|
4249
|
-
registerAiJobRoutes(
|
|
4250
|
-
registerTaskRoutes(
|
|
4251
|
-
registerControlRoutes(
|
|
4252
|
-
registerFileRoutes(
|
|
4253
|
-
registerSessionRoutes(
|
|
4254
|
-
|
|
4985
|
+
const projectRoutes = async (scope2) => {
|
|
4986
|
+
registerProjectScopeRoutes(scope2, store);
|
|
4987
|
+
registerAiJobRoutes(scope2, store);
|
|
4988
|
+
registerTaskRoutes(scope2, store);
|
|
4989
|
+
registerControlRoutes(scope2, store);
|
|
4990
|
+
registerFileRoutes(scope2, store);
|
|
4991
|
+
registerSessionRoutes(scope2, store);
|
|
4992
|
+
scope2.get("/members", async (request) => ({
|
|
4255
4993
|
members: store.members(authorizeDevice(store, request))
|
|
4256
4994
|
}));
|
|
4257
4995
|
};
|
|
@@ -4268,7 +5006,7 @@ function buildServer(options) {
|
|
|
4268
5006
|
|
|
4269
5007
|
// apps/server/main.ts
|
|
4270
5008
|
import { homedir } from "node:os";
|
|
4271
|
-
import { join as
|
|
5009
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
4272
5010
|
var help = `teammy-server \u2014 the Teammy collaboration hub
|
|
4273
5011
|
|
|
4274
5012
|
Usage: teammy-server [--host HOST] [--port PORT]
|
|
@@ -4293,13 +5031,13 @@ Open the server URL to initialize the Web dashboard and preset member IDs.
|
|
|
4293
5031
|
File synchronization, session search and manual AI analysis are available.
|
|
4294
5032
|
AI tasks require a separate ai-worker process using the same data directory.
|
|
4295
5033
|
`;
|
|
4296
|
-
async function runServerCli(args,
|
|
5034
|
+
async function runServerCli(args, version2) {
|
|
4297
5035
|
try {
|
|
4298
5036
|
if (args[0] === "ai-worker") {
|
|
4299
5037
|
const { values: values2 } = parseArgs({
|
|
4300
5038
|
args: args.slice(1),
|
|
4301
5039
|
options: {
|
|
4302
|
-
"data-dir": { type: "string", default:
|
|
5040
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") },
|
|
4303
5041
|
once: { type: "boolean" }
|
|
4304
5042
|
}
|
|
4305
5043
|
});
|
|
@@ -4315,7 +5053,7 @@ async function runServerCli(args, version) {
|
|
|
4315
5053
|
options: {
|
|
4316
5054
|
workspace: { type: "string" },
|
|
4317
5055
|
member: { type: "string" },
|
|
4318
|
-
"data-dir": { type: "string", default:
|
|
5056
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
|
|
4319
5057
|
}
|
|
4320
5058
|
});
|
|
4321
5059
|
if (!values2.workspace || !values2.member)
|
|
@@ -4340,7 +5078,7 @@ async function runServerCli(args, version) {
|
|
|
4340
5078
|
version: { type: "boolean", short: "v" },
|
|
4341
5079
|
host: { type: "string", default: "127.0.0.1" },
|
|
4342
5080
|
port: { type: "string", default: "4317" },
|
|
4343
|
-
"data-dir": { type: "string", default:
|
|
5081
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
|
|
4344
5082
|
}
|
|
4345
5083
|
});
|
|
4346
5084
|
if (values.help) {
|
|
@@ -4348,7 +5086,7 @@ async function runServerCli(args, version) {
|
|
|
4348
5086
|
return 0;
|
|
4349
5087
|
}
|
|
4350
5088
|
if (values.version) {
|
|
4351
|
-
process.stdout.write(`${
|
|
5089
|
+
process.stdout.write(`${version2}
|
|
4352
5090
|
`);
|
|
4353
5091
|
return 0;
|
|
4354
5092
|
}
|
|
@@ -4358,10 +5096,10 @@ async function runServerCli(args, version) {
|
|
|
4358
5096
|
}
|
|
4359
5097
|
if (!values.host?.trim()) throw new Error("Host must not be empty.");
|
|
4360
5098
|
const server = buildServer({
|
|
4361
|
-
version,
|
|
5099
|
+
version: version2,
|
|
4362
5100
|
logger: true,
|
|
4363
5101
|
dataDir: resolve3(values["data-dir"]),
|
|
4364
|
-
webDir:
|
|
5102
|
+
webDir: fileURLToPath3(new URL("./web/", import.meta.url))
|
|
4365
5103
|
});
|
|
4366
5104
|
try {
|
|
4367
5105
|
await server.listen({ host: values.host, port: Number(portText) });
|