@teammy/server 0.0.7 → 0.0.9
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 +9 -5
- package/dist/line-diff-worker.js +32 -0
- package/dist/main.js +2229 -1310
- package/dist/native-pi-entry.js +247 -0
- package/dist/runtime/README.md +4 -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-CAyrJ3RP.js +1 -0
- package/dist/web/assets/Files-B9FJqcJ4.js +1 -0
- package/dist/web/assets/Markdown-DPF_hcrA.js +29 -0
- package/dist/web/assets/Sessions-DAIRx5CI.js +1 -0
- package/dist/web/assets/{index-DT_BrKO8.js → index-CETlKRD0.js} +6 -4
- package/dist/web/assets/index-hjEc8Pme.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/Dashboard-DXshk6qj.js +0 -1
- package/dist/web/assets/Files-BiIfupOU.js +0 -1
- package/dist/web/assets/Markdown-DOAYIfza.js +0 -29
- package/dist/web/assets/Sessions-BhnUEPaJ.js +0 -1
- package/dist/web/assets/index-BrRuqgnh.css +0 -1
package/dist/main.js
CHANGED
|
@@ -1,15 +1,92 @@
|
|
|
1
|
-
// apps/server/ai/
|
|
2
|
-
import {
|
|
1
|
+
// apps/server/ai/snapshots.ts
|
|
2
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3
3
|
|
|
4
|
-
// apps/server/
|
|
5
|
-
import { createHash
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// apps/server/files.ts
|
|
5
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// packages/storage/local-blob-store.ts
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
function hash(content) {
|
|
13
|
+
return createHash("sha256").update(content).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function hasCode(error, code) {
|
|
16
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
17
|
+
}
|
|
18
|
+
var BlobIntegrityError = class extends Error {
|
|
19
|
+
constructor(sha256) {
|
|
20
|
+
super(`Stored content failed integrity verification: ${sha256}`);
|
|
21
|
+
this.sha256 = sha256;
|
|
22
|
+
this.name = "BlobIntegrityError";
|
|
23
|
+
}
|
|
24
|
+
sha256;
|
|
25
|
+
};
|
|
26
|
+
var LocalBlobStore = class {
|
|
27
|
+
root;
|
|
28
|
+
constructor(root) {
|
|
29
|
+
this.root = resolve(root);
|
|
30
|
+
}
|
|
31
|
+
async put(content) {
|
|
32
|
+
const bytes = Buffer.from(content);
|
|
33
|
+
const sha256 = hash(bytes);
|
|
34
|
+
const target = this.pathFor(sha256);
|
|
35
|
+
await mkdir(dirname(target), { recursive: true, mode: 448 });
|
|
36
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
37
|
+
const file = await open(temporary, "wx", 384);
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
await file.writeFile(bytes);
|
|
41
|
+
await file.sync();
|
|
42
|
+
} finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await link(temporary, target);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
49
|
+
await this.get(sha256);
|
|
50
|
+
}
|
|
51
|
+
if (process.platform !== "win32") {
|
|
52
|
+
let directory = dirname(target);
|
|
53
|
+
for (; ; ) {
|
|
54
|
+
const handle = await open(directory, "r");
|
|
55
|
+
try {
|
|
56
|
+
await handle.sync();
|
|
57
|
+
} finally {
|
|
58
|
+
await handle.close();
|
|
59
|
+
}
|
|
60
|
+
if (directory === dirname(this.root)) break;
|
|
61
|
+
directory = dirname(directory);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await unlink(temporary);
|
|
66
|
+
}
|
|
67
|
+
return { sha256, size: bytes.byteLength };
|
|
68
|
+
}
|
|
69
|
+
async get(sha256) {
|
|
70
|
+
const target = this.pathFor(sha256);
|
|
71
|
+
let content;
|
|
72
|
+
try {
|
|
73
|
+
content = await readFile(target);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (hasCode(error, "ENOENT")) return void 0;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
if (hash(content) !== sha256) throw new BlobIntegrityError(sha256);
|
|
79
|
+
return content;
|
|
80
|
+
}
|
|
81
|
+
pathFor(sha256) {
|
|
82
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
83
|
+
return join(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
9
86
|
|
|
10
87
|
// packages/state/sqlite.ts
|
|
11
88
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
89
|
+
import { dirname as dirname2 } from "node:path";
|
|
13
90
|
import { DatabaseSync } from "node:sqlite";
|
|
14
91
|
function transaction(db, action) {
|
|
15
92
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -25,7 +102,7 @@ function transaction(db, action) {
|
|
|
25
102
|
function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
26
103
|
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
|
|
27
104
|
throw new Error("Invalid SQLite busy timeout.");
|
|
28
|
-
mkdirSync(
|
|
105
|
+
mkdirSync(dirname2(path), { recursive: true, mode: 448 });
|
|
29
106
|
const db = new DatabaseSync(path);
|
|
30
107
|
try {
|
|
31
108
|
chmodSync(path, 384);
|
|
@@ -33,10 +110,10 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
|
33
110
|
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;`
|
|
34
111
|
);
|
|
35
112
|
transaction(db, () => {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
113
|
+
const version2 = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
114
|
+
if (version2 > migrations2.length)
|
|
38
115
|
throw new Error("Database was created by a newer Teammy version.");
|
|
39
|
-
for (let index =
|
|
116
|
+
for (let index = version2; index < migrations2.length; index++) {
|
|
40
117
|
db.exec(migrations2[index]);
|
|
41
118
|
db.exec(`PRAGMA user_version = ${index + 1}`);
|
|
42
119
|
}
|
|
@@ -48,17 +125,71 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
|
48
125
|
}
|
|
49
126
|
}
|
|
50
127
|
|
|
51
|
-
// packages/protocol/
|
|
128
|
+
// packages/protocol/files.ts
|
|
52
129
|
import { z } from "zod";
|
|
53
|
-
var
|
|
54
|
-
var
|
|
55
|
-
var
|
|
56
|
-
var
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
var MAX_BLOB_BYTES = 10 * 1024 * 1024;
|
|
131
|
+
var MAX_BATCH_FILES = 200;
|
|
132
|
+
var HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
133
|
+
var RevisionSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
134
|
+
var FilePathSchema = z.string().min(1).max(1024).refine((path) => {
|
|
135
|
+
if (path !== path.normalize("NFC") || /[\\\x00-\x1f\x7f:<>"|?*]/.test(path)) return false;
|
|
136
|
+
return path.split("/").every(
|
|
137
|
+
(part) => part.length > 0 && part.length <= 255 && part !== "." && part !== ".." && !/[. ]$/.test(part) && !/^\.teammy(?:-|$)/i.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
|
|
138
|
+
);
|
|
139
|
+
}, "Path must be a portable, normalized relative file path.");
|
|
140
|
+
var BlobRefSchema = z.object({
|
|
141
|
+
sha256: HashSchema,
|
|
142
|
+
size: z.number().int().min(0).max(MAX_BLOB_BYTES)
|
|
143
|
+
});
|
|
144
|
+
var FileChangeSchema = z.discriminatedUnion("kind", [
|
|
145
|
+
BlobRefSchema.extend({ kind: z.literal("upsert"), path: FilePathSchema }).strict(),
|
|
146
|
+
z.object({ kind: z.literal("delete"), path: FilePathSchema }).strict()
|
|
147
|
+
]);
|
|
148
|
+
var FileCommitSchema = z.object({
|
|
149
|
+
operationId: z.uuid(),
|
|
150
|
+
parentRevision: RevisionSchema,
|
|
151
|
+
changes: z.array(FileChangeSchema).min(1).max(MAX_BATCH_FILES)
|
|
152
|
+
}).strict().refine(
|
|
153
|
+
(batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
|
|
154
|
+
"Duplicate paths in a batch."
|
|
155
|
+
);
|
|
156
|
+
var CommitResultSchema = z.object({ operationId: z.uuid(), revision: RevisionSchema });
|
|
157
|
+
var FileEntrySchema = BlobRefSchema.extend({
|
|
158
|
+
path: FilePathSchema,
|
|
159
|
+
revision: RevisionSchema
|
|
160
|
+
});
|
|
161
|
+
var ManifestSchema = z.object({
|
|
162
|
+
revision: RevisionSchema,
|
|
163
|
+
files: z.array(FileEntrySchema),
|
|
164
|
+
nextCursor: z.string().nullable()
|
|
165
|
+
});
|
|
166
|
+
var FileRevisionSchema = z.object({
|
|
167
|
+
revision: RevisionSchema,
|
|
168
|
+
operationId: z.uuid(),
|
|
169
|
+
createdAt: z.string(),
|
|
170
|
+
changes: z.array(FileChangeSchema)
|
|
171
|
+
});
|
|
172
|
+
var ChangesSchema = z.object({
|
|
173
|
+
headRevision: RevisionSchema,
|
|
174
|
+
revisions: z.array(FileRevisionSchema)
|
|
175
|
+
});
|
|
176
|
+
var BlobCheckSchema = z.object({ hashes: z.array(HashSchema).max(MAX_BATCH_FILES) }).strict();
|
|
177
|
+
var MissingBlobsSchema = z.object({ missing: z.array(HashSchema) });
|
|
178
|
+
var FileTreeEntrySchema = z.discriminatedUnion("kind", [
|
|
179
|
+
FileEntrySchema.extend({ kind: z.literal("file") }),
|
|
180
|
+
z.object({
|
|
181
|
+
kind: z.literal("directory"),
|
|
182
|
+
path: FilePathSchema,
|
|
183
|
+
fileCount: z.number().int().positive()
|
|
184
|
+
})
|
|
185
|
+
]);
|
|
186
|
+
var FileTreeSchema = z.object({
|
|
187
|
+
revision: RevisionSchema,
|
|
188
|
+
path: z.union([z.literal(""), FilePathSchema]),
|
|
189
|
+
entries: z.array(FileTreeEntrySchema),
|
|
190
|
+
nextCursor: z.string().nullable()
|
|
60
191
|
});
|
|
61
|
-
var
|
|
192
|
+
var FileSelectionSchema = z.object({ revision: RevisionSchema, file: FileEntrySchema });
|
|
62
193
|
|
|
63
194
|
// apps/server/errors.ts
|
|
64
195
|
var HubError = class extends Error {
|
|
@@ -72,454 +203,131 @@ var HubError = class extends Error {
|
|
|
72
203
|
code;
|
|
73
204
|
};
|
|
74
205
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
CREATE TABLE sessions (
|
|
78
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
80
|
-
agent TEXT NOT NULL, native_id TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL,
|
|
81
|
-
revision INTEGER NOT NULL, updated_at TEXT NOT NULL, message_count INTEGER NOT NULL,
|
|
82
|
-
hash TEXT NOT NULL, size INTEGER NOT NULL,
|
|
83
|
-
UNIQUE(workspace_id, owner, session_key)
|
|
84
|
-
);
|
|
85
|
-
CREATE INDEX session_listing ON sessions(workspace_id, id);
|
|
86
|
-
CREATE TABLE session_revisions (
|
|
87
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
88
|
-
revision INTEGER NOT NULL, info_json TEXT NOT NULL,
|
|
89
|
-
PRIMARY KEY(workspace_id, owner, session_key, revision)
|
|
90
|
-
);
|
|
91
|
-
CREATE TABLE session_operations (
|
|
92
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
93
|
-
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
94
|
-
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
95
|
-
);
|
|
96
|
-
CREATE TABLE session_messages (
|
|
97
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
99
|
-
revision INTEGER NOT NULL, message_id TEXT NOT NULL, role TEXT NOT NULL, body TEXT NOT NULL, search_text TEXT NOT NULL
|
|
100
|
-
);
|
|
101
|
-
CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
|
|
102
|
-
CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
|
|
103
|
-
`;
|
|
104
|
-
var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
|
|
105
|
-
|
|
106
|
-
// apps/server/control-schema.ts
|
|
107
|
-
var controlMigration = String.raw`
|
|
108
|
-
CREATE TABLE device_status(workspace_id TEXT NOT NULL,member_id TEXT NOT NULL,device_id TEXT NOT NULL,seen_at TEXT NOT NULL,status_json TEXT NOT NULL,PRIMARY KEY(workspace_id,member_id));
|
|
109
|
-
`;
|
|
206
|
+
// packages/protocol/shared-files.ts
|
|
207
|
+
import { z as z3 } from "zod";
|
|
110
208
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
209
|
+
// packages/protocol/identity.ts
|
|
210
|
+
import { z as z2 } from "zod";
|
|
211
|
+
var NameSchema = z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
|
|
212
|
+
var DeviceIdSchema = z2.uuid();
|
|
213
|
+
var BindRequestSchema = z2.object({ deviceId: DeviceIdSchema, projectId: NameSchema.optional() }).strict();
|
|
214
|
+
var BindingSchema = z2.object({
|
|
215
|
+
workspaceId: NameSchema,
|
|
216
|
+
memberId: NameSchema,
|
|
217
|
+
deviceId: DeviceIdSchema
|
|
218
|
+
});
|
|
219
|
+
var MembersSchema = z2.object({ members: z2.array(NameSchema) });
|
|
115
220
|
|
|
116
|
-
//
|
|
117
|
-
var
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
|
|
141
|
-
setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
|
|
142
|
-
);
|
|
143
|
-
CREATE TABLE web_sessions (
|
|
144
|
-
token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
|
|
145
|
-
);
|
|
146
|
-
DROP TABLE ai_profiles;
|
|
147
|
-
CREATE TABLE ai_profiles (
|
|
148
|
-
id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
|
|
221
|
+
// packages/protocol/shared-files.ts
|
|
222
|
+
var PROJECT_FILE_OWNER = "_project";
|
|
223
|
+
var FileOwnerSchema = z3.union([NameSchema, z3.literal(PROJECT_FILE_OWNER)]);
|
|
224
|
+
var SharedPathSchema = z3.string().max(255).refine((path) => {
|
|
225
|
+
const directory = path.endsWith("/");
|
|
226
|
+
const name = directory ? path.slice(0, -1) : path;
|
|
227
|
+
return FilePathSchema.safeParse(name).success && !name.includes("/") && !name.startsWith(".") && !/^_?teammy/i.test(name) && (directory ? name.startsWith("_") : name.includes("."));
|
|
228
|
+
}, "Use a root file (README.md) or an underscore-prefixed directory (_template/).");
|
|
229
|
+
var SharedFilesConfigSchema = z3.object({
|
|
230
|
+
maintainer: NameSchema.nullable(),
|
|
231
|
+
paths: z3.array(SharedPathSchema).max(30).refine((paths2) => new Set(paths2.map((p) => p.toLowerCase())).size === paths2.length),
|
|
232
|
+
revision: RevisionSchema
|
|
233
|
+
}).strict();
|
|
234
|
+
var SharedFilesUpdateSchema = SharedFilesConfigSchema.omit({ revision: true }).extend({ expectedRevision: RevisionSchema }).strict();
|
|
235
|
+
var SharedFileCommitSchema = FileCommitSchema.safeExtend({
|
|
236
|
+
configRevision: RevisionSchema
|
|
237
|
+
});
|
|
238
|
+
var DEFAULT_SHARED_PATHS = ["README.md", "_template/"];
|
|
239
|
+
function privateWorkspacePath(path) {
|
|
240
|
+
return path.split("/").some((part) => /^\.teammy(?:[-.]|$)/i.test(part));
|
|
241
|
+
}
|
|
242
|
+
function sharedPathAllowed(path, paths2) {
|
|
243
|
+
return !privateWorkspacePath(path) && paths2.some(
|
|
244
|
+
(entry) => entry.endsWith("/") ? path === entry.slice(0, -1) || path.startsWith(entry) : path === entry
|
|
149
245
|
);
|
|
150
|
-
|
|
151
|
-
var sitePolicyMigration = `
|
|
152
|
-
UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
|
|
153
|
-
(SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
|
|
154
|
-
'{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
|
|
155
|
-
)));
|
|
156
|
-
UPDATE projects SET config_json=json_set(config_json,
|
|
157
|
-
'$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
|
|
158
|
-
'$.configRevision',json_extract(config_json,'$.configRevision')+1)
|
|
159
|
-
WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
|
|
160
|
-
`;
|
|
161
|
-
var equalMembershipMigration = `
|
|
162
|
-
ALTER TABLE members DROP COLUMN role;
|
|
163
|
-
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
164
|
-
`;
|
|
246
|
+
}
|
|
165
247
|
|
|
166
|
-
// apps/server/
|
|
167
|
-
|
|
168
|
-
ALTER TABLE members ADD COLUMN file_revision INTEGER NOT NULL DEFAULT 0;
|
|
169
|
-
CREATE TABLE workspace_blobs (workspace_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(workspace_id, hash));
|
|
170
|
-
CREATE TABLE file_operations (
|
|
171
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
172
|
-
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
173
|
-
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
174
|
-
);
|
|
175
|
-
CREATE TABLE file_revisions (
|
|
176
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
177
|
-
operation_id TEXT NOT NULL, created_at TEXT NOT NULL, changes_json TEXT NOT NULL,
|
|
178
|
-
PRIMARY KEY(workspace_id, owner, revision)
|
|
179
|
-
);
|
|
180
|
-
CREATE TABLE current_files (
|
|
181
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, path_key TEXT NOT NULL,
|
|
182
|
-
hash TEXT NOT NULL, size INTEGER NOT NULL, revision INTEGER NOT NULL,
|
|
183
|
-
PRIMARY KEY(workspace_id, owner, path), UNIQUE(workspace_id, owner, path_key)
|
|
184
|
-
);
|
|
185
|
-
CREATE TABLE file_events (
|
|
186
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
187
|
-
hash TEXT, size INTEGER,
|
|
188
|
-
PRIMARY KEY(workspace_id, owner, path, revision)
|
|
189
|
-
);
|
|
190
|
-
`;
|
|
191
|
-
|
|
192
|
-
// apps/server/ai/task-schema.ts
|
|
193
|
-
var taskBoardMigration = `
|
|
194
|
-
ALTER TABLE ai_jobs ADD COLUMN runtime_json TEXT;
|
|
195
|
-
ALTER TABLE ai_jobs ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
|
196
|
-
CREATE TABLE task_board_members (
|
|
197
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, job_id TEXT NOT NULL REFERENCES ai_jobs(id),
|
|
198
|
-
PRIMARY KEY(workspace_id,owner)
|
|
199
|
-
);
|
|
200
|
-
CREATE TABLE task_cards (
|
|
201
|
-
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, owner TEXT NOT NULL,
|
|
202
|
-
job_id TEXT NOT NULL REFERENCES ai_jobs(id), fields_json TEXT NOT NULL,
|
|
203
|
-
sources_json TEXT NOT NULL, synced_at TEXT NOT NULL, correction_json TEXT,
|
|
204
|
-
active INTEGER NOT NULL DEFAULT 1, needs_review INTEGER NOT NULL DEFAULT 0
|
|
205
|
-
);
|
|
206
|
-
CREATE INDEX task_cards_scope ON task_cards(workspace_id,owner,active);
|
|
207
|
-
`;
|
|
208
|
-
var taskMaintenanceMigration = `
|
|
209
|
-
ALTER TABLE ai_jobs ADD COLUMN task_context_json TEXT;
|
|
210
|
-
ALTER TABLE task_board_members ADD COLUMN applied_job_id TEXT REFERENCES ai_jobs(id);
|
|
211
|
-
UPDATE task_board_members SET applied_job_id=(
|
|
212
|
-
SELECT j.id FROM ai_jobs j
|
|
213
|
-
WHERE j.workspace_id=task_board_members.workspace_id AND j.status='succeeded'
|
|
214
|
-
AND json_extract(j.request_json,'$.workflow')='task-bootstrap'
|
|
215
|
-
AND json_extract(j.snapshot_json,'$.owner')=task_board_members.owner
|
|
216
|
-
ORDER BY j.rowid DESC LIMIT 1
|
|
217
|
-
);
|
|
218
|
-
`;
|
|
219
|
-
|
|
220
|
-
// apps/server/ai/schema.ts
|
|
221
|
-
var aiConfigMigration = `
|
|
222
|
-
CREATE TABLE ai_profiles (
|
|
223
|
-
workspace_id TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
224
|
-
config_json TEXT NOT NULL, secret TEXT,
|
|
225
|
-
PRIMARY KEY(workspace_id,id)
|
|
226
|
-
);
|
|
227
|
-
`;
|
|
228
|
-
var aiJobsMigration = `
|
|
229
|
-
CREATE TABLE ai_jobs (
|
|
230
|
-
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, requester TEXT NOT NULL,
|
|
231
|
-
request_json TEXT NOT NULL, profile_json TEXT NOT NULL, snapshot_json TEXT NOT NULL,
|
|
232
|
-
prompt_version TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, recovery_count INTEGER NOT NULL DEFAULT 0,
|
|
233
|
-
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, run_token TEXT, lease_until INTEGER,
|
|
234
|
-
result_json TEXT, error TEXT
|
|
235
|
-
);
|
|
236
|
-
CREATE INDEX ai_jobs_queue ON ai_jobs(status,created_at);
|
|
237
|
-
CREATE INDEX ai_jobs_workspace ON ai_jobs(workspace_id,id);
|
|
238
|
-
CREATE TABLE ai_operations(workspace_id TEXT NOT NULL,requester TEXT NOT NULL,operation_id TEXT NOT NULL,fingerprint TEXT NOT NULL,job_id TEXT NOT NULL REFERENCES ai_jobs(id),PRIMARY KEY(workspace_id,requester,operation_id));
|
|
239
|
-
CREATE TABLE ai_attempts(job_id TEXT NOT NULL REFERENCES ai_jobs(id),number INTEGER NOT NULL,status TEXT NOT NULL,started_at TEXT NOT NULL,ended_at TEXT,error TEXT,usage_json TEXT,PRIMARY KEY(job_id,number));
|
|
240
|
-
CREATE TABLE ai_events(seq INTEGER PRIMARY KEY AUTOINCREMENT,job_id TEXT NOT NULL REFERENCES ai_jobs(id),attempt INTEGER NOT NULL,kind TEXT NOT NULL,message TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
241
|
-
CREATE INDEX ai_events_job ON ai_events(job_id,seq);
|
|
242
|
-
`;
|
|
243
|
-
var aiDefaultsMigration = `
|
|
244
|
-
CREATE TABLE ai_defaults (
|
|
245
|
-
scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
|
|
246
|
-
);
|
|
247
|
-
`;
|
|
248
|
-
|
|
249
|
-
// apps/server/store.ts
|
|
250
|
-
function digest(token) {
|
|
251
|
-
return createHash("sha256").update(token).digest("hex");
|
|
252
|
-
}
|
|
253
|
-
var migrations = [
|
|
254
|
-
String.raw`
|
|
255
|
-
CREATE TABLE members (
|
|
256
|
-
workspace_id TEXT NOT NULL,
|
|
257
|
-
member_id TEXT NOT NULL,
|
|
258
|
-
token_hash TEXT UNIQUE,
|
|
259
|
-
device_id TEXT,
|
|
260
|
-
PRIMARY KEY (workspace_id, member_id)
|
|
261
|
-
);
|
|
262
|
-
`,
|
|
263
|
-
fileMigration,
|
|
264
|
-
sessionMigration,
|
|
265
|
-
projectMigration,
|
|
266
|
-
deviceMigration,
|
|
267
|
-
controlMigration,
|
|
268
|
-
aiConfigMigration,
|
|
269
|
-
aiJobsMigration,
|
|
270
|
-
multiProjectMigration,
|
|
271
|
-
aiDefaultsMigration,
|
|
272
|
-
sitePolicyMigration,
|
|
273
|
-
sessionOwnerIndexMigration,
|
|
274
|
-
equalMembershipMigration,
|
|
275
|
-
taskBoardMigration,
|
|
276
|
-
taskMaintenanceMigration
|
|
277
|
-
];
|
|
278
|
-
var HubStore = class {
|
|
279
|
-
constructor(directory) {
|
|
280
|
-
this.directory = directory;
|
|
281
|
-
const path = join(directory, "hub.sqlite3");
|
|
282
|
-
if (existsSync(path)) {
|
|
283
|
-
const existing = new DatabaseSync2(path, { readOnly: true });
|
|
284
|
-
try {
|
|
285
|
-
const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
|
|
286
|
-
if (version > 0 && version < 9)
|
|
287
|
-
throw new Error(
|
|
288
|
-
"Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
|
|
289
|
-
);
|
|
290
|
-
} finally {
|
|
291
|
-
existing.close();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
this.db = openDatabase(path, migrations);
|
|
295
|
-
}
|
|
296
|
-
directory;
|
|
297
|
-
db;
|
|
298
|
-
close() {
|
|
299
|
-
this.db.close();
|
|
300
|
-
}
|
|
301
|
-
issueMember(workspaceId, memberId, reset = false) {
|
|
302
|
-
NameSchema.parse(workspaceId);
|
|
303
|
-
NameSchema.parse(memberId);
|
|
304
|
-
const token = `tmy_${randomBytes(32).toString("hex")}`;
|
|
305
|
-
transaction(this.db, () => {
|
|
306
|
-
this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
|
|
307
|
-
const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
|
|
308
|
-
if (exists && !reset)
|
|
309
|
-
throw new HubError(
|
|
310
|
-
409,
|
|
311
|
-
"MEMBER_EXISTS",
|
|
312
|
-
"Member exists; use member reset to replace its credential and device binding."
|
|
313
|
-
);
|
|
314
|
-
if (!exists && reset) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
315
|
-
this.db.prepare(
|
|
316
|
-
`INSERT INTO members (workspace_id, member_id, token_hash) VALUES (?, ?, ?)
|
|
317
|
-
ON CONFLICT(workspace_id, member_id) DO UPDATE SET token_hash=excluded.token_hash, device_id=NULL, enabled=1, member_revision=member_revision+1`
|
|
318
|
-
).run(workspaceId, memberId, digest(token));
|
|
319
|
-
});
|
|
320
|
-
return token;
|
|
321
|
-
}
|
|
322
|
-
revokeMember(workspaceId, memberId) {
|
|
323
|
-
const result = this.db.prepare(
|
|
324
|
-
"UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
|
|
325
|
-
).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
|
|
326
|
-
if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
327
|
-
this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
|
|
328
|
-
}
|
|
329
|
-
identity(authorization) {
|
|
330
|
-
const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
|
|
331
|
-
const row = match && this.db.prepare(
|
|
332
|
-
"SELECT m.workspace_id,m.member_id,m.device_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.token_hash=? AND m.enabled=1 AND u.enabled=1"
|
|
333
|
-
).get(digest(match[1]));
|
|
334
|
-
if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
|
|
335
|
-
return {
|
|
336
|
-
workspaceId: String(row.workspace_id),
|
|
337
|
-
memberId: String(row.member_id),
|
|
338
|
-
deviceId: row.device_id
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
bind(authorization, deviceId, projectId2) {
|
|
342
|
-
return transaction(this.db, () => {
|
|
343
|
-
const identity = this.identity(authorization);
|
|
344
|
-
if (projectId2 && projectId2 !== identity.workspaceId)
|
|
345
|
-
throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
|
|
346
|
-
if (identity.deviceId && identity.deviceId !== deviceId)
|
|
347
|
-
throw new HubError(
|
|
348
|
-
409,
|
|
349
|
-
"DEVICE_BOUND",
|
|
350
|
-
"This member is bound to another device. An administrator must reset the binding."
|
|
351
|
-
);
|
|
352
|
-
this.db.prepare("UPDATE members SET device_id=? WHERE workspace_id=? AND member_id=?").run(deviceId, identity.workspaceId, identity.memberId);
|
|
353
|
-
return BindingSchema.parse({ ...identity, deviceId });
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
authorize(authorization, deviceId) {
|
|
357
|
-
const identity = this.identity(authorization);
|
|
358
|
-
if (!identity.deviceId || identity.deviceId !== deviceId)
|
|
359
|
-
throw new HubError(
|
|
360
|
-
403,
|
|
361
|
-
"DEVICE_MISMATCH",
|
|
362
|
-
"Connect this device before using workspace APIs."
|
|
363
|
-
);
|
|
364
|
-
return BindingSchema.parse(identity);
|
|
365
|
-
}
|
|
366
|
-
members(identity) {
|
|
367
|
-
return this.db.prepare("SELECT member_id FROM members WHERE workspace_id=? ORDER BY member_id").all(identity.workspaceId).map((row) => String(row.member_id));
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
// apps/server/ai/jobs.ts
|
|
372
|
-
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
373
|
-
|
|
374
|
-
// packages/protocol/ai.ts
|
|
375
|
-
import { z as z5 } from "zod";
|
|
376
|
-
|
|
377
|
-
// packages/protocol/files.ts
|
|
378
|
-
import { z as z2 } from "zod";
|
|
379
|
-
var MAX_BLOB_BYTES = 10 * 1024 * 1024;
|
|
380
|
-
var MAX_BATCH_FILES = 200;
|
|
381
|
-
var HashSchema = z2.string().regex(/^[a-f0-9]{64}$/);
|
|
382
|
-
var RevisionSchema = z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
383
|
-
var FilePathSchema = z2.string().min(1).max(1024).refine((path) => {
|
|
384
|
-
if (path !== path.normalize("NFC") || /[\\\x00-\x1f\x7f:<>"|?*]/.test(path)) return false;
|
|
385
|
-
return path.split("/").every(
|
|
386
|
-
(part) => part.length > 0 && part.length <= 255 && part !== "." && part !== ".." && !/[. ]$/.test(part) && !/^\.teammy(?:-|$)/i.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
|
|
387
|
-
);
|
|
388
|
-
}, "Path must be a portable, normalized relative file path.");
|
|
389
|
-
var BlobRefSchema = z2.object({
|
|
390
|
-
sha256: HashSchema,
|
|
391
|
-
size: z2.number().int().min(0).max(MAX_BLOB_BYTES)
|
|
392
|
-
});
|
|
393
|
-
var FileChangeSchema = z2.discriminatedUnion("kind", [
|
|
394
|
-
BlobRefSchema.extend({ kind: z2.literal("upsert"), path: FilePathSchema }).strict(),
|
|
395
|
-
z2.object({ kind: z2.literal("delete"), path: FilePathSchema }).strict()
|
|
396
|
-
]);
|
|
397
|
-
var FileCommitSchema = z2.object({
|
|
398
|
-
operationId: z2.uuid(),
|
|
399
|
-
parentRevision: RevisionSchema,
|
|
400
|
-
changes: z2.array(FileChangeSchema).min(1).max(MAX_BATCH_FILES)
|
|
401
|
-
}).strict().refine(
|
|
402
|
-
(batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
|
|
403
|
-
"Duplicate paths in a batch."
|
|
404
|
-
);
|
|
405
|
-
var CommitResultSchema = z2.object({ operationId: z2.uuid(), revision: RevisionSchema });
|
|
406
|
-
var FileEntrySchema = BlobRefSchema.extend({
|
|
407
|
-
path: FilePathSchema,
|
|
408
|
-
revision: RevisionSchema
|
|
409
|
-
});
|
|
410
|
-
var ManifestSchema = z2.object({
|
|
411
|
-
revision: RevisionSchema,
|
|
412
|
-
files: z2.array(FileEntrySchema),
|
|
413
|
-
nextCursor: z2.string().nullable()
|
|
414
|
-
});
|
|
415
|
-
var FileRevisionSchema = z2.object({
|
|
416
|
-
revision: RevisionSchema,
|
|
417
|
-
operationId: z2.uuid(),
|
|
418
|
-
createdAt: z2.string(),
|
|
419
|
-
changes: z2.array(FileChangeSchema)
|
|
420
|
-
});
|
|
421
|
-
var ChangesSchema = z2.object({
|
|
422
|
-
headRevision: RevisionSchema,
|
|
423
|
-
revisions: z2.array(FileRevisionSchema)
|
|
424
|
-
});
|
|
425
|
-
var BlobCheckSchema = z2.object({ hashes: z2.array(HashSchema).max(MAX_BATCH_FILES) }).strict();
|
|
426
|
-
var MissingBlobsSchema = z2.object({ missing: z2.array(HashSchema) });
|
|
427
|
-
var FileTreeEntrySchema = z2.discriminatedUnion("kind", [
|
|
428
|
-
FileEntrySchema.extend({ kind: z2.literal("file") }),
|
|
429
|
-
z2.object({
|
|
430
|
-
kind: z2.literal("directory"),
|
|
431
|
-
path: FilePathSchema,
|
|
432
|
-
fileCount: z2.number().int().positive()
|
|
433
|
-
})
|
|
434
|
-
]);
|
|
435
|
-
var FileTreeSchema = z2.object({
|
|
436
|
-
revision: RevisionSchema,
|
|
437
|
-
path: z2.union([z2.literal(""), FilePathSchema]),
|
|
438
|
-
entries: z2.array(FileTreeEntrySchema),
|
|
439
|
-
nextCursor: z2.string().nullable()
|
|
440
|
-
});
|
|
441
|
-
var FileSelectionSchema = z2.object({ revision: RevisionSchema, file: FileEntrySchema });
|
|
248
|
+
// apps/server/project.ts
|
|
249
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
442
250
|
|
|
443
251
|
// packages/protocol/project.ts
|
|
444
|
-
import { z as
|
|
445
|
-
var ServerUrlSchema =
|
|
252
|
+
import { z as z4 } from "zod";
|
|
253
|
+
var ServerUrlSchema = z4.string().url().max(2048).refine((value) => {
|
|
446
254
|
const url = new URL(value);
|
|
447
255
|
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
|
|
448
256
|
}, "Use an HTTP(S) server URL without credentials, query or fragment.");
|
|
449
|
-
var SyncPolicySchema =
|
|
450
|
-
checkIntervalSeconds:
|
|
451
|
-
fileSettleSeconds:
|
|
257
|
+
var SyncPolicySchema = z4.object({
|
|
258
|
+
checkIntervalSeconds: z4.number().int().min(1).max(86400),
|
|
259
|
+
fileSettleSeconds: z4.number().int().min(0).max(86400)
|
|
452
260
|
}).strict();
|
|
453
261
|
var DEFAULT_POLICY = { checkIntervalSeconds: 60, fileSettleSeconds: 60 };
|
|
454
|
-
var ProjectConfigSchema =
|
|
455
|
-
schemaVersion:
|
|
262
|
+
var ProjectConfigSchema = z4.object({
|
|
263
|
+
schemaVersion: z4.literal(1),
|
|
456
264
|
projectId: NameSchema,
|
|
457
|
-
projectName:
|
|
458
|
-
description:
|
|
265
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
266
|
+
description: z4.string().max(2e3),
|
|
459
267
|
publicServerUrl: ServerUrlSchema,
|
|
460
268
|
configRevision: RevisionSchema,
|
|
461
|
-
scopeProfile:
|
|
269
|
+
scopeProfile: z4.literal("workspace-and-sessions-v1"),
|
|
462
270
|
sync: SyncPolicySchema
|
|
463
271
|
}).strict();
|
|
464
|
-
var SetupSchema =
|
|
465
|
-
operationId:
|
|
466
|
-
projectName:
|
|
467
|
-
description:
|
|
272
|
+
var SetupSchema = z4.object({
|
|
273
|
+
operationId: z4.uuid(),
|
|
274
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
275
|
+
description: z4.string().max(2e3).default(""),
|
|
468
276
|
publicServerUrl: ServerUrlSchema,
|
|
469
277
|
adminId: NameSchema,
|
|
470
|
-
members:
|
|
278
|
+
members: z4.array(NameSchema).max(100).default([]),
|
|
471
279
|
sync: SyncPolicySchema.default(DEFAULT_POLICY)
|
|
472
280
|
}).strict();
|
|
473
|
-
var SetupStatusSchema =
|
|
474
|
-
initialized:
|
|
281
|
+
var SetupStatusSchema = z4.object({
|
|
282
|
+
initialized: z4.boolean()
|
|
475
283
|
});
|
|
476
284
|
var ConfigUpdateSchema = ProjectConfigSchema.pick({
|
|
477
285
|
projectName: true,
|
|
478
286
|
description: true
|
|
479
287
|
}).extend({ expectedRevision: RevisionSchema.optional() }).strict();
|
|
480
|
-
var MemberSchema =
|
|
288
|
+
var MemberSchema = z4.object({
|
|
481
289
|
id: NameSchema,
|
|
482
|
-
displayName:
|
|
483
|
-
enabled:
|
|
484
|
-
accountEnabled:
|
|
290
|
+
displayName: z4.string().max(120),
|
|
291
|
+
enabled: z4.boolean(),
|
|
292
|
+
accountEnabled: z4.boolean(),
|
|
485
293
|
revision: RevisionSchema,
|
|
486
|
-
bound:
|
|
294
|
+
bound: z4.boolean(),
|
|
487
295
|
fileRevision: RevisionSchema,
|
|
488
|
-
lastPublishedAt:
|
|
296
|
+
lastPublishedAt: z4.string().nullable()
|
|
489
297
|
});
|
|
490
|
-
var WebIdentitySchema =
|
|
298
|
+
var WebIdentitySchema = z4.object({
|
|
491
299
|
memberId: NameSchema,
|
|
492
|
-
displayName:
|
|
493
|
-
siteRole:
|
|
300
|
+
displayName: z4.string(),
|
|
301
|
+
siteRole: z4.enum(["admin", "member"])
|
|
494
302
|
});
|
|
495
303
|
var WebSessionSchema = WebIdentitySchema.extend({
|
|
496
304
|
workspaceId: NameSchema
|
|
497
305
|
});
|
|
498
|
-
var MemberCreateSchema =
|
|
306
|
+
var MemberCreateSchema = z4.object({
|
|
499
307
|
id: NameSchema
|
|
500
308
|
}).strict();
|
|
501
|
-
var MemberUpdateSchema =
|
|
309
|
+
var MemberUpdateSchema = z4.object({
|
|
502
310
|
expectedRevision: RevisionSchema,
|
|
503
|
-
displayName:
|
|
504
|
-
enabled:
|
|
311
|
+
displayName: z4.string().trim().max(120).optional(),
|
|
312
|
+
enabled: z4.boolean().optional()
|
|
505
313
|
}).strict();
|
|
506
|
-
var ProjectCreateSchema =
|
|
507
|
-
operationId:
|
|
508
|
-
projectName:
|
|
509
|
-
description:
|
|
314
|
+
var ProjectCreateSchema = z4.object({
|
|
315
|
+
operationId: z4.uuid(),
|
|
316
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
317
|
+
description: z4.string().max(2e3).default("")
|
|
510
318
|
}).strict();
|
|
511
|
-
var ProjectSummarySchema =
|
|
319
|
+
var ProjectSummarySchema = z4.object({
|
|
512
320
|
projectId: NameSchema,
|
|
513
|
-
projectName:
|
|
514
|
-
description:
|
|
321
|
+
projectName: z4.string(),
|
|
322
|
+
description: z4.string()
|
|
515
323
|
});
|
|
516
|
-
var ProjectsSchema =
|
|
517
|
-
var SiteConfigSchema =
|
|
324
|
+
var ProjectsSchema = z4.object({ projects: z4.array(ProjectSummarySchema) });
|
|
325
|
+
var SiteConfigSchema = z4.object({
|
|
518
326
|
sync: SyncPolicySchema,
|
|
519
327
|
publicServerUrl: ServerUrlSchema,
|
|
520
328
|
revision: RevisionSchema
|
|
521
329
|
});
|
|
522
|
-
var SiteConfigUpdateSchema =
|
|
330
|
+
var SiteConfigUpdateSchema = z4.object({
|
|
523
331
|
publicServerUrl: ServerUrlSchema.optional(),
|
|
524
332
|
sync: SyncPolicySchema.optional(),
|
|
525
333
|
expectedRevision: RevisionSchema
|
|
@@ -527,222 +335,28 @@ var SiteConfigUpdateSchema = z3.object({
|
|
|
527
335
|
(v) => v.publicServerUrl !== void 0 || v.sync !== void 0,
|
|
528
336
|
"Provide a setting to update."
|
|
529
337
|
);
|
|
530
|
-
var UserSchema =
|
|
338
|
+
var UserSchema = z4.object({
|
|
531
339
|
id: NameSchema,
|
|
532
|
-
displayName:
|
|
533
|
-
siteRole:
|
|
534
|
-
enabled:
|
|
340
|
+
displayName: z4.string(),
|
|
341
|
+
siteRole: z4.enum(["admin", "member"]),
|
|
342
|
+
enabled: z4.boolean(),
|
|
535
343
|
revision: RevisionSchema
|
|
536
344
|
});
|
|
537
|
-
var UserCreateSchema =
|
|
345
|
+
var UserCreateSchema = z4.object({
|
|
538
346
|
id: NameSchema,
|
|
539
|
-
displayName:
|
|
540
|
-
siteRole:
|
|
347
|
+
displayName: z4.string().trim().max(120).default(""),
|
|
348
|
+
siteRole: z4.enum(["admin", "member"]).default("member")
|
|
541
349
|
}).strict();
|
|
542
|
-
var UserUpdateSchema =
|
|
350
|
+
var UserUpdateSchema = z4.object({
|
|
543
351
|
expectedRevision: RevisionSchema,
|
|
544
|
-
displayName:
|
|
545
|
-
enabled:
|
|
546
|
-
siteRole:
|
|
547
|
-
}).strict();
|
|
548
|
-
|
|
549
|
-
// packages/protocol/tasks.ts
|
|
550
|
-
import { z as z4 } from "zod";
|
|
551
|
-
var TaskStatusSchema = z4.enum(["planned", "in_progress", "in_review", "completed"]);
|
|
552
|
-
var TaskFieldsSchema = z4.object({
|
|
553
|
-
title: z4.string().trim().min(1).max(200),
|
|
554
|
-
goal: z4.string().trim().min(1).max(2e3).nullable(),
|
|
555
|
-
status: TaskStatusSchema.nullable(),
|
|
556
|
-
progress: z4.string().trim().min(1).max(2e3)
|
|
352
|
+
displayName: z4.string().trim().max(120).optional(),
|
|
353
|
+
enabled: z4.boolean().optional(),
|
|
354
|
+
siteRole: z4.enum(["admin", "member"]).optional()
|
|
557
355
|
}).strict();
|
|
558
|
-
var TaskSourceSchema = z4.object({
|
|
559
|
-
path: FilePathSchema,
|
|
560
|
-
sha256: HashSchema,
|
|
561
|
-
revision: RevisionSchema,
|
|
562
|
-
syncedAt: z4.string()
|
|
563
|
-
});
|
|
564
|
-
var TaskCardSchema = TaskFieldsSchema.extend({
|
|
565
|
-
id: z4.uuid(),
|
|
566
|
-
owner: NameSchema,
|
|
567
|
-
jobId: z4.uuid(),
|
|
568
|
-
sources: z4.array(TaskSourceSchema).min(1),
|
|
569
|
-
syncedAt: z4.string(),
|
|
570
|
-
corrected: z4.boolean(),
|
|
571
|
-
needsReview: z4.boolean()
|
|
572
|
-
});
|
|
573
|
-
var TaskCorrectionSchema = z4.object({
|
|
574
|
-
title: TaskFieldsSchema.shape.title.optional(),
|
|
575
|
-
status: TaskStatusSchema.nullable().optional()
|
|
576
|
-
}).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
|
|
577
|
-
var TaskBoardSchema = z4.object({
|
|
578
|
-
tasks: z4.array(TaskCardSchema),
|
|
579
|
-
members: z4.array(
|
|
580
|
-
z4.object({
|
|
581
|
-
owner: NameSchema,
|
|
582
|
-
jobId: z4.uuid().nullable(),
|
|
583
|
-
status: z4.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
|
|
584
|
-
error: z4.string().nullable(),
|
|
585
|
-
sourceChanged: z4.boolean(),
|
|
586
|
-
partial: z4.boolean(),
|
|
587
|
-
uncertainties: z4.array(z4.string())
|
|
588
|
-
})
|
|
589
|
-
),
|
|
590
|
-
modelAvailable: z4.boolean()
|
|
591
|
-
});
|
|
592
|
-
var TaskBootstrapRequestSchema = z4.object({ operationId: z4.uuid(), owner: NameSchema }).strict();
|
|
593
|
-
var TaskBootstrapResponseSchema = z4.object({
|
|
594
|
-
jobId: z4.uuid().nullable(),
|
|
595
|
-
empty: z4.boolean()
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// packages/protocol/ai.ts
|
|
599
|
-
var AiProfileSchema = z5.object({
|
|
600
|
-
id: NameSchema,
|
|
601
|
-
name: z5.string().trim().min(1).max(120),
|
|
602
|
-
protocol: z5.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
|
|
603
|
-
baseUrl: ServerUrlSchema,
|
|
604
|
-
model: z5.string().trim().min(1).max(200),
|
|
605
|
-
enabled: z5.boolean().default(true),
|
|
606
|
-
contextWindow: z5.number().int().min(4096).max(2e6).default(32e3),
|
|
607
|
-
maxOutputTokens: z5.number().int().min(128).max(16e3).default(2048)
|
|
608
|
-
}).strict().refine(
|
|
609
|
-
(p) => p.maxOutputTokens < p.contextWindow,
|
|
610
|
-
"Output budget must be smaller than context window."
|
|
611
|
-
);
|
|
612
|
-
var AiPublicProfileSchema = AiProfileSchema.safeExtend({
|
|
613
|
-
revision: z5.number().int(),
|
|
614
|
-
keyConfigured: z5.boolean()
|
|
615
|
-
});
|
|
616
|
-
var AiProfileUpdateSchema = z5.object({
|
|
617
|
-
expectedRevision: z5.number().int().nonnegative(),
|
|
618
|
-
profile: AiProfileSchema,
|
|
619
|
-
apiKey: z5.string().min(1).max(4096).nullable().optional()
|
|
620
|
-
}).strict();
|
|
621
|
-
var AiProfilesSchema = z5.object({ profiles: z5.array(AiPublicProfileSchema) });
|
|
622
|
-
var AiDefaultUpdateSchema = z5.object({
|
|
623
|
-
expectedRevision: z5.number().int().nonnegative(),
|
|
624
|
-
profileId: NameSchema.nullable()
|
|
625
|
-
}).strict();
|
|
626
|
-
var AiDefaultSchema = z5.object({
|
|
627
|
-
revision: z5.number().int().nonnegative(),
|
|
628
|
-
profileId: NameSchema.nullable(),
|
|
629
|
-
effectiveProfileId: NameSchema.nullable(),
|
|
630
|
-
source: z5.enum(["site", "project", "none"]),
|
|
631
|
-
available: z5.boolean()
|
|
632
|
-
});
|
|
633
|
-
var AiProbeSchema = z5.object({ mode: z5.enum(["text", "tools", "json"]) }).strict();
|
|
634
|
-
var AiProbeResultSchema = z5.object({
|
|
635
|
-
ok: z5.literal(true),
|
|
636
|
-
mode: AiProbeSchema.shape.mode,
|
|
637
|
-
streamed: z5.boolean(),
|
|
638
|
-
inputTokens: z5.number(),
|
|
639
|
-
outputTokens: z5.number()
|
|
640
|
-
});
|
|
641
|
-
var AiSelectionSchema = z5.object({
|
|
642
|
-
owner: NameSchema,
|
|
643
|
-
kind: z5.enum(["file", "directory"]),
|
|
644
|
-
path: z5.union([z5.literal(""), FilePathSchema]),
|
|
645
|
-
revision: z5.number().int().nonnegative().optional()
|
|
646
|
-
}).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
|
|
647
|
-
var AiJobCreateSchema = z5.object({
|
|
648
|
-
operationId: z5.uuid(),
|
|
649
|
-
profileId: NameSchema.optional(),
|
|
650
|
-
workflow: z5.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
|
|
651
|
-
selection: AiSelectionSchema,
|
|
652
|
-
question: z5.string().trim().max(2e3).default("")
|
|
653
|
-
}).strict();
|
|
654
|
-
var AiCitationSchema = z5.object({
|
|
655
|
-
path: FilePathSchema,
|
|
656
|
-
startLine: z5.number().int().positive(),
|
|
657
|
-
endLine: z5.number().int().positive(),
|
|
658
|
-
sha256: HashSchema
|
|
659
|
-
});
|
|
660
|
-
var AiCoverageSchema = z5.object({
|
|
661
|
-
totalFiles: z5.number().int(),
|
|
662
|
-
readFiles: z5.array(FilePathSchema),
|
|
663
|
-
partial: z5.boolean(),
|
|
664
|
-
limitations: z5.array(z5.string())
|
|
665
|
-
});
|
|
666
|
-
var AiResultSchema = z5.object({
|
|
667
|
-
summary: z5.string().min(1).max(2e4),
|
|
668
|
-
findings: z5.array(z5.string().max(2e3)).max(30),
|
|
669
|
-
uncertainties: z5.array(z5.string().max(2e3)).max(30),
|
|
670
|
-
citations: z5.array(AiCitationSchema).max(50),
|
|
671
|
-
coverage: AiCoverageSchema
|
|
672
|
-
});
|
|
673
|
-
var TaskAnalysisResultSchema = z5.object({
|
|
674
|
-
kind: z5.literal("task-bootstrap"),
|
|
675
|
-
tasks: z5.array(
|
|
676
|
-
TaskFieldsSchema.extend({
|
|
677
|
-
taskId: z5.uuid().nullable().optional(),
|
|
678
|
-
sources: z5.array(TaskSourceSchema).min(1).max(50)
|
|
679
|
-
})
|
|
680
|
-
).max(100),
|
|
681
|
-
uncertainties: z5.array(z5.string().max(2e3)).max(50),
|
|
682
|
-
coverage: AiCoverageSchema
|
|
683
|
-
});
|
|
684
|
-
var AiWorkflowResultSchema = z5.union([AiResultSchema, TaskAnalysisResultSchema]);
|
|
685
|
-
var AiUsageSchema = z5.object({
|
|
686
|
-
inputTokens: z5.number().nonnegative(),
|
|
687
|
-
outputTokens: z5.number().nonnegative()
|
|
688
|
-
});
|
|
689
|
-
var AiJobSchema = z5.object({
|
|
690
|
-
id: z5.uuid(),
|
|
691
|
-
requester: NameSchema,
|
|
692
|
-
workflow: AiJobCreateSchema.shape.workflow,
|
|
693
|
-
question: z5.string(),
|
|
694
|
-
profileId: NameSchema,
|
|
695
|
-
model: z5.string(),
|
|
696
|
-
profileRevision: z5.number().int(),
|
|
697
|
-
promptVersion: z5.string(),
|
|
698
|
-
origin: z5.enum(["manual", "automatic"]).default("manual"),
|
|
699
|
-
status: z5.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
|
|
700
|
-
createdAt: z5.string(),
|
|
701
|
-
updatedAt: z5.string(),
|
|
702
|
-
attemptCount: z5.number().int(),
|
|
703
|
-
error: z5.string().nullable(),
|
|
704
|
-
input: z5.object({
|
|
705
|
-
owner: NameSchema,
|
|
706
|
-
kind: z5.enum(["file", "directory"]),
|
|
707
|
-
path: z5.string(),
|
|
708
|
-
revision: z5.number().int(),
|
|
709
|
-
fileCount: z5.number().int(),
|
|
710
|
-
fingerprint: HashSchema
|
|
711
|
-
}),
|
|
712
|
-
result: AiWorkflowResultSchema.nullable(),
|
|
713
|
-
sourceChanged: z5.boolean()
|
|
714
|
-
});
|
|
715
|
-
var AiJobsSchema = z5.object({
|
|
716
|
-
jobs: z5.array(AiJobSchema),
|
|
717
|
-
nextCursor: z5.string().nullable()
|
|
718
|
-
});
|
|
719
|
-
var AiEventSchema = z5.object({
|
|
720
|
-
seq: z5.number().int(),
|
|
721
|
-
attempt: z5.number().int(),
|
|
722
|
-
kind: z5.string(),
|
|
723
|
-
message: z5.string(),
|
|
724
|
-
createdAt: z5.string()
|
|
725
|
-
});
|
|
726
|
-
var AiEventsSchema = z5.object({
|
|
727
|
-
events: z5.array(AiEventSchema),
|
|
728
|
-
nextCursor: z5.number().int()
|
|
729
|
-
});
|
|
730
|
-
var AiAttemptSchema = z5.object({
|
|
731
|
-
number: z5.number().int(),
|
|
732
|
-
status: z5.string(),
|
|
733
|
-
startedAt: z5.string(),
|
|
734
|
-
endedAt: z5.string().nullable(),
|
|
735
|
-
error: z5.string().nullable(),
|
|
736
|
-
usage: AiUsageSchema.nullable()
|
|
737
|
-
});
|
|
738
|
-
var AiJobDetailSchema = z5.object({ job: AiJobSchema, attempts: z5.array(AiAttemptSchema) });
|
|
739
|
-
|
|
740
|
-
// apps/server/project.ts
|
|
741
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
742
356
|
|
|
743
357
|
// apps/server/site.ts
|
|
744
|
-
import { randomBytes
|
|
745
|
-
var
|
|
358
|
+
import { randomBytes, createHash as createHash2 } from "node:crypto";
|
|
359
|
+
var digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
746
360
|
function requireSiteAdmin(identity) {
|
|
747
361
|
if (identity.siteRole !== "admin")
|
|
748
362
|
throw new HubError(403, "SITE_ADMIN_REQUIRED", "Site administrator access required.");
|
|
@@ -791,15 +405,15 @@ var SiteRepository = class {
|
|
|
791
405
|
const row = this.hub.db.prepare("SELECT 1 FROM users WHERE id=? AND enabled=1").get(id);
|
|
792
406
|
if (!row)
|
|
793
407
|
throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
|
|
794
|
-
const token =
|
|
408
|
+
const token = randomBytes(32).toString("hex");
|
|
795
409
|
this.hub.db.prepare("DELETE FROM web_sessions WHERE expires_at<=?").run(Date.now());
|
|
796
|
-
this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?)").run(
|
|
410
|
+
this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?)").run(digest(token), id, Date.now() + 7 * 864e5);
|
|
797
411
|
return { token, session: this.identity(token) };
|
|
798
412
|
}
|
|
799
413
|
identity(token) {
|
|
800
414
|
const row = token && /^[a-f0-9]{64}$/.test(token) ? this.hub.db.prepare(
|
|
801
415
|
`SELECT u.* FROM web_sessions s JOIN users u ON u.id=s.member_id WHERE s.token_hash=? AND s.expires_at>? AND u.enabled=1`
|
|
802
|
-
).get(
|
|
416
|
+
).get(digest(token), Date.now()) : void 0;
|
|
803
417
|
if (!row)
|
|
804
418
|
throw new HubError(401, "LOGIN_REQUIRED", "Please sign in with an existing member ID.");
|
|
805
419
|
return {
|
|
@@ -810,7 +424,7 @@ var SiteRepository = class {
|
|
|
810
424
|
}
|
|
811
425
|
logout(token) {
|
|
812
426
|
if (token)
|
|
813
|
-
this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(
|
|
427
|
+
this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(digest(token));
|
|
814
428
|
}
|
|
815
429
|
users(identity) {
|
|
816
430
|
requireSiteAdmin(identity);
|
|
@@ -881,7 +495,7 @@ var ProjectRepository = class {
|
|
|
881
495
|
return ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
|
|
882
496
|
}
|
|
883
497
|
insertProject(value, creator, url) {
|
|
884
|
-
const fingerprint =
|
|
498
|
+
const fingerprint = digest(JSON.stringify([creator, value]));
|
|
885
499
|
const prior = this.hub.db.prepare("SELECT id,fingerprint FROM projects WHERE operation_id=?").get(value.operationId);
|
|
886
500
|
if (prior) {
|
|
887
501
|
if (prior.fingerprint !== fingerprint)
|
|
@@ -894,7 +508,7 @@ var ProjectRepository = class {
|
|
|
894
508
|
}
|
|
895
509
|
const config = ProjectConfigSchema.parse({
|
|
896
510
|
schemaVersion: 1,
|
|
897
|
-
projectId: `proj-${
|
|
511
|
+
projectId: `proj-${randomBytes2(8).toString("hex")}`,
|
|
898
512
|
projectName: value.projectName,
|
|
899
513
|
description: value.description,
|
|
900
514
|
publicServerUrl: url,
|
|
@@ -904,11 +518,12 @@ var ProjectRepository = class {
|
|
|
904
518
|
});
|
|
905
519
|
this.hub.db.prepare("INSERT INTO projects VALUES (?,?,?,?)").run(config.projectId, JSON.stringify(config), value.operationId, fingerprint);
|
|
906
520
|
this.hub.db.prepare("INSERT INTO members(workspace_id,member_id) VALUES (?,?)").run(config.projectId, creator);
|
|
521
|
+
this.hub.db.prepare("INSERT INTO project_files(workspace_id,maintainer,paths_json) VALUES (?,?,?)").run(config.projectId, creator, JSON.stringify(DEFAULT_SHARED_PATHS));
|
|
907
522
|
return config;
|
|
908
523
|
}
|
|
909
524
|
setup(input) {
|
|
910
525
|
const value = SetupSchema.parse(input);
|
|
911
|
-
const fingerprint =
|
|
526
|
+
const fingerprint = digest(JSON.stringify(value));
|
|
912
527
|
return transaction(this.hub.db, () => {
|
|
913
528
|
const prior = this.hub.db.prepare("SELECT setup_operation,setup_fingerprint FROM site_settings").get();
|
|
914
529
|
if (prior) {
|
|
@@ -1084,236 +699,60 @@ var ProjectRepository = class {
|
|
|
1084
699
|
}
|
|
1085
700
|
};
|
|
1086
701
|
|
|
1087
|
-
// apps/server/
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
702
|
+
// apps/server/shared-files.ts
|
|
703
|
+
var sharedFilesMigration = `
|
|
704
|
+
CREATE TABLE project_files (
|
|
705
|
+
workspace_id TEXT PRIMARY KEY, maintainer TEXT, paths_json TEXT NOT NULL,
|
|
706
|
+
config_revision INTEGER NOT NULL DEFAULT 1, file_revision INTEGER NOT NULL DEFAULT 0
|
|
707
|
+
);
|
|
708
|
+
INSERT INTO project_files(workspace_id,maintainer,paths_json)
|
|
709
|
+
SELECT p.id,(SELECT member_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=p.id AND m.enabled=1 AND u.enabled=1 ORDER BY m.rowid LIMIT 1),'["README.md","_template/"]' FROM projects p;
|
|
710
|
+
`;
|
|
711
|
+
var SharedFilesRepository = class {
|
|
1092
712
|
constructor(hub) {
|
|
1093
713
|
this.hub = hub;
|
|
1094
714
|
}
|
|
1095
715
|
hub;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const key = readFileSync(path);
|
|
1106
|
-
if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
|
|
1107
|
-
return key;
|
|
1108
|
-
}
|
|
1109
|
-
encrypt(value, id) {
|
|
1110
|
-
const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
|
|
1111
|
-
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1112
|
-
const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
1113
|
-
return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
|
|
1114
|
-
}
|
|
1115
|
-
list() {
|
|
1116
|
-
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
|
|
1117
|
-
...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
|
|
1118
|
-
revision: Number(row.revision),
|
|
1119
|
-
keyConfigured: !!row.secret
|
|
1120
|
-
}));
|
|
1121
|
-
}
|
|
1122
|
-
get(id) {
|
|
1123
|
-
const profile = this.list().find((p) => p.id === id);
|
|
1124
|
-
if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
|
|
1125
|
-
const { keyConfigured: _, ...result } = profile;
|
|
1126
|
-
return result;
|
|
1127
|
-
}
|
|
1128
|
-
secret(id) {
|
|
1129
|
-
const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
|
|
1130
|
-
if (!row?.secret) return "";
|
|
1131
|
-
const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
|
|
1132
|
-
const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
|
|
1133
|
-
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1134
|
-
cipher.setAuthTag(tag);
|
|
1135
|
-
return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
|
|
1136
|
-
}
|
|
1137
|
-
save(session, value) {
|
|
1138
|
-
requireSiteAdmin(session);
|
|
1139
|
-
const input = AiProfileUpdateSchema.parse(value), { profile } = input;
|
|
1140
|
-
return transaction(this.hub.db, () => {
|
|
1141
|
-
const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
|
|
1142
|
-
if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
|
|
1143
|
-
throw new HubError(
|
|
1144
|
-
409,
|
|
1145
|
-
"AI_CONFIG_CONFLICT",
|
|
1146
|
-
"Model configuration changed; reload before saving."
|
|
1147
|
-
);
|
|
1148
|
-
if (!old && this.list().length >= 20)
|
|
1149
|
-
throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
|
|
1150
|
-
const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
|
|
1151
|
-
this.hub.db.prepare(
|
|
1152
|
-
"INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
|
|
1153
|
-
).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
|
|
1154
|
-
return this.list().find((p) => p.id === profile.id);
|
|
716
|
+
config(identity) {
|
|
717
|
+
new ProjectRepository(this.hub).config(identity);
|
|
718
|
+
const row = this.hub.db.prepare("SELECT * FROM project_files WHERE workspace_id=?").get(identity.workspaceId);
|
|
719
|
+
if (!row)
|
|
720
|
+
throw new HubError(500, "SHARED_CONFIG_MISSING", "Project file settings are missing.");
|
|
721
|
+
return SharedFilesConfigSchema.parse({
|
|
722
|
+
maintainer: row.maintainer,
|
|
723
|
+
paths: JSON.parse(String(row.paths_json)),
|
|
724
|
+
revision: row.config_revision
|
|
1155
725
|
});
|
|
1156
726
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
hub;
|
|
1165
|
-
read(scope) {
|
|
1166
|
-
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
|
|
1167
|
-
return {
|
|
1168
|
-
profileId: row?.profile_id ? String(row.profile_id) : null,
|
|
1169
|
-
revision: Number(row?.revision ?? 0)
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
view(projectId2) {
|
|
1173
|
-
const site = this.read("site");
|
|
1174
|
-
const own = projectId2 ? this.read(`project:${projectId2}`) : site;
|
|
1175
|
-
const effectiveProfileId = own.profileId ?? site.profileId;
|
|
1176
|
-
return {
|
|
1177
|
-
...own,
|
|
1178
|
-
effectiveProfileId,
|
|
1179
|
-
source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
|
|
1180
|
-
available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
site() {
|
|
1184
|
-
return this.view();
|
|
1185
|
-
}
|
|
1186
|
-
project(projectId2) {
|
|
1187
|
-
return this.view(projectId2);
|
|
1188
|
-
}
|
|
1189
|
-
save(scope, value) {
|
|
1190
|
-
const input = AiDefaultUpdateSchema.parse(value);
|
|
1191
|
-
transaction(this.hub.db, () => {
|
|
1192
|
-
if (this.read(scope).revision !== input.expectedRevision)
|
|
727
|
+
update(identity, input) {
|
|
728
|
+
new ProjectRepository(this.hub).requireMember(identity);
|
|
729
|
+
const value = SharedFilesUpdateSchema.parse(input);
|
|
730
|
+
return transaction(this.hub.db, () => {
|
|
731
|
+
const current = this.config(identity);
|
|
732
|
+
if (current.revision !== value.expectedRevision)
|
|
1193
733
|
throw new HubError(
|
|
1194
734
|
409,
|
|
1195
|
-
"
|
|
1196
|
-
"
|
|
735
|
+
"STALE_CONFIG",
|
|
736
|
+
"Shared file settings changed; reload before saving."
|
|
1197
737
|
);
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
738
|
+
if (value.maintainer && !this.hub.db.prepare(
|
|
739
|
+
"SELECT 1 FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=? AND m.member_id=? AND m.enabled=1 AND u.enabled=1"
|
|
740
|
+
).get(identity.workspaceId, value.maintainer))
|
|
741
|
+
throw new HubError(400, "INVALID_MAINTAINER", "Choose an active project member.");
|
|
742
|
+
if (current.maintainer !== value.maintainer || JSON.stringify(current.paths) !== JSON.stringify(value.paths))
|
|
743
|
+
this.hub.db.prepare(
|
|
744
|
+
"UPDATE project_files SET maintainer=?,paths_json=?,config_revision=config_revision+1 WHERE workspace_id=?"
|
|
745
|
+
).run(value.maintainer, JSON.stringify(value.paths), identity.workspaceId);
|
|
746
|
+
return this.config(identity);
|
|
1203
747
|
});
|
|
1204
748
|
}
|
|
1205
|
-
saveSite(identity, value) {
|
|
1206
|
-
requireSiteAdmin(identity);
|
|
1207
|
-
this.save("site", value);
|
|
1208
|
-
return this.site();
|
|
1209
|
-
}
|
|
1210
|
-
saveProject(identity, value) {
|
|
1211
|
-
new ProjectRepository(this.hub).requireMember(identity);
|
|
1212
|
-
this.save(`project:${identity.workspaceId}`, value);
|
|
1213
|
-
return this.project(identity.workspaceId);
|
|
1214
|
-
}
|
|
1215
|
-
resolve(projectId2, explicit) {
|
|
1216
|
-
const id = explicit ?? this.project(projectId2).effectiveProfileId;
|
|
1217
|
-
if (!id)
|
|
1218
|
-
throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
|
|
1219
|
-
const profile = new AiConfig(this.hub).get(id);
|
|
1220
|
-
if (!profile.enabled)
|
|
1221
|
-
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1222
|
-
return profile;
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
|
|
1226
|
-
// apps/server/ai/snapshots.ts
|
|
1227
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
1228
|
-
|
|
1229
|
-
// apps/server/files.ts
|
|
1230
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1231
|
-
import { join as join4 } from "node:path";
|
|
1232
|
-
|
|
1233
|
-
// packages/storage/local-blob-store.ts
|
|
1234
|
-
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
1235
|
-
import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
1236
|
-
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
1237
|
-
function hash(content) {
|
|
1238
|
-
return createHash3("sha256").update(content).digest("hex");
|
|
1239
|
-
}
|
|
1240
|
-
function hasCode(error, code) {
|
|
1241
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
1242
|
-
}
|
|
1243
|
-
var BlobIntegrityError = class extends Error {
|
|
1244
|
-
constructor(sha256) {
|
|
1245
|
-
super(`Stored content failed integrity verification: ${sha256}`);
|
|
1246
|
-
this.sha256 = sha256;
|
|
1247
|
-
this.name = "BlobIntegrityError";
|
|
1248
|
-
}
|
|
1249
|
-
sha256;
|
|
1250
|
-
};
|
|
1251
|
-
var LocalBlobStore = class {
|
|
1252
|
-
root;
|
|
1253
|
-
constructor(root) {
|
|
1254
|
-
this.root = resolve(root);
|
|
1255
|
-
}
|
|
1256
|
-
async put(content) {
|
|
1257
|
-
const bytes = Buffer.from(content);
|
|
1258
|
-
const sha256 = hash(bytes);
|
|
1259
|
-
const target = this.pathFor(sha256);
|
|
1260
|
-
await mkdir(dirname2(target), { recursive: true, mode: 448 });
|
|
1261
|
-
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
1262
|
-
const file = await open(temporary, "wx", 384);
|
|
1263
|
-
try {
|
|
1264
|
-
try {
|
|
1265
|
-
await file.writeFile(bytes);
|
|
1266
|
-
await file.sync();
|
|
1267
|
-
} finally {
|
|
1268
|
-
await file.close();
|
|
1269
|
-
}
|
|
1270
|
-
try {
|
|
1271
|
-
await link(temporary, target);
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
if (!hasCode(error, "EEXIST")) throw error;
|
|
1274
|
-
await this.get(sha256);
|
|
1275
|
-
}
|
|
1276
|
-
if (process.platform !== "win32") {
|
|
1277
|
-
let directory = dirname2(target);
|
|
1278
|
-
for (; ; ) {
|
|
1279
|
-
const handle = await open(directory, "r");
|
|
1280
|
-
try {
|
|
1281
|
-
await handle.sync();
|
|
1282
|
-
} finally {
|
|
1283
|
-
await handle.close();
|
|
1284
|
-
}
|
|
1285
|
-
if (directory === dirname2(this.root)) break;
|
|
1286
|
-
directory = dirname2(directory);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
} finally {
|
|
1290
|
-
await unlink(temporary);
|
|
1291
|
-
}
|
|
1292
|
-
return { sha256, size: bytes.byteLength };
|
|
1293
|
-
}
|
|
1294
|
-
async get(sha256) {
|
|
1295
|
-
const target = this.pathFor(sha256);
|
|
1296
|
-
let content;
|
|
1297
|
-
try {
|
|
1298
|
-
content = await readFile(target);
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
if (hasCode(error, "ENOENT")) return void 0;
|
|
1301
|
-
throw error;
|
|
1302
|
-
}
|
|
1303
|
-
if (hash(content) !== sha256) throw new BlobIntegrityError(sha256);
|
|
1304
|
-
return content;
|
|
1305
|
-
}
|
|
1306
|
-
pathFor(sha256) {
|
|
1307
|
-
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
1308
|
-
return join3(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
1309
|
-
}
|
|
1310
749
|
};
|
|
1311
750
|
|
|
1312
751
|
// apps/server/files.ts
|
|
1313
752
|
var FileRepository = class {
|
|
1314
753
|
constructor(hub) {
|
|
1315
754
|
this.hub = hub;
|
|
1316
|
-
this.blobs = new LocalBlobStore(
|
|
755
|
+
this.blobs = new LocalBlobStore(join2(hub.directory, "blobs"));
|
|
1317
756
|
}
|
|
1318
757
|
hub;
|
|
1319
758
|
blobs;
|
|
@@ -1325,7 +764,7 @@ var FileRepository = class {
|
|
|
1325
764
|
}
|
|
1326
765
|
async upload(authorization, device, hash2, bytes) {
|
|
1327
766
|
this.hub.authorize(authorization, device);
|
|
1328
|
-
if (
|
|
767
|
+
if (createHash3("sha256").update(bytes).digest("hex") !== HashSchema.parse(hash2))
|
|
1329
768
|
throw new HubError(400, "HASH_MISMATCH", "Content does not match its SHA-256.");
|
|
1330
769
|
const ref = await this.blobs.put(bytes);
|
|
1331
770
|
transaction(this.hub.db, () => {
|
|
@@ -1342,19 +781,41 @@ var FileRepository = class {
|
|
|
1342
781
|
return bytes;
|
|
1343
782
|
}
|
|
1344
783
|
head(identity, owner) {
|
|
784
|
+
if (owner === PROJECT_FILE_OWNER) {
|
|
785
|
+
new SharedFilesRepository(this.hub).config(identity);
|
|
786
|
+
return Number(
|
|
787
|
+
this.hub.db.prepare("SELECT file_revision FROM project_files WHERE workspace_id=?").get(identity.workspaceId).file_revision
|
|
788
|
+
);
|
|
789
|
+
}
|
|
1345
790
|
const row = this.hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(identity.workspaceId, owner);
|
|
1346
791
|
if (!row) throw new HubError(404, "MEMBER_NOT_FOUND", "Member is not part of this workspace.");
|
|
1347
792
|
return Number(row.file_revision);
|
|
1348
793
|
}
|
|
1349
|
-
commit(authorization, device, input) {
|
|
794
|
+
commit(authorization, device, input, sharedConfigRevision) {
|
|
1350
795
|
const batch = FileCommitSchema.parse(input);
|
|
796
|
+
if (batch.changes.some((change) => privateWorkspacePath(change.path)))
|
|
797
|
+
throw new HubError(400, "PRIVATE_PATH", "Teammy configuration is not a shared document.");
|
|
1351
798
|
const changes = [...batch.changes].sort(
|
|
1352
799
|
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
1353
800
|
);
|
|
1354
|
-
const fingerprint =
|
|
801
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify({ parentRevision: batch.parentRevision, changes })).digest("hex");
|
|
1355
802
|
return transaction(this.hub.db, () => {
|
|
1356
803
|
const identity = this.hub.authorize(authorization, device);
|
|
1357
|
-
const
|
|
804
|
+
const workspace = identity.workspaceId;
|
|
805
|
+
const owner = sharedConfigRevision === void 0 ? identity.memberId : PROJECT_FILE_OWNER;
|
|
806
|
+
if (owner === PROJECT_FILE_OWNER) {
|
|
807
|
+
const config = new SharedFilesRepository(this.hub).config(identity);
|
|
808
|
+
if (config.maintainer !== identity.memberId)
|
|
809
|
+
throw new HubError(
|
|
810
|
+
403,
|
|
811
|
+
"SHARED_FILES_READ_ONLY",
|
|
812
|
+
"Only the designated maintainer can publish project files."
|
|
813
|
+
);
|
|
814
|
+
if (config.revision !== sharedConfigRevision)
|
|
815
|
+
throw new HubError(409, "STALE_CONFIG", "Shared file settings changed.");
|
|
816
|
+
if (batch.changes.some((change) => !sharedPathAllowed(change.path, config.paths)))
|
|
817
|
+
throw new HubError(403, "SHARED_PATH_DENIED", "Path is outside the shared file list.");
|
|
818
|
+
}
|
|
1358
819
|
const prior = this.hub.db.prepare(
|
|
1359
820
|
"SELECT fingerprint, revision FROM file_operations WHERE workspace_id=? AND owner=? AND operation_id=?"
|
|
1360
821
|
).get(workspace, owner, batch.operationId);
|
|
@@ -1451,7 +912,10 @@ var FileRepository = class {
|
|
|
1451
912
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1452
913
|
JSON.stringify(effective)
|
|
1453
914
|
);
|
|
1454
|
-
|
|
915
|
+
if (owner === PROJECT_FILE_OWNER)
|
|
916
|
+
this.hub.db.prepare("UPDATE project_files SET file_revision=? WHERE workspace_id=?").run(revision, workspace);
|
|
917
|
+
else
|
|
918
|
+
this.hub.db.prepare("UPDATE members SET file_revision=? WHERE workspace_id=? AND member_id=?").run(revision, workspace, owner);
|
|
1455
919
|
}
|
|
1456
920
|
this.hub.db.prepare("INSERT INTO file_operations VALUES (?, ?, ?, ?, ?)").run(workspace, owner, batch.operationId, fingerprint, revision);
|
|
1457
921
|
return { operationId: batch.operationId, revision };
|
|
@@ -1567,10 +1031,15 @@ var FileRepository = class {
|
|
|
1567
1031
|
};
|
|
1568
1032
|
|
|
1569
1033
|
// apps/server/ai/snapshots.ts
|
|
1034
|
+
function taskSnapshotCurrent(hub, workspace, snapshot) {
|
|
1035
|
+
const member = hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(workspace, snapshot.owner);
|
|
1036
|
+
const project = hub.db.prepare("SELECT file_revision FROM project_files WHERE workspace_id=?").get(workspace);
|
|
1037
|
+
return Number(member?.file_revision) === snapshot.revision && Number(project?.file_revision ?? 0) === (snapshot.project?.revision ?? 0);
|
|
1038
|
+
}
|
|
1570
1039
|
function snapshotDigest(files) {
|
|
1571
|
-
return
|
|
1040
|
+
return createHash4("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
|
|
1572
1041
|
}
|
|
1573
|
-
function
|
|
1042
|
+
function captureScope(hub, identity, selection, allowEmpty = false) {
|
|
1574
1043
|
const repository = new FileRepository(hub);
|
|
1575
1044
|
const files = [];
|
|
1576
1045
|
let cursor = "";
|
|
@@ -1600,36 +1069,61 @@ function captureSnapshot(hub, identity, selection, allowEmpty = false) {
|
|
|
1600
1069
|
fingerprint: snapshotDigest(files)
|
|
1601
1070
|
};
|
|
1602
1071
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1072
|
+
function captureSnapshot(hub, identity, selection, options = {}) {
|
|
1073
|
+
const snapshot = captureScope(hub, identity, selection, options.allowEmpty);
|
|
1074
|
+
if (options.projectReferences && hub.db.prepare("SELECT 1 FROM project_files WHERE workspace_id=?").get(identity.workspaceId)) {
|
|
1075
|
+
const shared = captureScope(
|
|
1076
|
+
hub,
|
|
1077
|
+
identity,
|
|
1078
|
+
{ owner: PROJECT_FILE_OWNER, kind: "directory", path: "" },
|
|
1079
|
+
true
|
|
1080
|
+
);
|
|
1081
|
+
snapshot.project = {
|
|
1082
|
+
revision: shared.revision,
|
|
1083
|
+
files: shared.files,
|
|
1084
|
+
fingerprint: shared.fingerprint
|
|
1085
|
+
};
|
|
1086
|
+
if (shared.revision)
|
|
1087
|
+
snapshot.fingerprint = createHash4("sha256").update(snapshot.fingerprint + ":" + shared.fingerprint).digest("hex");
|
|
1088
|
+
}
|
|
1089
|
+
return snapshot;
|
|
1090
|
+
}
|
|
1091
|
+
var SnapshotReader = class _SnapshotReader {
|
|
1092
|
+
constructor(hub, identity, snapshot, budget = Number.POSITIVE_INFINITY, sharedBudget) {
|
|
1605
1093
|
this.hub = hub;
|
|
1606
1094
|
this.identity = identity;
|
|
1607
1095
|
this.snapshot = snapshot;
|
|
1608
|
-
this.
|
|
1096
|
+
this.budget = sharedBudget ?? { remaining: budget };
|
|
1609
1097
|
}
|
|
1610
1098
|
hub;
|
|
1611
1099
|
identity;
|
|
1612
1100
|
snapshot;
|
|
1613
1101
|
readFiles = /* @__PURE__ */ new Set();
|
|
1614
|
-
lineCounts = /* @__PURE__ */ new Map();
|
|
1615
|
-
readRanges = /* @__PURE__ */ new Map();
|
|
1616
1102
|
limitations = /* @__PURE__ */ new Set();
|
|
1617
|
-
|
|
1103
|
+
budget;
|
|
1104
|
+
commonReader;
|
|
1105
|
+
projectReader() {
|
|
1106
|
+
const project = this.snapshot.project;
|
|
1107
|
+
if (!project)
|
|
1108
|
+
throw new HubError(403, "AI_SCOPE_VIOLATION", "No project reference snapshot is available.");
|
|
1109
|
+
return this.commonReader ??= new _SnapshotReader(
|
|
1110
|
+
this.hub,
|
|
1111
|
+
this.identity,
|
|
1112
|
+
{ ...project, owner: PROJECT_FILE_OWNER, kind: "directory", path: "" },
|
|
1113
|
+
0,
|
|
1114
|
+
this.budget
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
forSnapshot(snapshot) {
|
|
1118
|
+
return new _SnapshotReader(this.hub, this.identity, snapshot, 0, this.budget);
|
|
1119
|
+
}
|
|
1618
1120
|
afterIndex(after) {
|
|
1619
1121
|
if (!after) return 0;
|
|
1620
1122
|
const index = this.snapshot.files.findIndex((f) => f.path === after);
|
|
1621
1123
|
if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
|
|
1622
1124
|
return index + 1;
|
|
1623
1125
|
}
|
|
1624
|
-
|
|
1625
|
-
let end = 0;
|
|
1626
|
-
for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
|
|
1627
|
-
if (range[0] > end + 1) break;
|
|
1628
|
-
end = Math.max(end, range[1]);
|
|
1629
|
-
}
|
|
1630
|
-
return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
|
|
1631
|
-
}
|
|
1632
|
-
list(after = "", limit = 50) {
|
|
1126
|
+
list(after = "", limit = this.snapshot.files.length) {
|
|
1633
1127
|
const entries = this.snapshot.files.slice(
|
|
1634
1128
|
this.afterIndex(after),
|
|
1635
1129
|
this.afterIndex(after) + limit + 1
|
|
@@ -1642,12 +1136,10 @@ var SnapshotReader = class {
|
|
|
1642
1136
|
}
|
|
1643
1137
|
source(path) {
|
|
1644
1138
|
const file = this.snapshot.files.find((entry) => entry.path === path);
|
|
1645
|
-
if (!file
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
"Task source must be an actually read snapshot file."
|
|
1650
|
-
);
|
|
1139
|
+
if (!file) {
|
|
1140
|
+
this.limitations.add(`\u6765\u6E90\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${path}`);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1651
1143
|
const row = this.hub.db.prepare(
|
|
1652
1144
|
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
1653
1145
|
).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
|
|
@@ -1655,45 +1147,35 @@ var SnapshotReader = class {
|
|
|
1655
1147
|
throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
|
|
1656
1148
|
return { ...file, syncedAt: String(row.created_at) };
|
|
1657
1149
|
}
|
|
1658
|
-
async
|
|
1150
|
+
async bytes(path) {
|
|
1659
1151
|
const entry = this.snapshot.files.find((f) => f.path === path);
|
|
1660
1152
|
if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
|
|
1661
|
-
if (entry.size > 256 * 1024)
|
|
1662
|
-
throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
|
|
1663
1153
|
const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
|
|
1664
|
-
if (bytes.byteLength !== entry.size ||
|
|
1154
|
+
if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
1665
1155
|
throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1156
|
+
return bytes;
|
|
1157
|
+
}
|
|
1158
|
+
async text(path) {
|
|
1159
|
+
return new TextDecoder().decode(await this.bytes(path));
|
|
1160
|
+
}
|
|
1161
|
+
consume(size) {
|
|
1162
|
+
if (size > this.budget.remaining) {
|
|
1163
|
+
this.limitations.add("\u672C\u6B21\u5206\u6790\u7684\u7D2F\u8BA1\u6B63\u6587\u8BFB\u53D6\u91CF\u5DF2\u8FBE\u5230\u914D\u7F6E\u989D\u5EA6\u3002");
|
|
1164
|
+
throw new HubError(413, "AI_READ_BUDGET", "Workflow read allowance exceeded.");
|
|
1671
1165
|
}
|
|
1672
|
-
|
|
1673
|
-
throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
|
|
1674
|
-
this.lineCounts.set(path, text.split("\n").length);
|
|
1675
|
-
return text;
|
|
1166
|
+
this.budget.remaining -= size;
|
|
1676
1167
|
}
|
|
1677
|
-
async read(path, startLine = 1, limit
|
|
1168
|
+
async read(path, startLine = 1, limit) {
|
|
1678
1169
|
const text = await this.text(path), lines = text.split("\n");
|
|
1679
1170
|
if (startLine > lines.length)
|
|
1680
1171
|
throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
|
|
1681
|
-
const selected =
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
selected.push(line);
|
|
1687
|
-
size += cost;
|
|
1688
|
-
}
|
|
1689
|
-
if (!selected.length) {
|
|
1690
|
-
this.limitations.add("\u8BFB\u53D6\u9884\u7B97\u6216\u5355\u884C\u957F\u5EA6\u9650\u5236\u963B\u6B62\u4E86\u7EE7\u7EED\u8BFB\u53D6\u3002");
|
|
1691
|
-
throw new HubError(413, "AI_READ_BUDGET", "Read budget exceeded or line is too long.");
|
|
1692
|
-
}
|
|
1693
|
-
this.remaining -= size;
|
|
1172
|
+
const selected = lines.slice(
|
|
1173
|
+
startLine - 1,
|
|
1174
|
+
limit === void 0 ? void 0 : startLine - 1 + limit
|
|
1175
|
+
);
|
|
1176
|
+
this.consume(Buffer.byteLength(selected.join("\n")));
|
|
1694
1177
|
this.readFiles.add(path);
|
|
1695
1178
|
const endLine = startLine + selected.length - 1;
|
|
1696
|
-
this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
|
|
1697
1179
|
return {
|
|
1698
1180
|
path,
|
|
1699
1181
|
startLine,
|
|
@@ -1703,56 +1185,713 @@ var SnapshotReader = class {
|
|
|
1703
1185
|
hasMore: endLine < lines.length
|
|
1704
1186
|
};
|
|
1705
1187
|
}
|
|
1706
|
-
async whole(path
|
|
1188
|
+
async whole(path) {
|
|
1707
1189
|
const text = await this.text(path);
|
|
1708
|
-
|
|
1709
|
-
throw new HubError(
|
|
1710
|
-
413,
|
|
1711
|
-
"AI_CONTEXT_LIMIT",
|
|
1712
|
-
"File is too large for this model budget; use directory analysis to read portions."
|
|
1713
|
-
);
|
|
1190
|
+
this.consume(Buffer.byteLength(text));
|
|
1714
1191
|
this.readFiles.add(path);
|
|
1715
|
-
this.readRanges.set(path, [[1, text.split("\n").length]]);
|
|
1716
1192
|
return text;
|
|
1717
1193
|
}
|
|
1718
|
-
async search(query, after = "",
|
|
1719
|
-
const
|
|
1194
|
+
async search(query, after = "", files = this.snapshot.files) {
|
|
1195
|
+
const fingerprint = snapshotDigest(files) + ":" + this.snapshot.owner + ":" + this.snapshot.revision;
|
|
1196
|
+
let fileIndex = 0, startLine = 1;
|
|
1197
|
+
if (after) {
|
|
1198
|
+
try {
|
|
1199
|
+
const cursor = JSON.parse(Buffer.from(after, "base64url").toString("utf8"));
|
|
1200
|
+
fileIndex = files.findIndex((file) => file.path === cursor.path);
|
|
1201
|
+
if (fileIndex < 0 || cursor.query !== query || cursor.fingerprint !== fingerprint || !Number.isSafeInteger(cursor.line) || cursor.line < 1)
|
|
1202
|
+
throw new Error();
|
|
1203
|
+
startLine = cursor.line;
|
|
1204
|
+
} catch {
|
|
1205
|
+
throw new HubError(
|
|
1206
|
+
400,
|
|
1207
|
+
"AI_CURSOR_INVALID",
|
|
1208
|
+
"Search cursor does not match this query and snapshot."
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1720
1212
|
const matches = [];
|
|
1721
|
-
|
|
1722
|
-
for (
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1213
|
+
const needle = query.toLowerCase();
|
|
1214
|
+
for (let f = fileIndex; f < files.length; f++) {
|
|
1215
|
+
const file = files[f];
|
|
1216
|
+
const lines = (await this.text(file.path)).split("\n");
|
|
1217
|
+
for (let i = f === fileIndex ? startLine - 1 : 0; i < lines.length; i++) {
|
|
1218
|
+
if (!lines[i].toLowerCase().includes(needle)) continue;
|
|
1219
|
+
if (matches.length === 100) {
|
|
1220
|
+
return {
|
|
1221
|
+
matches,
|
|
1222
|
+
nextCursor: Buffer.from(
|
|
1223
|
+
JSON.stringify({
|
|
1224
|
+
fingerprint,
|
|
1225
|
+
query,
|
|
1226
|
+
path: file.path,
|
|
1227
|
+
line: i + 1
|
|
1228
|
+
})
|
|
1229
|
+
).toString("base64url"),
|
|
1230
|
+
limited: true
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
matches.push({ path: file.path, line: i + 1, excerpt: lines[i] });
|
|
1726
1234
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1235
|
+
}
|
|
1236
|
+
return { matches, nextCursor: null, limited: false };
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
// apps/server/ai/worker.ts
|
|
1241
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1242
|
+
|
|
1243
|
+
// apps/server/store.ts
|
|
1244
|
+
import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
|
|
1245
|
+
import { join as join3 } from "node:path";
|
|
1246
|
+
import { existsSync } from "node:fs";
|
|
1247
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1248
|
+
|
|
1249
|
+
// apps/server/session-schema.ts
|
|
1250
|
+
var sessionMigration = String.raw`
|
|
1251
|
+
CREATE TABLE sessions (
|
|
1252
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1253
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1254
|
+
agent TEXT NOT NULL, native_id TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL,
|
|
1255
|
+
revision INTEGER NOT NULL, updated_at TEXT NOT NULL, message_count INTEGER NOT NULL,
|
|
1256
|
+
hash TEXT NOT NULL, size INTEGER NOT NULL,
|
|
1257
|
+
UNIQUE(workspace_id, owner, session_key)
|
|
1258
|
+
);
|
|
1259
|
+
CREATE INDEX session_listing ON sessions(workspace_id, id);
|
|
1260
|
+
CREATE TABLE session_revisions (
|
|
1261
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1262
|
+
revision INTEGER NOT NULL, info_json TEXT NOT NULL,
|
|
1263
|
+
PRIMARY KEY(workspace_id, owner, session_key, revision)
|
|
1264
|
+
);
|
|
1265
|
+
CREATE TABLE session_operations (
|
|
1266
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
1267
|
+
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1268
|
+
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
1269
|
+
);
|
|
1270
|
+
CREATE TABLE session_messages (
|
|
1271
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1272
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1273
|
+
revision INTEGER NOT NULL, message_id TEXT NOT NULL, role TEXT NOT NULL, body TEXT NOT NULL, search_text TEXT NOT NULL
|
|
1274
|
+
);
|
|
1275
|
+
CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
|
|
1276
|
+
CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
|
|
1277
|
+
`;
|
|
1278
|
+
var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
|
|
1279
|
+
|
|
1280
|
+
// apps/server/control-schema.ts
|
|
1281
|
+
var controlMigration = String.raw`
|
|
1282
|
+
CREATE TABLE device_status(workspace_id TEXT NOT NULL,member_id TEXT NOT NULL,device_id TEXT NOT NULL,seen_at TEXT NOT NULL,status_json TEXT NOT NULL,PRIMARY KEY(workspace_id,member_id));
|
|
1283
|
+
`;
|
|
1284
|
+
|
|
1285
|
+
// apps/server/device-schema.ts
|
|
1286
|
+
var deviceMigration = String.raw`
|
|
1287
|
+
CREATE TABLE device_enrollments(workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, operation_id TEXT NOT NULL, fingerprint TEXT NOT NULL, PRIMARY KEY(workspace_id,member_id,operation_id));
|
|
1288
|
+
`;
|
|
1289
|
+
|
|
1290
|
+
// apps/server/project-schema.ts
|
|
1291
|
+
var projectMigration = String.raw`
|
|
1292
|
+
ALTER TABLE members ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
|
1293
|
+
ALTER TABLE members ADD COLUMN role TEXT NOT NULL DEFAULT 'member';
|
|
1294
|
+
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
1295
|
+
ALTER TABLE members ADD COLUMN member_revision INTEGER NOT NULL DEFAULT 1;
|
|
1296
|
+
UPDATE members SET enabled=0 WHERE token_hash IS NULL;
|
|
1297
|
+
CREATE TABLE project_settings(id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL, setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL);
|
|
1298
|
+
CREATE TABLE web_sessions(token_hash TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, expires_at INTEGER NOT NULL);
|
|
1299
|
+
`;
|
|
1300
|
+
var multiProjectMigration = String.raw`
|
|
1301
|
+
DROP TABLE project_settings;
|
|
1302
|
+
DROP TABLE web_sessions;
|
|
1303
|
+
ALTER TABLE members DROP COLUMN display_name;
|
|
1304
|
+
CREATE TABLE users (
|
|
1305
|
+
id TEXT PRIMARY KEY, display_name TEXT NOT NULL DEFAULT '',
|
|
1306
|
+
site_role TEXT NOT NULL DEFAULT 'member', enabled INTEGER NOT NULL DEFAULT 1,
|
|
1307
|
+
revision INTEGER NOT NULL DEFAULT 1
|
|
1308
|
+
);
|
|
1309
|
+
CREATE TABLE projects (
|
|
1310
|
+
id TEXT PRIMARY KEY, config_json TEXT NOT NULL,
|
|
1311
|
+
operation_id TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL
|
|
1312
|
+
);
|
|
1313
|
+
CREATE TABLE site_settings (
|
|
1314
|
+
id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
|
|
1315
|
+
setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
|
|
1316
|
+
);
|
|
1317
|
+
CREATE TABLE web_sessions (
|
|
1318
|
+
token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
|
|
1319
|
+
);
|
|
1320
|
+
DROP TABLE ai_profiles;
|
|
1321
|
+
CREATE TABLE ai_profiles (
|
|
1322
|
+
id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
|
|
1323
|
+
);
|
|
1324
|
+
`;
|
|
1325
|
+
var sitePolicyMigration = `
|
|
1326
|
+
UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
|
|
1327
|
+
(SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
|
|
1328
|
+
'{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
|
|
1329
|
+
)));
|
|
1330
|
+
UPDATE projects SET config_json=json_set(config_json,
|
|
1331
|
+
'$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
|
|
1332
|
+
'$.configRevision',json_extract(config_json,'$.configRevision')+1)
|
|
1333
|
+
WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
|
|
1334
|
+
`;
|
|
1335
|
+
var equalMembershipMigration = `
|
|
1336
|
+
ALTER TABLE members DROP COLUMN role;
|
|
1337
|
+
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
1338
|
+
`;
|
|
1339
|
+
|
|
1340
|
+
// apps/server/file-schema.ts
|
|
1341
|
+
var fileMigration = String.raw`
|
|
1342
|
+
ALTER TABLE members ADD COLUMN file_revision INTEGER NOT NULL DEFAULT 0;
|
|
1343
|
+
CREATE TABLE workspace_blobs (workspace_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(workspace_id, hash));
|
|
1344
|
+
CREATE TABLE file_operations (
|
|
1345
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
1346
|
+
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1347
|
+
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
1348
|
+
);
|
|
1349
|
+
CREATE TABLE file_revisions (
|
|
1350
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1351
|
+
operation_id TEXT NOT NULL, created_at TEXT NOT NULL, changes_json TEXT NOT NULL,
|
|
1352
|
+
PRIMARY KEY(workspace_id, owner, revision)
|
|
1353
|
+
);
|
|
1354
|
+
CREATE TABLE current_files (
|
|
1355
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, path_key TEXT NOT NULL,
|
|
1356
|
+
hash TEXT NOT NULL, size INTEGER NOT NULL, revision INTEGER NOT NULL,
|
|
1357
|
+
PRIMARY KEY(workspace_id, owner, path), UNIQUE(workspace_id, owner, path_key)
|
|
1358
|
+
);
|
|
1359
|
+
CREATE TABLE file_events (
|
|
1360
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1361
|
+
hash TEXT, size INTEGER,
|
|
1362
|
+
PRIMARY KEY(workspace_id, owner, path, revision)
|
|
1363
|
+
);
|
|
1364
|
+
`;
|
|
1365
|
+
|
|
1366
|
+
// apps/server/ai/task-schema.ts
|
|
1367
|
+
var taskBoardMigration = `
|
|
1368
|
+
ALTER TABLE ai_jobs ADD COLUMN runtime_json TEXT;
|
|
1369
|
+
ALTER TABLE ai_jobs ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
|
1370
|
+
CREATE TABLE task_board_members (
|
|
1371
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, job_id TEXT NOT NULL REFERENCES ai_jobs(id),
|
|
1372
|
+
PRIMARY KEY(workspace_id,owner)
|
|
1373
|
+
);
|
|
1374
|
+
CREATE TABLE task_cards (
|
|
1375
|
+
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, owner TEXT NOT NULL,
|
|
1376
|
+
job_id TEXT NOT NULL REFERENCES ai_jobs(id), fields_json TEXT NOT NULL,
|
|
1377
|
+
sources_json TEXT NOT NULL, synced_at TEXT NOT NULL, correction_json TEXT,
|
|
1378
|
+
active INTEGER NOT NULL DEFAULT 1, needs_review INTEGER NOT NULL DEFAULT 0
|
|
1379
|
+
);
|
|
1380
|
+
CREATE INDEX task_cards_scope ON task_cards(workspace_id,owner,active);
|
|
1381
|
+
`;
|
|
1382
|
+
var taskMaintenanceMigration = `
|
|
1383
|
+
ALTER TABLE ai_jobs ADD COLUMN task_context_json TEXT;
|
|
1384
|
+
ALTER TABLE task_board_members ADD COLUMN applied_job_id TEXT REFERENCES ai_jobs(id);
|
|
1385
|
+
UPDATE task_board_members SET applied_job_id=(
|
|
1386
|
+
SELECT j.id FROM ai_jobs j
|
|
1387
|
+
WHERE j.workspace_id=task_board_members.workspace_id AND j.status='succeeded'
|
|
1388
|
+
AND json_extract(j.request_json,'$.workflow')='task-bootstrap'
|
|
1389
|
+
AND json_extract(j.snapshot_json,'$.owner')=task_board_members.owner
|
|
1390
|
+
ORDER BY j.rowid DESC LIMIT 1
|
|
1391
|
+
);
|
|
1392
|
+
`;
|
|
1393
|
+
|
|
1394
|
+
// apps/server/ai/schema.ts
|
|
1395
|
+
var aiConfigMigration = `
|
|
1396
|
+
CREATE TABLE ai_profiles (
|
|
1397
|
+
workspace_id TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1398
|
+
config_json TEXT NOT NULL, secret TEXT,
|
|
1399
|
+
PRIMARY KEY(workspace_id,id)
|
|
1400
|
+
);
|
|
1401
|
+
`;
|
|
1402
|
+
var aiJobsMigration = `
|
|
1403
|
+
CREATE TABLE ai_jobs (
|
|
1404
|
+
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, requester TEXT NOT NULL,
|
|
1405
|
+
request_json TEXT NOT NULL, profile_json TEXT NOT NULL, snapshot_json TEXT NOT NULL,
|
|
1406
|
+
prompt_version TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, recovery_count INTEGER NOT NULL DEFAULT 0,
|
|
1407
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, run_token TEXT, lease_until INTEGER,
|
|
1408
|
+
result_json TEXT, error TEXT
|
|
1409
|
+
);
|
|
1410
|
+
CREATE INDEX ai_jobs_queue ON ai_jobs(status,created_at);
|
|
1411
|
+
CREATE INDEX ai_jobs_workspace ON ai_jobs(workspace_id,id);
|
|
1412
|
+
CREATE TABLE ai_operations(workspace_id TEXT NOT NULL,requester TEXT NOT NULL,operation_id TEXT NOT NULL,fingerprint TEXT NOT NULL,job_id TEXT NOT NULL REFERENCES ai_jobs(id),PRIMARY KEY(workspace_id,requester,operation_id));
|
|
1413
|
+
CREATE TABLE ai_attempts(job_id TEXT NOT NULL REFERENCES ai_jobs(id),number INTEGER NOT NULL,status TEXT NOT NULL,started_at TEXT NOT NULL,ended_at TEXT,error TEXT,usage_json TEXT,PRIMARY KEY(job_id,number));
|
|
1414
|
+
CREATE TABLE ai_events(seq INTEGER PRIMARY KEY AUTOINCREMENT,job_id TEXT NOT NULL REFERENCES ai_jobs(id),attempt INTEGER NOT NULL,kind TEXT NOT NULL,message TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
1415
|
+
CREATE INDEX ai_events_job ON ai_events(job_id,seq);
|
|
1416
|
+
`;
|
|
1417
|
+
var aiDefaultsMigration = `
|
|
1418
|
+
CREATE TABLE ai_defaults (
|
|
1419
|
+
scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
|
|
1420
|
+
);
|
|
1421
|
+
`;
|
|
1422
|
+
var aiRetryMigration = `
|
|
1423
|
+
ALTER TABLE ai_jobs ADD COLUMN auto_retry_count INTEGER NOT NULL DEFAULT 0;
|
|
1424
|
+
ALTER TABLE ai_jobs ADD COLUMN next_retry_at INTEGER;
|
|
1425
|
+
`;
|
|
1426
|
+
|
|
1427
|
+
// apps/server/store.ts
|
|
1428
|
+
function digest2(token) {
|
|
1429
|
+
return createHash5("sha256").update(token).digest("hex");
|
|
1430
|
+
}
|
|
1431
|
+
var migrations = [
|
|
1432
|
+
String.raw`
|
|
1433
|
+
CREATE TABLE members (
|
|
1434
|
+
workspace_id TEXT NOT NULL,
|
|
1435
|
+
member_id TEXT NOT NULL,
|
|
1436
|
+
token_hash TEXT UNIQUE,
|
|
1437
|
+
device_id TEXT,
|
|
1438
|
+
PRIMARY KEY (workspace_id, member_id)
|
|
1439
|
+
);
|
|
1440
|
+
`,
|
|
1441
|
+
fileMigration,
|
|
1442
|
+
sessionMigration,
|
|
1443
|
+
projectMigration,
|
|
1444
|
+
deviceMigration,
|
|
1445
|
+
controlMigration,
|
|
1446
|
+
aiConfigMigration,
|
|
1447
|
+
aiJobsMigration,
|
|
1448
|
+
multiProjectMigration,
|
|
1449
|
+
aiDefaultsMigration,
|
|
1450
|
+
sitePolicyMigration,
|
|
1451
|
+
sessionOwnerIndexMigration,
|
|
1452
|
+
equalMembershipMigration,
|
|
1453
|
+
taskBoardMigration,
|
|
1454
|
+
taskMaintenanceMigration,
|
|
1455
|
+
sharedFilesMigration,
|
|
1456
|
+
aiRetryMigration
|
|
1457
|
+
];
|
|
1458
|
+
var HubStore = class {
|
|
1459
|
+
constructor(directory) {
|
|
1460
|
+
this.directory = directory;
|
|
1461
|
+
const path = join3(directory, "hub.sqlite3");
|
|
1462
|
+
if (existsSync(path)) {
|
|
1463
|
+
const existing = new DatabaseSync2(path, { readOnly: true });
|
|
1464
|
+
try {
|
|
1465
|
+
const version2 = Number(existing.prepare("PRAGMA user_version").get().user_version);
|
|
1466
|
+
if (version2 > 0 && version2 < 9)
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
"Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
|
|
1469
|
+
);
|
|
1470
|
+
} finally {
|
|
1471
|
+
existing.close();
|
|
1733
1472
|
}
|
|
1734
|
-
|
|
1473
|
+
}
|
|
1474
|
+
this.db = openDatabase(path, migrations);
|
|
1475
|
+
}
|
|
1476
|
+
directory;
|
|
1477
|
+
db;
|
|
1478
|
+
close() {
|
|
1479
|
+
this.db.close();
|
|
1480
|
+
}
|
|
1481
|
+
issueMember(workspaceId, memberId, reset = false) {
|
|
1482
|
+
NameSchema.parse(workspaceId);
|
|
1483
|
+
NameSchema.parse(memberId);
|
|
1484
|
+
const token = `tmy_${randomBytes3(32).toString("hex")}`;
|
|
1485
|
+
transaction(this.db, () => {
|
|
1486
|
+
this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
|
|
1487
|
+
const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
|
|
1488
|
+
if (exists && !reset)
|
|
1489
|
+
throw new HubError(
|
|
1490
|
+
409,
|
|
1491
|
+
"MEMBER_EXISTS",
|
|
1492
|
+
"Member exists; use member reset to replace its credential and device binding."
|
|
1493
|
+
);
|
|
1494
|
+
if (!exists && reset) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
1495
|
+
this.db.prepare(
|
|
1496
|
+
`INSERT INTO members (workspace_id, member_id, token_hash) VALUES (?, ?, ?)
|
|
1497
|
+
ON CONFLICT(workspace_id, member_id) DO UPDATE SET token_hash=excluded.token_hash, device_id=NULL, enabled=1, member_revision=member_revision+1`
|
|
1498
|
+
).run(workspaceId, memberId, digest2(token));
|
|
1499
|
+
});
|
|
1500
|
+
return token;
|
|
1501
|
+
}
|
|
1502
|
+
revokeMember(workspaceId, memberId) {
|
|
1503
|
+
const result = this.db.prepare(
|
|
1504
|
+
"UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
|
|
1505
|
+
).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
|
|
1506
|
+
if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
1507
|
+
this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
|
|
1508
|
+
}
|
|
1509
|
+
identity(authorization) {
|
|
1510
|
+
const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
|
|
1511
|
+
const row = match && this.db.prepare(
|
|
1512
|
+
"SELECT m.workspace_id,m.member_id,m.device_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.token_hash=? AND m.enabled=1 AND u.enabled=1"
|
|
1513
|
+
).get(digest2(match[1]));
|
|
1514
|
+
if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
|
|
1515
|
+
return {
|
|
1516
|
+
workspaceId: String(row.workspace_id),
|
|
1517
|
+
memberId: String(row.member_id),
|
|
1518
|
+
deviceId: row.device_id
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
bind(authorization, deviceId, projectId2) {
|
|
1522
|
+
return transaction(this.db, () => {
|
|
1523
|
+
const identity = this.identity(authorization);
|
|
1524
|
+
if (projectId2 && projectId2 !== identity.workspaceId)
|
|
1525
|
+
throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
|
|
1526
|
+
if (identity.deviceId && identity.deviceId !== deviceId)
|
|
1527
|
+
throw new HubError(
|
|
1528
|
+
409,
|
|
1529
|
+
"DEVICE_BOUND",
|
|
1530
|
+
"This member is bound to another device. An administrator must reset the binding."
|
|
1531
|
+
);
|
|
1532
|
+
this.db.prepare("UPDATE members SET device_id=? WHERE workspace_id=? AND member_id=?").run(deviceId, identity.workspaceId, identity.memberId);
|
|
1533
|
+
return BindingSchema.parse({ ...identity, deviceId });
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
authorize(authorization, deviceId) {
|
|
1537
|
+
const identity = this.identity(authorization);
|
|
1538
|
+
if (!identity.deviceId || identity.deviceId !== deviceId)
|
|
1539
|
+
throw new HubError(
|
|
1540
|
+
403,
|
|
1541
|
+
"DEVICE_MISMATCH",
|
|
1542
|
+
"Connect this device before using workspace APIs."
|
|
1543
|
+
);
|
|
1544
|
+
return BindingSchema.parse(identity);
|
|
1545
|
+
}
|
|
1546
|
+
members(identity) {
|
|
1547
|
+
return this.db.prepare("SELECT member_id FROM members WHERE workspace_id=? ORDER BY member_id").all(identity.workspaceId).map((row) => String(row.member_id));
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
// apps/server/ai/jobs.ts
|
|
1552
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
1553
|
+
|
|
1554
|
+
// packages/protocol/ai.ts
|
|
1555
|
+
import { z as z6 } from "zod";
|
|
1556
|
+
|
|
1557
|
+
// packages/protocol/tasks.ts
|
|
1558
|
+
import { z as z5 } from "zod";
|
|
1559
|
+
var TaskStatusSchema = z5.enum(["planned", "in_progress", "in_review", "completed"]);
|
|
1560
|
+
var TaskFieldsSchema = z5.object({
|
|
1561
|
+
title: z5.string().trim(),
|
|
1562
|
+
goal: z5.string().trim().nullable(),
|
|
1563
|
+
status: TaskStatusSchema.nullable(),
|
|
1564
|
+
progress: z5.string().trim(),
|
|
1565
|
+
needsConfirmation: z5.boolean().default(false)
|
|
1566
|
+
}).strict();
|
|
1567
|
+
var TaskSourceSchema = z5.object({
|
|
1568
|
+
path: FilePathSchema,
|
|
1569
|
+
sha256: HashSchema,
|
|
1570
|
+
revision: RevisionSchema,
|
|
1571
|
+
syncedAt: z5.string()
|
|
1572
|
+
});
|
|
1573
|
+
var TaskCardSchema = TaskFieldsSchema.extend({
|
|
1574
|
+
id: z5.uuid(),
|
|
1575
|
+
owner: NameSchema,
|
|
1576
|
+
jobId: z5.uuid(),
|
|
1577
|
+
sources: z5.array(TaskSourceSchema),
|
|
1578
|
+
syncedAt: z5.string().nullable(),
|
|
1579
|
+
corrected: z5.boolean(),
|
|
1580
|
+
needsReview: z5.boolean()
|
|
1581
|
+
});
|
|
1582
|
+
var TaskCorrectionSchema = z5.object({
|
|
1583
|
+
title: TaskFieldsSchema.shape.title.optional(),
|
|
1584
|
+
status: TaskStatusSchema.nullable().optional(),
|
|
1585
|
+
goal: TaskFieldsSchema.shape.goal.optional(),
|
|
1586
|
+
progress: TaskFieldsSchema.shape.progress.optional(),
|
|
1587
|
+
needsConfirmation: z5.boolean().optional()
|
|
1588
|
+
}).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
|
|
1589
|
+
var TaskBoardSchema = z5.object({
|
|
1590
|
+
tasks: z5.array(TaskCardSchema),
|
|
1591
|
+
members: z5.array(
|
|
1592
|
+
z5.object({
|
|
1593
|
+
owner: NameSchema,
|
|
1594
|
+
jobId: z5.uuid().nullable(),
|
|
1595
|
+
status: z5.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
|
|
1596
|
+
error: z5.string().nullable(),
|
|
1597
|
+
sourceChanged: z5.boolean(),
|
|
1598
|
+
uncertainties: z5.array(z5.string())
|
|
1599
|
+
})
|
|
1600
|
+
),
|
|
1601
|
+
modelAvailable: z5.boolean()
|
|
1602
|
+
});
|
|
1603
|
+
var TaskBootstrapRequestSchema = z5.object({ operationId: z5.uuid(), owner: NameSchema }).strict();
|
|
1604
|
+
var TaskBootstrapResponseSchema = z5.object({
|
|
1605
|
+
jobId: z5.uuid().nullable(),
|
|
1606
|
+
empty: z5.boolean()
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// packages/protocol/ai.ts
|
|
1610
|
+
var AiProfileSchema = z6.object({
|
|
1611
|
+
id: NameSchema,
|
|
1612
|
+
name: z6.string().trim().min(1).max(120),
|
|
1613
|
+
protocol: z6.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
|
|
1614
|
+
baseUrl: ServerUrlSchema,
|
|
1615
|
+
model: z6.string().trim().min(1).max(200),
|
|
1616
|
+
enabled: z6.boolean().default(true)
|
|
1617
|
+
}).strict();
|
|
1618
|
+
var AiPublicProfileSchema = AiProfileSchema.safeExtend({
|
|
1619
|
+
revision: z6.number().int(),
|
|
1620
|
+
keyConfigured: z6.boolean()
|
|
1621
|
+
});
|
|
1622
|
+
var AiProfileUpdateSchema = z6.object({
|
|
1623
|
+
expectedRevision: z6.number().int().nonnegative(),
|
|
1624
|
+
profile: AiProfileSchema,
|
|
1625
|
+
apiKey: z6.string().min(1).max(4096).nullable().optional()
|
|
1626
|
+
}).strict();
|
|
1627
|
+
var AiProfilesSchema = z6.object({ profiles: z6.array(AiPublicProfileSchema) });
|
|
1628
|
+
var AiDefaultUpdateSchema = z6.object({
|
|
1629
|
+
expectedRevision: z6.number().int().nonnegative(),
|
|
1630
|
+
profileId: NameSchema.nullable()
|
|
1631
|
+
}).strict();
|
|
1632
|
+
var AiDefaultSchema = z6.object({
|
|
1633
|
+
revision: z6.number().int().nonnegative(),
|
|
1634
|
+
profileId: NameSchema.nullable(),
|
|
1635
|
+
effectiveProfileId: NameSchema.nullable(),
|
|
1636
|
+
source: z6.enum(["site", "project", "none"]),
|
|
1637
|
+
available: z6.boolean()
|
|
1638
|
+
});
|
|
1639
|
+
var AiProbeSchema = z6.object({ mode: z6.enum(["text", "tools", "json"]) }).strict();
|
|
1640
|
+
var AiProbeResultSchema = z6.object({
|
|
1641
|
+
ok: z6.literal(true),
|
|
1642
|
+
mode: AiProbeSchema.shape.mode,
|
|
1643
|
+
streamed: z6.boolean(),
|
|
1644
|
+
inputTokens: z6.number(),
|
|
1645
|
+
outputTokens: z6.number()
|
|
1646
|
+
});
|
|
1647
|
+
var AiSelectionSchema = z6.object({
|
|
1648
|
+
owner: NameSchema,
|
|
1649
|
+
kind: z6.enum(["file", "directory"]),
|
|
1650
|
+
path: z6.union([z6.literal(""), FilePathSchema]),
|
|
1651
|
+
revision: z6.number().int().nonnegative().optional()
|
|
1652
|
+
}).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
|
|
1653
|
+
var AiJobCreateSchema = z6.object({
|
|
1654
|
+
operationId: z6.uuid(),
|
|
1655
|
+
profileId: NameSchema.optional(),
|
|
1656
|
+
workflow: z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
|
|
1657
|
+
selection: AiSelectionSchema,
|
|
1658
|
+
question: z6.string().trim().default("")
|
|
1659
|
+
}).strict();
|
|
1660
|
+
var AiCitationSchema = z6.object({
|
|
1661
|
+
path: FilePathSchema,
|
|
1662
|
+
startLine: z6.number().int().positive(),
|
|
1663
|
+
endLine: z6.number().int().positive(),
|
|
1664
|
+
sha256: HashSchema
|
|
1665
|
+
});
|
|
1666
|
+
var AiCoverageSchema = z6.object({
|
|
1667
|
+
totalFiles: z6.number().int(),
|
|
1668
|
+
// Access diagnostics only; reading a file does not establish semantic completeness.
|
|
1669
|
+
readFiles: z6.array(FilePathSchema),
|
|
1670
|
+
accessTracking: z6.literal("unavailable").optional(),
|
|
1671
|
+
limitations: z6.array(z6.string())
|
|
1672
|
+
});
|
|
1673
|
+
var AiResultSchema = z6.object({
|
|
1674
|
+
summary: z6.string(),
|
|
1675
|
+
findings: z6.array(z6.string()),
|
|
1676
|
+
uncertainties: z6.array(z6.string()).default([]),
|
|
1677
|
+
citations: z6.array(AiCitationSchema),
|
|
1678
|
+
coverage: AiCoverageSchema
|
|
1679
|
+
});
|
|
1680
|
+
var TaskAnalysisResultSchema = z6.object({
|
|
1681
|
+
kind: z6.literal("task-bootstrap"),
|
|
1682
|
+
// Absent for historical full-list results; new workflows publish explicit patches.
|
|
1683
|
+
updateMode: z6.literal("patch").optional(),
|
|
1684
|
+
archive: z6.array(z6.uuid()).optional(),
|
|
1685
|
+
tasks: z6.array(
|
|
1686
|
+
TaskFieldsSchema.extend({
|
|
1687
|
+
taskId: z6.uuid().nullable().optional(),
|
|
1688
|
+
sources: z6.array(TaskSourceSchema)
|
|
1689
|
+
})
|
|
1690
|
+
),
|
|
1691
|
+
// Read historical result notes without asking new task workflows to generate them.
|
|
1692
|
+
uncertainties: z6.array(z6.string()).optional(),
|
|
1693
|
+
coverage: AiCoverageSchema
|
|
1694
|
+
});
|
|
1695
|
+
var AiWorkflowResultSchema = z6.union([AiResultSchema, TaskAnalysisResultSchema]);
|
|
1696
|
+
var AiUsageSchema = z6.object({
|
|
1697
|
+
inputTokens: z6.number().nonnegative(),
|
|
1698
|
+
outputTokens: z6.number().nonnegative()
|
|
1699
|
+
});
|
|
1700
|
+
var AiJobSchema = z6.object({
|
|
1701
|
+
id: z6.uuid(),
|
|
1702
|
+
requester: NameSchema,
|
|
1703
|
+
workflow: AiJobCreateSchema.shape.workflow,
|
|
1704
|
+
question: z6.string(),
|
|
1705
|
+
profileId: NameSchema,
|
|
1706
|
+
model: z6.string(),
|
|
1707
|
+
profileRevision: z6.number().int(),
|
|
1708
|
+
promptVersion: z6.string(),
|
|
1709
|
+
origin: z6.enum(["manual", "automatic"]).default("manual"),
|
|
1710
|
+
status: z6.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
|
|
1711
|
+
createdAt: z6.string(),
|
|
1712
|
+
updatedAt: z6.string(),
|
|
1713
|
+
attemptCount: z6.number().int(),
|
|
1714
|
+
error: z6.string().nullable(),
|
|
1715
|
+
input: z6.object({
|
|
1716
|
+
owner: NameSchema,
|
|
1717
|
+
kind: z6.enum(["file", "directory"]),
|
|
1718
|
+
path: z6.string(),
|
|
1719
|
+
revision: z6.number().int(),
|
|
1720
|
+
fileCount: z6.number().int(),
|
|
1721
|
+
fingerprint: HashSchema
|
|
1722
|
+
}),
|
|
1723
|
+
result: AiWorkflowResultSchema.nullable(),
|
|
1724
|
+
sourceChanged: z6.boolean()
|
|
1725
|
+
});
|
|
1726
|
+
var AiJobsSchema = z6.object({
|
|
1727
|
+
jobs: z6.array(AiJobSchema),
|
|
1728
|
+
nextCursor: z6.string().nullable()
|
|
1729
|
+
});
|
|
1730
|
+
var AiEventSchema = z6.object({
|
|
1731
|
+
seq: z6.number().int(),
|
|
1732
|
+
attempt: z6.number().int(),
|
|
1733
|
+
kind: z6.string(),
|
|
1734
|
+
message: z6.string(),
|
|
1735
|
+
createdAt: z6.string()
|
|
1736
|
+
});
|
|
1737
|
+
var AiEventsSchema = z6.object({
|
|
1738
|
+
events: z6.array(AiEventSchema),
|
|
1739
|
+
nextCursor: z6.number().int()
|
|
1740
|
+
});
|
|
1741
|
+
var AiAttemptSchema = z6.object({
|
|
1742
|
+
number: z6.number().int(),
|
|
1743
|
+
status: z6.string(),
|
|
1744
|
+
startedAt: z6.string(),
|
|
1745
|
+
endedAt: z6.string().nullable(),
|
|
1746
|
+
error: z6.string().nullable(),
|
|
1747
|
+
usage: AiUsageSchema.nullable()
|
|
1748
|
+
});
|
|
1749
|
+
var AiJobDetailSchema = z6.object({ job: AiJobSchema, attempts: z6.array(AiAttemptSchema) });
|
|
1750
|
+
|
|
1751
|
+
// apps/server/ai/config.ts
|
|
1752
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
|
|
1753
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1754
|
+
import { join as join4 } from "node:path";
|
|
1755
|
+
var AiConfig = class {
|
|
1756
|
+
constructor(hub) {
|
|
1757
|
+
this.hub = hub;
|
|
1758
|
+
}
|
|
1759
|
+
hub;
|
|
1760
|
+
key(create = false) {
|
|
1761
|
+
const path = join4(this.hub.directory, "ai-secret.key");
|
|
1762
|
+
if (create) {
|
|
1735
1763
|
try {
|
|
1736
|
-
|
|
1737
|
-
for (let i = 0; i < lines.length; i++)
|
|
1738
|
-
if (lines[i].toLowerCase().includes(query.toLowerCase())) {
|
|
1739
|
-
matches.push({ path: file.path, line: i + 1, excerpt: lines[i].slice(0, 400) });
|
|
1740
|
-
if (matches.length >= 30) return { matches, nextCursor: file.path, limited: true };
|
|
1741
|
-
}
|
|
1764
|
+
writeFileSync(path, randomBytes4(32), { flag: "wx", mode: 384 });
|
|
1742
1765
|
} catch (e) {
|
|
1743
|
-
if (e
|
|
1744
|
-
this.limitations.add("\u90E8\u5206\u975E\u6587\u672C\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
|
|
1745
|
-
continue;
|
|
1746
|
-
}
|
|
1747
|
-
throw e;
|
|
1766
|
+
if (e.code !== "EEXIST") throw e;
|
|
1748
1767
|
}
|
|
1749
1768
|
}
|
|
1769
|
+
const key = readFileSync(path);
|
|
1770
|
+
if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
|
|
1771
|
+
return key;
|
|
1772
|
+
}
|
|
1773
|
+
encrypt(value, id) {
|
|
1774
|
+
const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
|
|
1775
|
+
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1776
|
+
const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
1777
|
+
return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
|
|
1778
|
+
}
|
|
1779
|
+
list() {
|
|
1780
|
+
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => {
|
|
1781
|
+
const {
|
|
1782
|
+
contextWindow: _context,
|
|
1783
|
+
maxOutputTokens: _output,
|
|
1784
|
+
...profile
|
|
1785
|
+
} = JSON.parse(String(row.config_json));
|
|
1786
|
+
return {
|
|
1787
|
+
...AiProfileSchema.parse(profile),
|
|
1788
|
+
revision: Number(row.revision),
|
|
1789
|
+
keyConfigured: !!row.secret
|
|
1790
|
+
};
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
get(id) {
|
|
1794
|
+
const profile = this.list().find((p) => p.id === id);
|
|
1795
|
+
if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
|
|
1796
|
+
const { keyConfigured: _, ...result } = profile;
|
|
1797
|
+
return result;
|
|
1798
|
+
}
|
|
1799
|
+
secret(id) {
|
|
1800
|
+
const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
|
|
1801
|
+
if (!row?.secret) return "";
|
|
1802
|
+
const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
|
|
1803
|
+
const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
|
|
1804
|
+
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1805
|
+
cipher.setAuthTag(tag);
|
|
1806
|
+
return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
|
|
1807
|
+
}
|
|
1808
|
+
save(session, value) {
|
|
1809
|
+
requireSiteAdmin(session);
|
|
1810
|
+
const input = AiProfileUpdateSchema.parse(value), { profile } = input;
|
|
1811
|
+
return transaction(this.hub.db, () => {
|
|
1812
|
+
const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
|
|
1813
|
+
if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
|
|
1814
|
+
throw new HubError(
|
|
1815
|
+
409,
|
|
1816
|
+
"AI_CONFIG_CONFLICT",
|
|
1817
|
+
"Model configuration changed; reload before saving."
|
|
1818
|
+
);
|
|
1819
|
+
if (!old && this.list().length >= 20)
|
|
1820
|
+
throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
|
|
1821
|
+
const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
|
|
1822
|
+
this.hub.db.prepare(
|
|
1823
|
+
"INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
|
|
1824
|
+
).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
|
|
1825
|
+
return this.list().find((p) => p.id === profile.id);
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
// apps/server/ai/defaults.ts
|
|
1831
|
+
var AiDefaults = class {
|
|
1832
|
+
constructor(hub) {
|
|
1833
|
+
this.hub = hub;
|
|
1834
|
+
}
|
|
1835
|
+
hub;
|
|
1836
|
+
read(scope2) {
|
|
1837
|
+
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope2);
|
|
1750
1838
|
return {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
limited: false
|
|
1839
|
+
profileId: row?.profile_id ? String(row.profile_id) : null,
|
|
1840
|
+
revision: Number(row?.revision ?? 0)
|
|
1754
1841
|
};
|
|
1755
1842
|
}
|
|
1843
|
+
view(projectId2) {
|
|
1844
|
+
const site = this.read("site");
|
|
1845
|
+
const own = projectId2 ? this.read(`project:${projectId2}`) : site;
|
|
1846
|
+
const effectiveProfileId = own.profileId ?? site.profileId;
|
|
1847
|
+
return {
|
|
1848
|
+
...own,
|
|
1849
|
+
effectiveProfileId,
|
|
1850
|
+
source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
|
|
1851
|
+
available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
site() {
|
|
1855
|
+
return this.view();
|
|
1856
|
+
}
|
|
1857
|
+
project(projectId2) {
|
|
1858
|
+
return this.view(projectId2);
|
|
1859
|
+
}
|
|
1860
|
+
save(scope2, value) {
|
|
1861
|
+
const input = AiDefaultUpdateSchema.parse(value);
|
|
1862
|
+
transaction(this.hub.db, () => {
|
|
1863
|
+
if (this.read(scope2).revision !== input.expectedRevision)
|
|
1864
|
+
throw new HubError(
|
|
1865
|
+
409,
|
|
1866
|
+
"AI_CONFIG_CONFLICT",
|
|
1867
|
+
"Model default changed; reload before saving."
|
|
1868
|
+
);
|
|
1869
|
+
if (input.profileId && !new AiConfig(this.hub).get(input.profileId).enabled)
|
|
1870
|
+
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1871
|
+
this.hub.db.prepare(
|
|
1872
|
+
`INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
|
|
1873
|
+
).run(scope2, input.profileId, input.expectedRevision + 1);
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
saveSite(identity, value) {
|
|
1877
|
+
requireSiteAdmin(identity);
|
|
1878
|
+
this.save("site", value);
|
|
1879
|
+
return this.site();
|
|
1880
|
+
}
|
|
1881
|
+
saveProject(identity, value) {
|
|
1882
|
+
new ProjectRepository(this.hub).requireMember(identity);
|
|
1883
|
+
this.save(`project:${identity.workspaceId}`, value);
|
|
1884
|
+
return this.project(identity.workspaceId);
|
|
1885
|
+
}
|
|
1886
|
+
resolve(projectId2, explicit) {
|
|
1887
|
+
const id = explicit ?? this.project(projectId2).effectiveProfileId;
|
|
1888
|
+
if (!id)
|
|
1889
|
+
throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
|
|
1890
|
+
const profile = new AiConfig(this.hub).get(id);
|
|
1891
|
+
if (!profile.enabled)
|
|
1892
|
+
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1893
|
+
return profile;
|
|
1894
|
+
}
|
|
1756
1895
|
};
|
|
1757
1896
|
|
|
1758
1897
|
// apps/server/ai/runtime-resources.ts
|
|
@@ -1760,33 +1899,59 @@ import { createHash as createHash6 } from "node:crypto";
|
|
|
1760
1899
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1761
1900
|
import { resolve as resolve2 } from "node:path";
|
|
1762
1901
|
import { fileURLToPath } from "node:url";
|
|
1763
|
-
import { z as
|
|
1764
|
-
var WorkflowNameSchema =
|
|
1765
|
-
var ToolNameSchema =
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
}).strict();
|
|
1778
|
-
var
|
|
1902
|
+
import { z as z7 } from "zod";
|
|
1903
|
+
var WorkflowNameSchema = z7.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
|
|
1904
|
+
var ToolNameSchema = z7.enum([
|
|
1905
|
+
"list_files",
|
|
1906
|
+
"read_file",
|
|
1907
|
+
"search_files",
|
|
1908
|
+
"inspect_changes",
|
|
1909
|
+
"query_tasks",
|
|
1910
|
+
"read",
|
|
1911
|
+
"bash",
|
|
1912
|
+
"grep",
|
|
1913
|
+
"find",
|
|
1914
|
+
"ls"
|
|
1915
|
+
]);
|
|
1916
|
+
var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1) }).strict();
|
|
1917
|
+
var RuntimeConfigSchema = z7.object({
|
|
1918
|
+
version: z7.number().int().positive(),
|
|
1919
|
+
execution: z7.enum(["snapshot-tools", "native-sandbox"]).optional(),
|
|
1920
|
+
thinkingLevel: z7.enum(["off", "minimal", "low", "medium", "high"]),
|
|
1921
|
+
tools: z7.array(ToolNameSchema).refine((v) => new Set(v).size === v.length),
|
|
1922
|
+
maxRounds: z7.number().int().min(1).max(384),
|
|
1923
|
+
timeoutMs: z7.number().int().min(1e3).max(21e5),
|
|
1924
|
+
requestTimeoutMs: z7.number().int().min(1e3).max(3e5),
|
|
1925
|
+
maxReadBytes: z7.number().int().positive().max(1024 * 1024 * 1024).optional(),
|
|
1926
|
+
settleSeconds: z7.number().int().min(0).max(3600).optional()
|
|
1927
|
+
}).strict().refine((config) => {
|
|
1928
|
+
const allowed = config.execution === "native-sandbox" ? ["read", "bash", "grep", "find", "ls", "query_tasks"] : ["list_files", "read_file", "search_files", "inspect_changes", "query_tasks"];
|
|
1929
|
+
return config.tools.every((tool) => allowed.includes(tool));
|
|
1930
|
+
}, "Native file tools require the isolated runtime.");
|
|
1931
|
+
var WorkflowRuntimeSchema = z7.object({
|
|
1779
1932
|
workflow: WorkflowNameSchema,
|
|
1780
|
-
system:
|
|
1781
|
-
prompt:
|
|
1933
|
+
system: z7.string().min(1),
|
|
1934
|
+
prompt: z7.string().min(1),
|
|
1782
1935
|
config: RuntimeConfigSchema,
|
|
1783
|
-
toolText:
|
|
1936
|
+
toolText: z7.object({
|
|
1784
1937
|
list_files: ToolTextSchema,
|
|
1785
1938
|
read_file: ToolTextSchema,
|
|
1786
|
-
search_files: ToolTextSchema
|
|
1939
|
+
search_files: ToolTextSchema,
|
|
1940
|
+
inspect_changes: ToolTextSchema.optional(),
|
|
1941
|
+
query_tasks: ToolTextSchema.optional()
|
|
1787
1942
|
}).strict(),
|
|
1788
|
-
fingerprint:
|
|
1943
|
+
fingerprint: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
1789
1944
|
}).strict();
|
|
1945
|
+
function withoutLegacyToolLimit(value) {
|
|
1946
|
+
const { maxToolCalls: _removed, ...config } = value;
|
|
1947
|
+
return config;
|
|
1948
|
+
}
|
|
1949
|
+
function parseWorkflowRuntime(value) {
|
|
1950
|
+
return WorkflowRuntimeSchema.parse({
|
|
1951
|
+
...value,
|
|
1952
|
+
config: withoutLegacyToolLimit(value.config)
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1790
1955
|
function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
|
|
1791
1956
|
try {
|
|
1792
1957
|
WorkflowNameSchema.parse(workflow);
|
|
@@ -1796,15 +1961,21 @@ function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
|
|
|
1796
1961
|
workflow,
|
|
1797
1962
|
system: read("shared/system.md"),
|
|
1798
1963
|
prompt: read(`workflows/${workflow}/prompt.md`),
|
|
1799
|
-
config: RuntimeConfigSchema.parse(
|
|
1964
|
+
config: RuntimeConfigSchema.parse(
|
|
1965
|
+
withoutLegacyToolLimit(JSON.parse(read(`workflows/${workflow}/config.json`)))
|
|
1966
|
+
),
|
|
1800
1967
|
toolText: JSON.parse(read("shared/tools.json"))
|
|
1801
1968
|
};
|
|
1802
|
-
|
|
1969
|
+
const native = resource.config.execution === "native-sandbox";
|
|
1970
|
+
if (workflow !== "file-summary" && !resource.config.tools.includes(native ? "read" : "read_file"))
|
|
1803
1971
|
throw new Error("Reader required");
|
|
1804
1972
|
if (workflow === "file-summary" && resource.config.tools.length)
|
|
1805
1973
|
throw new Error("Direct workflow has no tools");
|
|
1806
1974
|
if (workflow === "task-bootstrap" && resource.config.settleSeconds === void 0)
|
|
1807
1975
|
throw new Error("Settle interval required");
|
|
1976
|
+
for (const tool of resource.config.tools)
|
|
1977
|
+
if (!["read", "bash", "grep", "find", "ls"].includes(tool) && !resource.toolText[tool])
|
|
1978
|
+
throw new Error("Missing tool description");
|
|
1808
1979
|
return WorkflowRuntimeSchema.parse({
|
|
1809
1980
|
...resource,
|
|
1810
1981
|
fingerprint: createHash6("sha256").update(JSON.stringify(resource)).digest("hex")
|
|
@@ -1823,7 +1994,7 @@ function workflowPrompt(runtime, schema) {
|
|
|
1823
1994
|
${runtime.prompt}
|
|
1824
1995
|
|
|
1825
1996
|
JSON Schema:
|
|
1826
|
-
${JSON.stringify(
|
|
1997
|
+
${JSON.stringify(z7.toJSONSchema(schema))}`;
|
|
1827
1998
|
}
|
|
1828
1999
|
|
|
1829
2000
|
// apps/server/ai/access.ts
|
|
@@ -1853,42 +2024,55 @@ function applyTaskResult(hub, job, result) {
|
|
|
1853
2024
|
const current = hub.db.prepare("SELECT job_id FROM task_board_members WHERE workspace_id=? AND owner=?").get(job.workspace, job.snapshot.owner);
|
|
1854
2025
|
if (current?.job_id !== job.id)
|
|
1855
2026
|
throw new HubError(409, "AI_TASK_SUPERSEDED", "A newer workspace analysis exists.");
|
|
1856
|
-
|
|
1857
|
-
if (Number(head?.file_revision) !== job.snapshot.revision)
|
|
2027
|
+
if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
|
|
1858
2028
|
throw new HubError(
|
|
1859
2029
|
409,
|
|
1860
2030
|
"AI_SOURCE_CHANGED",
|
|
1861
2031
|
"Workspace changed during analysis; analyse the new revision."
|
|
1862
2032
|
);
|
|
1863
|
-
const
|
|
2033
|
+
const patch = result.updateMode === "patch";
|
|
2034
|
+
const previous = hub.db.prepare(
|
|
2035
|
+
"SELECT * FROM task_cards WHERE workspace_id=? AND owner=?" + (patch ? "" : " AND active=1")
|
|
2036
|
+
).all(job.workspace, job.snapshot.owner);
|
|
2037
|
+
const previousById = new Map(previous.map((row) => [String(row.id), row]));
|
|
2038
|
+
const frozenIds = new Set(job.taskContext?.tasks.map((task) => task.taskId));
|
|
1864
2039
|
const used = /* @__PURE__ */ new Set();
|
|
1865
2040
|
const explicit = new Set(result.tasks.flatMap((task) => task.taskId ? [task.taskId] : []));
|
|
2041
|
+
const archive = new Set(result.archive ?? []);
|
|
2042
|
+
if (patch) {
|
|
2043
|
+
const ids = [...explicit, ...archive];
|
|
2044
|
+
if (explicit.size !== result.tasks.filter((task) => task.taskId).length || archive.size !== (result.archive ?? []).length || ids.some(
|
|
2045
|
+
(id) => explicit.has(id) && archive.has(id) || !previousById.has(id) || !frozenIds.has(id)
|
|
2046
|
+
) || result.tasks.some((task) => task.taskId === void 0))
|
|
2047
|
+
throw new HubError(
|
|
2048
|
+
422,
|
|
2049
|
+
"AI_TASK_ID_INVALID",
|
|
2050
|
+
"Task changes must reference unique identities in the frozen member context."
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
1866
2053
|
for (const task of result.tasks) {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
);
|
|
1870
|
-
let old = task.taskId ? previous.find((row) => row.id === task.taskId) : void 0;
|
|
1871
|
-
if (task.taskId && (!old || used.has(task.taskId) || !job.taskContext?.tasks.some((item) => item.taskId === task.taskId)))
|
|
2054
|
+
let old = task.taskId ? previousById.get(task.taskId) : void 0;
|
|
2055
|
+
if (task.taskId && (!old || used.has(task.taskId) || !frozenIds.has(task.taskId)))
|
|
1872
2056
|
throw new HubError(
|
|
1873
2057
|
422,
|
|
1874
2058
|
"AI_TASK_ID_INVALID",
|
|
1875
2059
|
"Task identity is outside the frozen member context."
|
|
1876
2060
|
);
|
|
1877
|
-
if (task.taskId === void 0)
|
|
1878
|
-
old =
|
|
1879
|
-
(row) => paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
|
|
2061
|
+
if (!patch && task.taskId === void 0)
|
|
2062
|
+
old = previous.find(
|
|
2063
|
+
(row) => !used.has(String(row.id)) && !explicit.has(String(row.id)) && paths(sources(row)) === paths(task.sources) && title(fields(row).title) === title(task.title)
|
|
1880
2064
|
);
|
|
1881
|
-
if (!old && task.taskId === void 0) {
|
|
2065
|
+
if (!patch && !old && task.taskId === void 0 && task.sources.length) {
|
|
1882
2066
|
const primary = task.sources[0].path;
|
|
1883
|
-
const oldGroup = previous.filter((row) => sources(row)[0]
|
|
1884
|
-
const newGroup = result.tasks.filter((item) => item.sources[0]
|
|
2067
|
+
const oldGroup = previous.filter((row) => sources(row)[0]?.path === primary);
|
|
2068
|
+
const newGroup = result.tasks.filter((item) => item.sources[0]?.path === primary);
|
|
1885
2069
|
if (oldGroup.length === 1 && newGroup.length === 1 && !used.has(String(oldGroup[0].id)) && !explicit.has(String(oldGroup[0].id)))
|
|
1886
2070
|
old = oldGroup[0];
|
|
1887
2071
|
}
|
|
1888
2072
|
const id = old ? String(old.id) : randomUUID2();
|
|
1889
2073
|
used.add(id);
|
|
1890
2074
|
const { sources: source, taskId: _taskId, ...content } = task;
|
|
1891
|
-
const syncedAt = source.map((item) => item.syncedAt).sort().at(-1);
|
|
2075
|
+
const syncedAt = source.map((item) => item.syncedAt).sort().at(-1) ?? "";
|
|
1892
2076
|
hub.db.prepare(
|
|
1893
2077
|
`INSERT INTO task_cards(id,workspace_id,owner,job_id,fields_json,sources_json,synced_at)
|
|
1894
2078
|
VALUES(?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET job_id=excluded.job_id,fields_json=excluded.fields_json,
|
|
@@ -1904,11 +2088,8 @@ function applyTaskResult(hub, job, result) {
|
|
|
1904
2088
|
);
|
|
1905
2089
|
}
|
|
1906
2090
|
for (const old of previous)
|
|
1907
|
-
if (!used.has(String(old.id))) {
|
|
1908
|
-
const
|
|
1909
|
-
(source) => job.snapshot.files.some((file) => file.path === source.path)
|
|
1910
|
-
);
|
|
1911
|
-
const retain = Boolean(old.correction_json) || result.coverage.partial && hasSource;
|
|
2091
|
+
if (patch ? archive.has(String(old.id)) : !used.has(String(old.id))) {
|
|
2092
|
+
const retain = Boolean(old.correction_json);
|
|
1912
2093
|
hub.db.prepare("UPDATE task_cards SET active=?,needs_review=? WHERE id=?").run(retain ? 1 : 0, retain ? 1 : 0, old.id);
|
|
1913
2094
|
}
|
|
1914
2095
|
hub.db.prepare("UPDATE task_board_members SET applied_job_id=? WHERE workspace_id=? AND owner=?").run(job.id, job.workspace, job.snapshot.owner);
|
|
@@ -1926,9 +2107,9 @@ var TaskStore = class {
|
|
|
1926
2107
|
owner: row.owner,
|
|
1927
2108
|
jobId: row.job_id,
|
|
1928
2109
|
sources: sources(row),
|
|
1929
|
-
syncedAt: row.synced_at,
|
|
2110
|
+
syncedAt: row.synced_at || null,
|
|
1930
2111
|
corrected: Boolean(row.correction_json),
|
|
1931
|
-
needsReview: Boolean(row.needs_review)
|
|
2112
|
+
needsReview: Boolean(row.needs_review && row.correction_json)
|
|
1932
2113
|
});
|
|
1933
2114
|
}
|
|
1934
2115
|
board(session) {
|
|
@@ -1954,9 +2135,8 @@ var TaskStore = class {
|
|
|
1954
2135
|
jobId: row.job_id ?? null,
|
|
1955
2136
|
status: row.status ?? (row.has_files ? "waiting" : "empty"),
|
|
1956
2137
|
error: row.error ?? null,
|
|
1957
|
-
sourceChanged: snapshot ?
|
|
1958
|
-
|
|
1959
|
-
uncertainties: result ? [...result.uncertainties, ...result.coverage.limitations] : []
|
|
2138
|
+
sourceChanged: snapshot ? !taskSnapshotCurrent(this.hub, session.workspaceId, snapshot) : false,
|
|
2139
|
+
uncertainties: result ? [...result.uncertainties ?? [], ...result.coverage.limitations] : []
|
|
1960
2140
|
};
|
|
1961
2141
|
}),
|
|
1962
2142
|
modelAvailable: new AiDefaults(this.hub).project(session.workspaceId).available
|
|
@@ -1977,26 +2157,34 @@ var TaskStore = class {
|
|
|
1977
2157
|
};
|
|
1978
2158
|
|
|
1979
2159
|
// apps/server/ai/task-context.ts
|
|
2160
|
+
function changedPaths(beforeFiles, afterFiles) {
|
|
2161
|
+
const before = new Map(beforeFiles.map((file) => [file.path, file.sha256]));
|
|
2162
|
+
const after = new Map(afterFiles.map((file) => [file.path, file.sha256]));
|
|
2163
|
+
return {
|
|
2164
|
+
added: afterFiles.filter((file) => !before.has(file.path)).map((file) => file.path),
|
|
2165
|
+
modified: afterFiles.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
|
|
2166
|
+
deleted: [...before.keys()].filter((path) => !after.has(path))
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
1980
2169
|
function captureTaskContext(hub, workspace, snapshot) {
|
|
1981
2170
|
const applied = hub.db.prepare(
|
|
1982
2171
|
`SELECT j.snapshot_json FROM task_board_members b JOIN ai_jobs j ON j.id=b.applied_job_id
|
|
1983
2172
|
WHERE b.workspace_id=? AND b.owner=?`
|
|
1984
2173
|
).get(workspace, snapshot.owner);
|
|
1985
2174
|
const previous = applied ? JSON.parse(String(applied.snapshot_json)) : null;
|
|
1986
|
-
const before = new Map(previous?.files.map((file) => [file.path, file.sha256]));
|
|
1987
|
-
const after = new Map(snapshot.files.map((file) => [file.path, file.sha256]));
|
|
1988
2175
|
return {
|
|
1989
2176
|
previousRevision: previous?.revision ?? null,
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
modified: snapshot.files.filter((file) => before.has(file.path) && before.get(file.path) !== file.sha256).map((file) => file.path),
|
|
1993
|
-
deleted: [...before.keys()].filter((path) => !after.has(path))
|
|
1994
|
-
},
|
|
2177
|
+
baseline: previous,
|
|
2178
|
+
changes: changedPaths(previous?.files ?? [], snapshot.files),
|
|
1995
2179
|
tasks: hub.db.prepare(
|
|
1996
|
-
"SELECT id,fields_json,sources_json FROM task_cards WHERE workspace_id=? AND owner=?
|
|
2180
|
+
"SELECT id,fields_json,correction_json,sources_json,active FROM task_cards WHERE workspace_id=? AND owner=? ORDER BY id"
|
|
1997
2181
|
).all(workspace, snapshot.owner).map((row) => ({
|
|
1998
|
-
...
|
|
2182
|
+
...TaskFieldsSchema.parse({
|
|
2183
|
+
...JSON.parse(String(row.fields_json)),
|
|
2184
|
+
...row.correction_json ? JSON.parse(String(row.correction_json)) : {}
|
|
2185
|
+
}),
|
|
1999
2186
|
taskId: String(row.id),
|
|
2187
|
+
active: Boolean(row.active),
|
|
2000
2188
|
sources: JSON.parse(String(row.sources_json))
|
|
2001
2189
|
}))
|
|
2002
2190
|
};
|
|
@@ -2004,6 +2192,7 @@ function captureTaskContext(hub, workspace, snapshot) {
|
|
|
2004
2192
|
|
|
2005
2193
|
// apps/server/ai/jobs.ts
|
|
2006
2194
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2195
|
+
var retryDelays = [5e3, 15e3, 45e3];
|
|
2007
2196
|
var AiJobs = class {
|
|
2008
2197
|
constructor(hub) {
|
|
2009
2198
|
this.hub = hub;
|
|
@@ -2023,7 +2212,10 @@ var AiJobs = class {
|
|
|
2023
2212
|
this.hub,
|
|
2024
2213
|
{ workspaceId: String(row.workspace_id), memberId: String(row.requester) },
|
|
2025
2214
|
{ owner: snapshot.owner, kind: snapshot.kind, path: snapshot.path },
|
|
2026
|
-
|
|
2215
|
+
{
|
|
2216
|
+
allowEmpty: request.workflow === "task-bootstrap",
|
|
2217
|
+
projectReferences: request.workflow === "task-bootstrap"
|
|
2218
|
+
}
|
|
2027
2219
|
).fingerprint !== snapshot.fingerprint;
|
|
2028
2220
|
} catch {
|
|
2029
2221
|
sourceChanged = true;
|
|
@@ -2144,18 +2336,21 @@ var AiJobs = class {
|
|
|
2144
2336
|
"Task bootstrap uses the current complete member workspace without an additional question."
|
|
2145
2337
|
);
|
|
2146
2338
|
const id = this.operation(session, input.operationId, { action: "create", ...input }, () => {
|
|
2147
|
-
const snapshot = captureSnapshot(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
input.workflow === "task-bootstrap"
|
|
2152
|
-
);
|
|
2339
|
+
const snapshot = captureSnapshot(this.hub, session, input.selection, {
|
|
2340
|
+
allowEmpty: input.workflow === "task-bootstrap",
|
|
2341
|
+
projectReferences: input.workflow === "task-bootstrap"
|
|
2342
|
+
});
|
|
2153
2343
|
if (input.workflow === "task-bootstrap") {
|
|
2154
2344
|
if (origin === "automatic") {
|
|
2155
2345
|
const published = this.hub.db.prepare(
|
|
2156
|
-
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
2157
|
-
).get(
|
|
2158
|
-
|
|
2346
|
+
"SELECT MAX(created_at) created_at FROM file_revisions WHERE workspace_id=? AND ((owner=? AND revision=?) OR (owner='_project' AND revision=?))"
|
|
2347
|
+
).get(
|
|
2348
|
+
session.workspaceId,
|
|
2349
|
+
snapshot.owner,
|
|
2350
|
+
snapshot.revision,
|
|
2351
|
+
snapshot.project?.revision ?? 0
|
|
2352
|
+
);
|
|
2353
|
+
if (!published?.created_at || (scheduledAt ?? Date.now()) - Date.parse(String(published.created_at)) < resource.config.settleSeconds * 1e3)
|
|
2159
2354
|
throw new HubError(
|
|
2160
2355
|
409,
|
|
2161
2356
|
"AI_TASK_UNSETTLED",
|
|
@@ -2169,7 +2364,7 @@ var AiJobs = class {
|
|
|
2169
2364
|
if (latest) {
|
|
2170
2365
|
const previous = JSON.parse(String(latest.snapshot_json));
|
|
2171
2366
|
if (["queued", "running"].includes(String(latest.status))) {
|
|
2172
|
-
if (origin === "manual" && previous.revision === snapshot.revision)
|
|
2367
|
+
if (origin === "manual" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
|
|
2173
2368
|
return String(latest.id);
|
|
2174
2369
|
throw new HubError(
|
|
2175
2370
|
409,
|
|
@@ -2177,7 +2372,7 @@ var AiJobs = class {
|
|
|
2177
2372
|
"This member already has an analysis in progress."
|
|
2178
2373
|
);
|
|
2179
2374
|
}
|
|
2180
|
-
if (origin === "automatic" && previous.revision === snapshot.revision)
|
|
2375
|
+
if (origin === "automatic" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
|
|
2181
2376
|
throw new HubError(
|
|
2182
2377
|
409,
|
|
2183
2378
|
"AI_TASK_CURRENT",
|
|
@@ -2240,7 +2435,7 @@ var AiJobs = class {
|
|
|
2240
2435
|
this.own(session, row);
|
|
2241
2436
|
if (["queued", "running"].includes(String(row.status))) {
|
|
2242
2437
|
this.hub.db.prepare(
|
|
2243
|
-
"UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL WHERE id=?"
|
|
2438
|
+
"UPDATE ai_jobs SET status='cancelled',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
|
|
2244
2439
|
).run(now(), id);
|
|
2245
2440
|
this.hub.db.prepare(
|
|
2246
2441
|
"UPDATE ai_attempts SET status='cancelled',ended_at=? WHERE job_id=? AND status='running'"
|
|
@@ -2260,20 +2455,49 @@ var AiJobs = class {
|
|
|
2260
2455
|
if (!["failed", "cancelled"].includes(String(row.status)))
|
|
2261
2456
|
throw new HubError(409, "AI_TASK_STATE", "Only failed or cancelled tasks can be retried.");
|
|
2262
2457
|
this.hub.db.prepare(
|
|
2263
|
-
"UPDATE ai_jobs SET status='queued',updated_at=?,error=NULL,result_json=NULL,recovery_count=0 WHERE id=?"
|
|
2458
|
+
"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=?"
|
|
2264
2459
|
).run(now(), id);
|
|
2265
2460
|
this.event(id, Number(row.attempt_count), "queued", "\u4F7F\u7528\u539F\u8F93\u5165\u5FEB\u7167\u91CD\u8BD5\u3002");
|
|
2266
2461
|
return id;
|
|
2267
2462
|
});
|
|
2268
2463
|
return this.get(session.workspaceId, id);
|
|
2269
2464
|
}
|
|
2465
|
+
cancelStaleTaskJobs() {
|
|
2466
|
+
return transaction(this.hub.db, () => {
|
|
2467
|
+
const rows = this.hub.db.prepare(
|
|
2468
|
+
"SELECT * FROM ai_jobs WHERE status IN ('queued','running') AND json_extract(request_json,'$.workflow')='task-bootstrap'"
|
|
2469
|
+
).all();
|
|
2470
|
+
for (const row of rows) {
|
|
2471
|
+
if (taskSnapshotCurrent(
|
|
2472
|
+
this.hub,
|
|
2473
|
+
String(row.workspace_id),
|
|
2474
|
+
JSON.parse(String(row.snapshot_json))
|
|
2475
|
+
))
|
|
2476
|
+
continue;
|
|
2477
|
+
this.hub.db.prepare(
|
|
2478
|
+
"UPDATE ai_jobs SET status='cancelled',error='AI_SOURCE_CHANGED',updated_at=?,run_token=NULL,lease_until=NULL,next_retry_at=NULL WHERE id=?"
|
|
2479
|
+
).run(now(), row.id);
|
|
2480
|
+
this.hub.db.prepare(
|
|
2481
|
+
"UPDATE ai_attempts SET status='cancelled',ended_at=?,error='AI_SOURCE_CHANGED' WHERE job_id=? AND status='running'"
|
|
2482
|
+
).run(now(), row.id);
|
|
2483
|
+
this.event(
|
|
2484
|
+
String(row.id),
|
|
2485
|
+
Number(row.attempt_count),
|
|
2486
|
+
"cancelled",
|
|
2487
|
+
"\u8D44\u6599\u5DF2\u66F4\u65B0\uFF0C\u53D6\u6D88\u65E7\u7248\u672C\u5206\u6790\uFF0C\u7B49\u5F85\u65B0\u7248\u672C\u6574\u7406\u3002"
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2270
2492
|
claim(at = Date.now()) {
|
|
2493
|
+
this.cancelStaleTaskJobs();
|
|
2271
2494
|
return transaction(this.hub.db, () => {
|
|
2272
2495
|
for (const old of this.hub.db.prepare("SELECT * FROM ai_jobs WHERE status='running' AND lease_until<=?").all(at)) {
|
|
2273
|
-
const
|
|
2496
|
+
const retry = Number(old.auto_retry_count) < retryDelays.length;
|
|
2497
|
+
const status = retry ? "queued" : "failed";
|
|
2274
2498
|
this.hub.db.prepare(
|
|
2275
|
-
"UPDATE ai_jobs SET status=?,run_token=NULL,lease_until=NULL,error=?,updated_at=?,recovery_count=recovery_count+1 WHERE id=?"
|
|
2276
|
-
).run(status, "AI_WORKER_INTERRUPTED", now(), old.id);
|
|
2499
|
+
"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=?"
|
|
2500
|
+
).run(status, "AI_WORKER_INTERRUPTED", now(), retry ? 1 : 0, old.id);
|
|
2277
2501
|
this.hub.db.prepare(
|
|
2278
2502
|
"UPDATE ai_attempts SET status='interrupted',ended_at=?,error='AI_WORKER_INTERRUPTED' WHERE job_id=? AND status='running'"
|
|
2279
2503
|
).run(now(), old.id);
|
|
@@ -2286,11 +2510,13 @@ var AiJobs = class {
|
|
|
2286
2510
|
}
|
|
2287
2511
|
if (this.hub.db.prepare("SELECT 1 FROM ai_jobs WHERE status='running'").get())
|
|
2288
2512
|
return void 0;
|
|
2289
|
-
const row = this.hub.db.prepare(
|
|
2513
|
+
const row = this.hub.db.prepare(
|
|
2514
|
+
"SELECT * FROM ai_jobs WHERE status='queued' AND (next_retry_at IS NULL OR next_retry_at<=?) ORDER BY created_at,id LIMIT 1"
|
|
2515
|
+
).get(at);
|
|
2290
2516
|
if (!row) return void 0;
|
|
2291
2517
|
const token = randomUUID3(), attempt = Number(row.attempt_count) + 1, date = now();
|
|
2292
2518
|
this.hub.db.prepare(
|
|
2293
|
-
"UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL WHERE id=?"
|
|
2519
|
+
"UPDATE ai_jobs SET status='running',attempt_count=?,run_token=?,lease_until=?,updated_at=?,error=NULL,next_retry_at=NULL WHERE id=?"
|
|
2294
2520
|
).run(attempt, token, at + 3e4, date, row.id);
|
|
2295
2521
|
this.hub.db.prepare("INSERT INTO ai_attempts(job_id,number,status,started_at) VALUES(?,?,?,?)").run(row.id, attempt, "running", date);
|
|
2296
2522
|
this.event(String(row.id), attempt, "running", "\u5F00\u59CB\u5206\u6790\u3002");
|
|
@@ -2304,12 +2530,16 @@ var AiJobs = class {
|
|
|
2304
2530
|
profile: JSON.parse(String(row.profile_json)),
|
|
2305
2531
|
snapshot: JSON.parse(String(row.snapshot_json)),
|
|
2306
2532
|
promptVersion: String(row.prompt_version),
|
|
2307
|
-
runtime: row.runtime_json ?
|
|
2533
|
+
runtime: row.runtime_json ? parseWorkflowRuntime(JSON.parse(String(row.runtime_json))) : null,
|
|
2308
2534
|
taskContext: row.task_context_json ? JSON.parse(String(row.task_context_json)) : null
|
|
2309
2535
|
};
|
|
2310
2536
|
});
|
|
2311
2537
|
}
|
|
2312
2538
|
heartbeat(job) {
|
|
2539
|
+
if (job.request.workflow === "task-bootstrap" && !taskSnapshotCurrent(this.hub, job.workspace, job.snapshot)) {
|
|
2540
|
+
this.cancelStaleTaskJobs();
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2313
2543
|
return this.hub.db.prepare(
|
|
2314
2544
|
"UPDATE ai_jobs SET lease_until=? WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2315
2545
|
).run(Date.now() + 3e4, job.id, job.token, Date.now()).changes === 1;
|
|
@@ -2330,20 +2560,40 @@ var AiJobs = class {
|
|
|
2330
2560
|
}
|
|
2331
2561
|
finish(job, result, error, usage) {
|
|
2332
2562
|
return transaction(this.hub.db, () => {
|
|
2333
|
-
const
|
|
2563
|
+
const row = this.hub.db.prepare(
|
|
2564
|
+
"SELECT auto_retry_count,runtime_json FROM ai_jobs WHERE id=? AND run_token=? AND status='running' AND lease_until>?"
|
|
2565
|
+
).get(job.id, job.token, Date.now());
|
|
2566
|
+
if (!row) return false;
|
|
2567
|
+
const obsolete = error === "AI_SOURCE_CHANGED" || error === "AI_TASK_SUPERSEDED";
|
|
2568
|
+
const retryCount = Number(row.auto_retry_count);
|
|
2569
|
+
const retry = !result && !obsolete && retryCount < retryDelays.length;
|
|
2570
|
+
const attemptStatus = result ? "succeeded" : obsolete ? "cancelled" : "failed";
|
|
2571
|
+
const status = retry ? "queued" : attemptStatus;
|
|
2334
2572
|
if (result) {
|
|
2335
2573
|
AiWorkflowResultSchema.parse(result);
|
|
2336
2574
|
const taskResult = "kind" in result;
|
|
2337
2575
|
if (job.request.workflow === "task-bootstrap" !== taskResult)
|
|
2338
2576
|
throw new HubError(422, "AI_RESULT_INVALID", "Result does not match the workflow.");
|
|
2577
|
+
if (taskResult && row.runtime_json) {
|
|
2578
|
+
const runtime = JSON.parse(String(row.runtime_json));
|
|
2579
|
+
const expectsPatch = runtime.config.version >= 7;
|
|
2580
|
+
if (expectsPatch !== (result.updateMode === "patch"))
|
|
2581
|
+
throw new HubError(
|
|
2582
|
+
422,
|
|
2583
|
+
"AI_RESULT_INVALID",
|
|
2584
|
+
"Task result does not match its frozen output contract."
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2339
2587
|
}
|
|
2340
2588
|
const changed = this.hub.db.prepare(
|
|
2341
|
-
"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>?"
|
|
2589
|
+
"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>?"
|
|
2342
2590
|
).run(
|
|
2343
2591
|
status,
|
|
2344
2592
|
result ? JSON.stringify(result) : null,
|
|
2345
2593
|
error,
|
|
2346
2594
|
now(),
|
|
2595
|
+
retry ? 1 : 0,
|
|
2596
|
+
retry ? Date.now() + retryDelays[retryCount] : null,
|
|
2347
2597
|
job.id,
|
|
2348
2598
|
job.token,
|
|
2349
2599
|
Date.now()
|
|
@@ -2352,8 +2602,27 @@ var AiJobs = class {
|
|
|
2352
2602
|
if (result && "kind" in result) applyTaskResult(this.hub, job, result);
|
|
2353
2603
|
this.hub.db.prepare(
|
|
2354
2604
|
"UPDATE ai_attempts SET status=?,ended_at=?,error=?,usage_json=? WHERE job_id=? AND number=?"
|
|
2355
|
-
).run(
|
|
2356
|
-
|
|
2605
|
+
).run(
|
|
2606
|
+
attemptStatus,
|
|
2607
|
+
now(),
|
|
2608
|
+
error,
|
|
2609
|
+
usage ? JSON.stringify(usage) : null,
|
|
2610
|
+
job.id,
|
|
2611
|
+
job.attempt
|
|
2612
|
+
);
|
|
2613
|
+
this.event(
|
|
2614
|
+
job.id,
|
|
2615
|
+
job.attempt,
|
|
2616
|
+
attemptStatus,
|
|
2617
|
+
result ? "\u5206\u6790\u5B8C\u6210\u3002" : error ?? "\u5206\u6790\u5931\u8D25\u3002"
|
|
2618
|
+
);
|
|
2619
|
+
if (retry)
|
|
2620
|
+
this.event(
|
|
2621
|
+
job.id,
|
|
2622
|
+
job.attempt,
|
|
2623
|
+
"queued",
|
|
2624
|
+
`\u5C06\u5728 ${retryDelays[retryCount] / 1e3} \u79D2\u540E\u81EA\u52A8\u91CD\u8BD5\uFF08${retryCount + 1}/3\uFF09\u3002`
|
|
2625
|
+
);
|
|
2357
2626
|
}
|
|
2358
2627
|
return changed === 1;
|
|
2359
2628
|
});
|
|
@@ -2361,23 +2630,97 @@ var AiJobs = class {
|
|
|
2361
2630
|
};
|
|
2362
2631
|
|
|
2363
2632
|
// apps/server/ai/pi-runner.ts
|
|
2364
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2365
|
-
import { tmpdir } from "node:os";
|
|
2366
|
-
import { join as
|
|
2633
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
|
|
2634
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
2635
|
+
import { join as join7 } from "node:path";
|
|
2367
2636
|
import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi-ai";
|
|
2368
2637
|
|
|
2369
|
-
// apps/server/ai/tools.ts
|
|
2638
|
+
// apps/server/ai/task-tools.ts
|
|
2370
2639
|
import { Type } from "@earendil-works/pi-ai";
|
|
2371
|
-
import { z as
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2640
|
+
import { z as z8 } from "zod";
|
|
2641
|
+
var scope = z8.enum(["member", "project"]).optional();
|
|
2642
|
+
var version = z8.enum(["current", "baseline"]).optional();
|
|
2643
|
+
var filter = {
|
|
2644
|
+
path: z8.string().optional(),
|
|
2645
|
+
query: z8.string().optional(),
|
|
2646
|
+
pattern: z8.string().optional()
|
|
2647
|
+
};
|
|
2648
|
+
function taskTools(explorer, signal, progress, runtime) {
|
|
2649
|
+
function tool(name, schema, execute) {
|
|
2650
|
+
const text = runtime.toolText[name];
|
|
2651
|
+
return {
|
|
2652
|
+
name,
|
|
2653
|
+
...text,
|
|
2654
|
+
parameters: Type.Unsafe(z8.toJSONSchema(schema)),
|
|
2655
|
+
executionMode: "sequential",
|
|
2656
|
+
async execute(_id, params) {
|
|
2657
|
+
signal.throwIfAborted();
|
|
2658
|
+
const parsed = schema.parse(params);
|
|
2659
|
+
progress(name, `${text.label}\u3002`);
|
|
2660
|
+
const value = await execute(parsed);
|
|
2661
|
+
signal.throwIfAborted();
|
|
2662
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }], details: {} };
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
return [
|
|
2667
|
+
tool(
|
|
2668
|
+
"list_files",
|
|
2669
|
+
z8.object({ ...filter, scope, version }).strict(),
|
|
2670
|
+
(p) => explorer.list(p, p.scope, p.version)
|
|
2671
|
+
),
|
|
2672
|
+
tool(
|
|
2673
|
+
"read_file",
|
|
2674
|
+
z8.object({
|
|
2675
|
+
path: FilePathSchema,
|
|
2676
|
+
scope,
|
|
2677
|
+
version,
|
|
2678
|
+
startLine: z8.number().int().positive().optional(),
|
|
2679
|
+
limit: z8.number().int().positive().optional()
|
|
2680
|
+
}).strict(),
|
|
2681
|
+
(p) => explorer.reader(p.scope, p.version).read(p.path, p.startLine, p.limit)
|
|
2682
|
+
),
|
|
2683
|
+
tool(
|
|
2684
|
+
"search_files",
|
|
2685
|
+
z8.object({
|
|
2686
|
+
path: filter.path,
|
|
2687
|
+
pattern: filter.pattern,
|
|
2688
|
+
query: z8.string().min(1),
|
|
2689
|
+
after: z8.string().optional(),
|
|
2690
|
+
scope,
|
|
2691
|
+
version
|
|
2692
|
+
}).strict(),
|
|
2693
|
+
(p) => explorer.search(p.query, p.after, { path: p.path, pattern: p.pattern }, p.scope, p.version)
|
|
2694
|
+
),
|
|
2695
|
+
tool(
|
|
2696
|
+
"inspect_changes",
|
|
2697
|
+
z8.object({
|
|
2698
|
+
...filter,
|
|
2699
|
+
scope,
|
|
2700
|
+
version,
|
|
2701
|
+
startLine: z8.number().int().positive().optional(),
|
|
2702
|
+
endLine: z8.number().int().positive().optional()
|
|
2703
|
+
}).strict(),
|
|
2704
|
+
(p) => explorer.inspect(p, p.scope)
|
|
2705
|
+
),
|
|
2706
|
+
tool(
|
|
2707
|
+
"query_tasks",
|
|
2708
|
+
z8.object({
|
|
2709
|
+
id: z8.uuid().optional(),
|
|
2710
|
+
source: z8.string().optional(),
|
|
2711
|
+
query: z8.string().optional(),
|
|
2712
|
+
visibility: z8.enum(["active", "archived", "all"]).optional()
|
|
2713
|
+
}).strict(),
|
|
2714
|
+
(p) => explorer.queryTasks(p)
|
|
2715
|
+
)
|
|
2716
|
+
].filter((tool2) => runtime.config.tools.some((name) => name === tool2.name));
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
// apps/server/ai/tools.ts
|
|
2720
|
+
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
2721
|
+
import { z as z9 } from "zod";
|
|
2722
|
+
function analysisTools(reader, signal, progress, runtime, explorer) {
|
|
2723
|
+
if (explorer) return taskTools(explorer, signal, progress, runtime);
|
|
2381
2724
|
const reply = (value) => ({
|
|
2382
2725
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
2383
2726
|
details: {}
|
|
@@ -2386,55 +2729,69 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2386
2729
|
{
|
|
2387
2730
|
name: "list_files",
|
|
2388
2731
|
...runtime.toolText.list_files,
|
|
2389
|
-
parameters:
|
|
2390
|
-
{
|
|
2732
|
+
parameters: Type2.Object(
|
|
2733
|
+
{
|
|
2734
|
+
after: Type2.Optional(Type2.String()),
|
|
2735
|
+
scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")]))
|
|
2736
|
+
},
|
|
2391
2737
|
{ additionalProperties: false }
|
|
2392
2738
|
),
|
|
2393
2739
|
executionMode: "sequential",
|
|
2394
2740
|
async execute(_id, params) {
|
|
2395
|
-
|
|
2396
|
-
const p =
|
|
2741
|
+
signal.throwIfAborted();
|
|
2742
|
+
const p = z9.object({
|
|
2743
|
+
after: z9.string().default(""),
|
|
2744
|
+
scope: z9.enum(["member", "project"]).default("member")
|
|
2745
|
+
}).strict().parse(params);
|
|
2397
2746
|
progress("list", "\u6D4F\u89C8\u6587\u4EF6\u6E05\u5355\u3002");
|
|
2398
|
-
return reply(reader.list(p.after
|
|
2747
|
+
return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after));
|
|
2399
2748
|
}
|
|
2400
2749
|
},
|
|
2401
2750
|
{
|
|
2402
2751
|
name: "read_file",
|
|
2403
2752
|
...runtime.toolText.read_file,
|
|
2404
|
-
parameters:
|
|
2753
|
+
parameters: Type2.Object(
|
|
2405
2754
|
{
|
|
2406
|
-
path:
|
|
2407
|
-
|
|
2408
|
-
|
|
2755
|
+
path: Type2.String(),
|
|
2756
|
+
scope: Type2.Optional(Type2.Union([Type2.Literal("member"), Type2.Literal("project")])),
|
|
2757
|
+
startLine: Type2.Optional(Type2.Integer({ minimum: 1 })),
|
|
2758
|
+
limit: Type2.Optional(Type2.Integer({ minimum: 1 }))
|
|
2409
2759
|
},
|
|
2410
2760
|
{ additionalProperties: false }
|
|
2411
2761
|
),
|
|
2412
2762
|
executionMode: "sequential",
|
|
2413
2763
|
async execute(_id, params) {
|
|
2414
|
-
|
|
2415
|
-
const p =
|
|
2764
|
+
signal.throwIfAborted();
|
|
2765
|
+
const p = z9.object({
|
|
2416
2766
|
path: FilePathSchema,
|
|
2417
|
-
|
|
2418
|
-
|
|
2767
|
+
scope: z9.enum(["member", "project"]).default("member"),
|
|
2768
|
+
startLine: z9.number().int().min(1).default(1),
|
|
2769
|
+
limit: z9.number().int().min(1).optional()
|
|
2419
2770
|
}).strict().parse(params);
|
|
2420
2771
|
progress("read", `\u8BFB\u53D6 ${p.path}\uFF0C\u7B2C ${p.startLine} \u884C\u8D77\u3002`);
|
|
2421
|
-
return reply(
|
|
2772
|
+
return reply(
|
|
2773
|
+
await (p.scope === "project" ? reader.projectReader() : reader).read(
|
|
2774
|
+
p.path,
|
|
2775
|
+
p.startLine,
|
|
2776
|
+
p.limit
|
|
2777
|
+
)
|
|
2778
|
+
);
|
|
2422
2779
|
}
|
|
2423
2780
|
},
|
|
2424
2781
|
{
|
|
2425
2782
|
name: "search_files",
|
|
2426
2783
|
...runtime.toolText.search_files,
|
|
2427
|
-
parameters:
|
|
2784
|
+
parameters: Type2.Object(
|
|
2428
2785
|
{
|
|
2429
|
-
query:
|
|
2430
|
-
after:
|
|
2786
|
+
query: Type2.String({ minLength: 1 }),
|
|
2787
|
+
after: Type2.Optional(Type2.String())
|
|
2431
2788
|
},
|
|
2432
2789
|
{ additionalProperties: false }
|
|
2433
2790
|
),
|
|
2434
2791
|
executionMode: "sequential",
|
|
2435
2792
|
async execute(_id, params) {
|
|
2436
|
-
|
|
2437
|
-
const p =
|
|
2793
|
+
signal.throwIfAborted();
|
|
2794
|
+
const p = z9.object({ query: z9.string().min(1), after: z9.string().default("") }).strict().parse(params);
|
|
2438
2795
|
progress("search", "\u641C\u7D22\u5FEB\u7167\u6B63\u6587\u3002");
|
|
2439
2796
|
return reply(await reader.search(p.query, p.after));
|
|
2440
2797
|
}
|
|
@@ -2444,10 +2801,19 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2444
2801
|
}
|
|
2445
2802
|
|
|
2446
2803
|
// apps/server/ai/models.ts
|
|
2447
|
-
import { createModels, createProvider, Type as
|
|
2804
|
+
import { createModels, createProvider, Type as Type3 } from "@earendil-works/pi-ai";
|
|
2448
2805
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
2449
2806
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
2450
2807
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
2808
|
+
function withoutOutputLimits(payload) {
|
|
2809
|
+
const {
|
|
2810
|
+
max_tokens: _chat,
|
|
2811
|
+
max_completion_tokens: _completion,
|
|
2812
|
+
max_output_tokens: _response,
|
|
2813
|
+
...rest
|
|
2814
|
+
} = payload;
|
|
2815
|
+
return rest;
|
|
2816
|
+
}
|
|
2451
2817
|
function modelAccess(profile, key, reasoning = false) {
|
|
2452
2818
|
const model = {
|
|
2453
2819
|
id: profile.model,
|
|
@@ -2457,8 +2823,8 @@ function modelAccess(profile, key, reasoning = false) {
|
|
|
2457
2823
|
baseUrl: profile.protocol === "anthropic-messages" ? profile.baseUrl.replace(/\/v1\/?$/, "") : profile.baseUrl,
|
|
2458
2824
|
reasoning,
|
|
2459
2825
|
input: ["text"],
|
|
2460
|
-
contextWindow:
|
|
2461
|
-
maxTokens:
|
|
2826
|
+
contextWindow: 0,
|
|
2827
|
+
maxTokens: Number.MAX_SAFE_INTEGER,
|
|
2462
2828
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2463
2829
|
...profile.protocol === "openai-completions" ? {
|
|
2464
2830
|
compat: {
|
|
@@ -2504,11 +2870,11 @@ async function modelCall(profile, key, context, signal, extra = {}, thinkingLeve
|
|
|
2504
2870
|
const { models, model } = modelAccess(profile, key, thinkingLevel !== "off");
|
|
2505
2871
|
const options = {
|
|
2506
2872
|
...extra,
|
|
2507
|
-
maxTokens: profile.maxOutputTokens,
|
|
2508
2873
|
maxRetries: 0,
|
|
2509
|
-
timeoutMs: extra.timeoutMs ??
|
|
2874
|
+
timeoutMs: extra.timeoutMs ?? 3e5,
|
|
2510
2875
|
signal,
|
|
2511
|
-
transport: "sse"
|
|
2876
|
+
transport: "sse",
|
|
2877
|
+
onPayload: async (payload, model2) => withoutOutputLimits(await extra.onPayload?.(payload, model2) ?? payload)
|
|
2512
2878
|
};
|
|
2513
2879
|
const stream = thinkingLevel === "off" ? models.stream(model, context, options) : models.streamSimple(model, context, { ...options, reasoning: thinkingLevel });
|
|
2514
2880
|
let streamed = false;
|
|
@@ -2538,13 +2904,13 @@ async function probeModel(profile, key, mode, signal) {
|
|
|
2538
2904
|
{
|
|
2539
2905
|
name: "ping",
|
|
2540
2906
|
description: "Get the verification marker.",
|
|
2541
|
-
parameters:
|
|
2907
|
+
parameters: Type3.Object({ value: Type3.String() })
|
|
2542
2908
|
}
|
|
2543
2909
|
]
|
|
2544
2910
|
} : {}
|
|
2545
2911
|
};
|
|
2546
2912
|
const first = await modelCall(
|
|
2547
|
-
|
|
2913
|
+
profile,
|
|
2548
2914
|
key,
|
|
2549
2915
|
context,
|
|
2550
2916
|
signal,
|
|
@@ -2600,8 +2966,522 @@ async function probeModel(profile, key, mode, signal) {
|
|
|
2600
2966
|
return { ok: true, mode, streamed, inputTokens, outputTokens };
|
|
2601
2967
|
}
|
|
2602
2968
|
|
|
2969
|
+
// apps/server/ai/native-sandbox.ts
|
|
2970
|
+
import { spawn } from "node:child_process";
|
|
2971
|
+
import { lstat, mkdtemp, readlink, rm } from "node:fs/promises";
|
|
2972
|
+
import { tmpdir } from "node:os";
|
|
2973
|
+
import { basename, dirname as dirname4, join as join6 } from "node:path";
|
|
2974
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2975
|
+
|
|
2976
|
+
// apps/server/ai/agent-channel.ts
|
|
2977
|
+
import { createInterface } from "node:readline";
|
|
2978
|
+
var AgentChannel = class {
|
|
2979
|
+
constructor(input, output) {
|
|
2980
|
+
this.output = output;
|
|
2981
|
+
this.lines = createInterface({ input, crlfDelay: Infinity });
|
|
2982
|
+
this.iterator = this.lines[Symbol.asyncIterator]();
|
|
2983
|
+
output.on("error", () => {
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
output;
|
|
2987
|
+
lines;
|
|
2988
|
+
iterator;
|
|
2989
|
+
writes = Promise.resolve();
|
|
2990
|
+
async *messages() {
|
|
2991
|
+
for await (const line of this.iterator) if (line) yield JSON.parse(line);
|
|
2992
|
+
}
|
|
2993
|
+
send(value) {
|
|
2994
|
+
const write = this.writes.then(
|
|
2995
|
+
() => new Promise((resolve4, reject) => {
|
|
2996
|
+
this.output.write(
|
|
2997
|
+
JSON.stringify(value) + "\n",
|
|
2998
|
+
(error) => error ? reject(error) : resolve4()
|
|
2999
|
+
);
|
|
3000
|
+
})
|
|
3001
|
+
);
|
|
3002
|
+
this.writes = write.catch(() => {
|
|
3003
|
+
});
|
|
3004
|
+
return write;
|
|
3005
|
+
}
|
|
3006
|
+
close() {
|
|
3007
|
+
this.lines.close();
|
|
3008
|
+
this.output.end();
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
// apps/server/ai/agent-workspace.ts
|
|
3013
|
+
import { mkdir as mkdir2, writeFile } from "node:fs/promises";
|
|
3014
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
3015
|
+
|
|
3016
|
+
// apps/server/ai/line-diff.ts
|
|
3017
|
+
import { Worker } from "node:worker_threads";
|
|
3018
|
+
async function lineChanges(before, after, signal) {
|
|
3019
|
+
signal?.throwIfAborted();
|
|
3020
|
+
const worker = new Worker(new URL("./line-diff-worker.js", import.meta.url), {
|
|
3021
|
+
workerData: { before, after }
|
|
3022
|
+
});
|
|
3023
|
+
let abort;
|
|
3024
|
+
try {
|
|
3025
|
+
return await new Promise((resolve4, reject) => {
|
|
3026
|
+
abort = () => reject(signal.reason);
|
|
3027
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3028
|
+
worker.once("message", resolve4);
|
|
3029
|
+
worker.once("error", reject);
|
|
3030
|
+
worker.once(
|
|
3031
|
+
"exit",
|
|
3032
|
+
(code) => reject(new Error(`Line comparison exited without a result (${code}).`))
|
|
3033
|
+
);
|
|
3034
|
+
if (signal?.aborted) abort();
|
|
3035
|
+
});
|
|
3036
|
+
} finally {
|
|
3037
|
+
if (abort) signal?.removeEventListener("abort", abort);
|
|
3038
|
+
await worker.terminate();
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// apps/server/ai/task-explorer.ts
|
|
3043
|
+
import { matchesGlob } from "node:path";
|
|
3044
|
+
var displayCount = 40;
|
|
3045
|
+
function abbreviated(items) {
|
|
3046
|
+
const shown = items.slice(0, displayCount), omitted = items.length - shown.length;
|
|
3047
|
+
return {
|
|
3048
|
+
items: shown,
|
|
3049
|
+
total: items.length,
|
|
3050
|
+
omitted,
|
|
3051
|
+
...omitted ? { note: `\u2026\u2026\u7B49 ${omitted} \u9879\uFF1B\u53EF\u7F29\u5C0F\u67E5\u8BE2\u8303\u56F4\u3002` } : {}
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
function matchesFile(path, filter2) {
|
|
3055
|
+
const prefix = filter2.path?.replace(/\/$/, "") ?? "";
|
|
3056
|
+
return (!prefix || path === prefix || path.startsWith(prefix + "/")) && (!filter2.query || path.toLowerCase().includes(filter2.query.toLowerCase())) && (!filter2.pattern || matchesGlob(path, filter2.pattern));
|
|
3057
|
+
}
|
|
3058
|
+
var TaskExplorer = class {
|
|
3059
|
+
constructor(current, context, signal) {
|
|
3060
|
+
this.current = current;
|
|
3061
|
+
this.context = context;
|
|
3062
|
+
this.signal = signal;
|
|
3063
|
+
this.baseline = context?.baseline ? current.forSnapshot(context.baseline) : void 0;
|
|
3064
|
+
}
|
|
3065
|
+
current;
|
|
3066
|
+
context;
|
|
3067
|
+
signal;
|
|
3068
|
+
baseline;
|
|
3069
|
+
ranges = /* @__PURE__ */ new Map();
|
|
3070
|
+
reader(scope2 = "member", version2 = "current") {
|
|
3071
|
+
const reader = version2 === "current" ? this.current : this.baseline;
|
|
3072
|
+
if (!reader)
|
|
3073
|
+
throw new HubError(
|
|
3074
|
+
404,
|
|
3075
|
+
"AI_BASELINE_UNAVAILABLE",
|
|
3076
|
+
"There is no successful baseline snapshot."
|
|
3077
|
+
);
|
|
3078
|
+
return scope2 === "project" ? reader.projectReader() : reader;
|
|
3079
|
+
}
|
|
3080
|
+
files(scope2, version2) {
|
|
3081
|
+
const snapshot = version2 === "current" ? this.current.snapshot : this.context?.baseline;
|
|
3082
|
+
return (scope2 === "project" ? snapshot?.project?.files : snapshot?.files) ?? [];
|
|
3083
|
+
}
|
|
3084
|
+
list(filter2 = {}, scope2 = "member", version2 = "current") {
|
|
3085
|
+
this.reader(scope2, version2);
|
|
3086
|
+
const files = this.files(scope2, version2).filter((file) => matchesFile(file.path, filter2));
|
|
3087
|
+
const prefix = filter2.path?.replace(/\/$/, "") ?? "";
|
|
3088
|
+
const directories = /* @__PURE__ */ new Map();
|
|
3089
|
+
for (const file of files) {
|
|
3090
|
+
const relative = prefix && file.path.startsWith(prefix + "/") ? file.path.slice(prefix.length + 1) : file.path;
|
|
3091
|
+
if (!relative.includes("/")) continue;
|
|
3092
|
+
const path = (prefix ? prefix + "/" : "") + relative.split("/")[0];
|
|
3093
|
+
directories.set(path, (directories.get(path) ?? 0) + 1);
|
|
3094
|
+
}
|
|
3095
|
+
return {
|
|
3096
|
+
scope: scope2,
|
|
3097
|
+
version: version2,
|
|
3098
|
+
files: abbreviated(files.map(({ path, size }) => ({ path, size }))),
|
|
3099
|
+
directories: abbreviated([...directories].map(([path, files2]) => ({ path, files: files2 })))
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
async search(query, after = "", filter2 = {}, scope2 = "member", version2 = "current") {
|
|
3103
|
+
const reader = this.reader(scope2, version2);
|
|
3104
|
+
return reader.search(
|
|
3105
|
+
query,
|
|
3106
|
+
after,
|
|
3107
|
+
reader.snapshot.files.filter((file) => matchesFile(file.path, filter2))
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
changes(filter2 = {}, scope2 = "member") {
|
|
3111
|
+
const changes = changedPaths(this.files(scope2, "baseline"), this.files(scope2, "current"));
|
|
3112
|
+
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));
|
|
3113
|
+
}
|
|
3114
|
+
overview() {
|
|
3115
|
+
return {
|
|
3116
|
+
owner: this.current.snapshot.owner,
|
|
3117
|
+
revision: this.current.snapshot.revision,
|
|
3118
|
+
mode: this.context?.baseline ? "incremental" : "cold_start",
|
|
3119
|
+
baselineRevision: this.context?.previousRevision ?? null,
|
|
3120
|
+
inventory: this.list(),
|
|
3121
|
+
projectReferences: this.current.snapshot.project ? this.list({}, "project") : null,
|
|
3122
|
+
changes: {
|
|
3123
|
+
member: abbreviated(this.changes()),
|
|
3124
|
+
project: abbreviated(this.changes({}, "project"))
|
|
3125
|
+
},
|
|
3126
|
+
previousTasks: {
|
|
3127
|
+
active: this.context?.tasks.filter((task) => task.active !== false).length ?? 0,
|
|
3128
|
+
archived: this.context?.tasks.filter((task) => task.active === false).length ?? 0
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
queryTasks(filter2 = {}) {
|
|
3133
|
+
const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
|
|
3134
|
+
return abbreviated(
|
|
3135
|
+
(this.context?.tasks ?? []).filter(
|
|
3136
|
+
(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 || [
|
|
3137
|
+
task.title,
|
|
3138
|
+
task.goal ?? "",
|
|
3139
|
+
task.progress,
|
|
3140
|
+
...task.sources.map((source) => source.path)
|
|
3141
|
+
].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
|
|
3142
|
+
)
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
async inspect(filter2 = {}, scope2 = "member") {
|
|
3146
|
+
const changes = this.changes(filter2, scope2);
|
|
3147
|
+
const response = abbreviated(changes);
|
|
3148
|
+
const items = [];
|
|
3149
|
+
for (const change of response.items) {
|
|
3150
|
+
this.signal.throwIfAborted();
|
|
3151
|
+
const before = this.files(scope2, "baseline").find((file) => file.path === change.path);
|
|
3152
|
+
const after = this.files(scope2, "current").find((file) => file.path === change.path);
|
|
3153
|
+
const key = `${before?.sha256 ?? ""}:${after?.sha256 ?? ""}`;
|
|
3154
|
+
try {
|
|
3155
|
+
let pending = this.ranges.get(key);
|
|
3156
|
+
if (!pending) {
|
|
3157
|
+
pending = (async () => lineChanges(
|
|
3158
|
+
before ? await this.reader(scope2, "baseline").text(change.path) : "",
|
|
3159
|
+
after ? await this.reader(scope2).text(change.path) : "",
|
|
3160
|
+
this.signal
|
|
3161
|
+
))();
|
|
3162
|
+
this.ranges.set(key, pending);
|
|
3163
|
+
}
|
|
3164
|
+
const ranges = await pending;
|
|
3165
|
+
this.signal.throwIfAborted();
|
|
3166
|
+
const selected = ranges.filter((range) => {
|
|
3167
|
+
const location = filter2.version === "baseline" ? range.before : range.after;
|
|
3168
|
+
return (filter2.startLine === void 0 || location.startLine + Math.max(1, location.count) - 1 >= filter2.startLine) && (filter2.endLine === void 0 || location.startLine <= filter2.endLine);
|
|
3169
|
+
});
|
|
3170
|
+
items.push({ ...change, available: true, ranges: abbreviated(selected) });
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
this.signal.throwIfAborted();
|
|
3173
|
+
items.push({
|
|
3174
|
+
...change,
|
|
3175
|
+
available: false,
|
|
3176
|
+
reason: error instanceof HubError ? error.code : "DIFF_UNAVAILABLE"
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return { scope: scope2, ...response, items };
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
|
|
3184
|
+
// apps/server/ai/agent-workspace.ts
|
|
3185
|
+
async function materializeWorkspace(root, reader, signal) {
|
|
3186
|
+
const owner = FileOwnerSchema.parse(reader.snapshot.owner);
|
|
3187
|
+
await mkdir2(join5(root, owner), { recursive: true });
|
|
3188
|
+
async function copy(source, prefix, shared = false) {
|
|
3189
|
+
for (const file of source.snapshot.files) {
|
|
3190
|
+
signal.throwIfAborted();
|
|
3191
|
+
const path = FilePathSchema.parse(file.path);
|
|
3192
|
+
if (shared && !sharedPathAllowed(path, DEFAULT_SHARED_PATHS)) continue;
|
|
3193
|
+
const target = join5(root, prefix, path);
|
|
3194
|
+
await mkdir2(dirname3(target), { recursive: true });
|
|
3195
|
+
await writeFile(target, await source.bytes(path), { flag: "wx", mode: 292 });
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
await copy(reader, owner);
|
|
3199
|
+
if (reader.snapshot.project) await copy(reader.projectReader(), "", true);
|
|
3200
|
+
}
|
|
3201
|
+
function memberSourcePath(path, owner) {
|
|
3202
|
+
const relative = path.replace(/^\/workspaces\//, "").replace(/^\.\//, "");
|
|
3203
|
+
return relative.startsWith(owner + "/") ? relative.slice(owner.length + 1) : null;
|
|
3204
|
+
}
|
|
3205
|
+
async function taskInput(reader, context, signal) {
|
|
3206
|
+
const owner = reader.snapshot.owner;
|
|
3207
|
+
const lines = [
|
|
3208
|
+
`\u672C\u8F6E\u7EF4\u62A4\u6210\u5458 ${owner} \u7684\u4EFB\u52A1\u6E05\u5355\u3002`,
|
|
3209
|
+
`\u6A21\u5F0F\uFF1A${context?.baseline ? "\u589E\u91CF\u7EF4\u62A4" : "\u9996\u6B21\u6574\u7406"}\uFF1B\u5F53\u524D\u8D44\u6599\uFF1A/workspaces/${owner}/\u3002`,
|
|
3210
|
+
"\u516C\u5171\u8BF4\u660E\u53CA\u6A21\u677F\uFF08\u5982\u6709\uFF09\uFF1A/workspaces/README.md\u3001/workspaces/_template/\u3002",
|
|
3211
|
+
context?.baseline ? "\u5386\u53F2\u57FA\u7EBF\uFF1A/baseline/workspaces/\u3002" : "\u672C\u8F6E\u6CA1\u6709\u5386\u53F2\u57FA\u7EBF\u3002"
|
|
3212
|
+
];
|
|
3213
|
+
if (context?.baseline) {
|
|
3214
|
+
const explorer = new TaskExplorer(reader, context, signal);
|
|
3215
|
+
const changes = [
|
|
3216
|
+
...explorer.changes().map((change) => ({ ...change, scope: "member" })),
|
|
3217
|
+
...explorer.changes({}, "project").map((change) => ({ ...change, scope: "project" }))
|
|
3218
|
+
];
|
|
3219
|
+
lines.push("\u672C\u8F6E\u53D8\u5316\uFF1A");
|
|
3220
|
+
for (const change of changes.slice(0, 20)) {
|
|
3221
|
+
signal.throwIfAborted();
|
|
3222
|
+
const path = change.scope === "member" ? `${owner}/${change.path}` : change.path;
|
|
3223
|
+
let detail = "";
|
|
3224
|
+
if (change.kind === "modified") {
|
|
3225
|
+
const diff = await explorer.inspect({ path: change.path }, change.scope);
|
|
3226
|
+
const item = diff.items[0];
|
|
3227
|
+
if (item?.available) {
|
|
3228
|
+
detail = "\uFF1B\u53D8\u5316\u884C\uFF08\u65E7\u2192\u65B0\uFF09" + item.ranges.items.slice(0, 3).map(
|
|
3229
|
+
(range) => `${range.before.startLine}+${range.before.count}\u2192${range.after.startLine}+${range.after.count}`
|
|
3230
|
+
).join("\u3001");
|
|
3231
|
+
if (item.ranges.total > 3) detail += `\uFF0C\u53E6\u6709 ${item.ranges.total - 3} \u5904`;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
lines.push(`- ${change.kind} ${JSON.stringify(path)}${detail}`);
|
|
3235
|
+
}
|
|
3236
|
+
if (changes.length > 20) lines.push(`\u2026\u2026\u53E6\u6709 ${changes.length - 20} \u4E2A\u6587\u4EF6\u53D1\u751F\u53D8\u5316\u3002`);
|
|
3237
|
+
if (!changes.length) lines.push("- \u65E0\u6587\u4EF6\u53D8\u5316\u3002");
|
|
3238
|
+
}
|
|
3239
|
+
lines.push("\u5DF2\u6709\u4EFB\u52A1\u53EF\u901A\u8FC7 query_tasks \u67E5\u8BE2\u3002");
|
|
3240
|
+
return lines.join("\n");
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// apps/server/ai/agent-protocol.ts
|
|
3244
|
+
import { z as z10 } from "zod";
|
|
3245
|
+
var TaskQuerySchema = z10.object({
|
|
3246
|
+
id: z10.uuid().optional(),
|
|
3247
|
+
source: z10.string().optional(),
|
|
3248
|
+
query: z10.string().optional(),
|
|
3249
|
+
visibility: z10.enum(["active", "archived", "all"]).optional()
|
|
3250
|
+
}).strict();
|
|
3251
|
+
|
|
3252
|
+
// apps/server/ai/task-query.ts
|
|
3253
|
+
function queryTasks(context, owner, input) {
|
|
3254
|
+
const filter2 = TaskQuerySchema.parse(input);
|
|
3255
|
+
const visibility = filter2.visibility ?? (filter2.id ? "all" : "active");
|
|
3256
|
+
return abbreviated(
|
|
3257
|
+
(context?.tasks ?? []).map((task) => ({
|
|
3258
|
+
...task,
|
|
3259
|
+
sources: task.sources.map((source) => ({ ...source, path: `${owner}/${source.path}` }))
|
|
3260
|
+
})).filter(
|
|
3261
|
+
(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 || [
|
|
3262
|
+
task.title,
|
|
3263
|
+
task.goal ?? "",
|
|
3264
|
+
task.progress,
|
|
3265
|
+
...task.sources.map((source) => source.path)
|
|
3266
|
+
].some((text) => text.toLowerCase().includes(filter2.query.toLowerCase())))
|
|
3267
|
+
)
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
// apps/server/ai/native-sandbox.ts
|
|
3272
|
+
async function sandboxCommand(root, baseline) {
|
|
3273
|
+
if (process.platform !== "linux")
|
|
3274
|
+
throw new HubError(
|
|
3275
|
+
503,
|
|
3276
|
+
"AI_SANDBOX_UNAVAILABLE",
|
|
3277
|
+
"Native analysis requires a Linux worker with Bubblewrap."
|
|
3278
|
+
);
|
|
3279
|
+
let dependencies = dirname4(fileURLToPath2(import.meta.resolve("@earendil-works/pi-coding-agent")));
|
|
3280
|
+
while (basename(dependencies) !== "node_modules") {
|
|
3281
|
+
const parent = dirname4(dependencies);
|
|
3282
|
+
if (parent === dependencies) throw new Error("Pi dependency root not found.");
|
|
3283
|
+
dependencies = parent;
|
|
3284
|
+
}
|
|
3285
|
+
const args = [
|
|
3286
|
+
"--unshare-all",
|
|
3287
|
+
"--unshare-user",
|
|
3288
|
+
"--die-with-parent",
|
|
3289
|
+
"--new-session",
|
|
3290
|
+
"--cap-drop",
|
|
3291
|
+
"ALL",
|
|
3292
|
+
"--ro-bind",
|
|
3293
|
+
"/usr",
|
|
3294
|
+
"/usr"
|
|
3295
|
+
];
|
|
3296
|
+
for (const path of ["/bin", "/sbin", "/lib", "/lib64"]) {
|
|
3297
|
+
const stat = await lstat(path).catch(() => null);
|
|
3298
|
+
if (!stat) continue;
|
|
3299
|
+
if (stat.isSymbolicLink()) args.push("--symlink", await readlink(path), path);
|
|
3300
|
+
else args.push("--ro-bind", path, path);
|
|
3301
|
+
}
|
|
3302
|
+
if (await lstat("/etc/alternatives").catch(() => null))
|
|
3303
|
+
args.push("--ro-bind", "/etc/alternatives", "/etc/alternatives");
|
|
3304
|
+
args.push(
|
|
3305
|
+
"--proc",
|
|
3306
|
+
"/proc",
|
|
3307
|
+
"--dev",
|
|
3308
|
+
"/dev",
|
|
3309
|
+
"--tmpfs",
|
|
3310
|
+
"/tmp",
|
|
3311
|
+
"--dir",
|
|
3312
|
+
"/home/agent",
|
|
3313
|
+
"--setenv",
|
|
3314
|
+
"HOME",
|
|
3315
|
+
"/home/agent",
|
|
3316
|
+
"--setenv",
|
|
3317
|
+
"PATH",
|
|
3318
|
+
"/opt/node-bin:/usr/local/bin:/usr/bin:/bin",
|
|
3319
|
+
"--setenv",
|
|
3320
|
+
"LANG",
|
|
3321
|
+
"C.UTF-8",
|
|
3322
|
+
"--setenv",
|
|
3323
|
+
"SHELL",
|
|
3324
|
+
"/bin/bash",
|
|
3325
|
+
"--setenv",
|
|
3326
|
+
"PI_SKIP_VERSION_CHECK",
|
|
3327
|
+
"1",
|
|
3328
|
+
"--ro-bind",
|
|
3329
|
+
process.execPath,
|
|
3330
|
+
"/opt/node-bin/node",
|
|
3331
|
+
"--ro-bind",
|
|
3332
|
+
dependencies,
|
|
3333
|
+
"/opt/teammy/node_modules",
|
|
3334
|
+
"--ro-bind",
|
|
3335
|
+
fileURLToPath2(new URL("./native-pi-entry.js", import.meta.url)),
|
|
3336
|
+
"/opt/teammy/native-pi-entry.mjs",
|
|
3337
|
+
"--ro-bind",
|
|
3338
|
+
join6(root, "workspaces"),
|
|
3339
|
+
"/workspaces"
|
|
3340
|
+
);
|
|
3341
|
+
if (baseline) args.push("--ro-bind", join6(root, "baseline/workspaces"), "/baseline/workspaces");
|
|
3342
|
+
args.push("--chdir", "/workspaces", "/opt/node-bin/node", "/opt/teammy/native-pi-entry.mjs");
|
|
3343
|
+
return { command: "bwrap", args };
|
|
3344
|
+
}
|
|
3345
|
+
async function runNativeAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, context = null) {
|
|
3346
|
+
signal.throwIfAborted();
|
|
3347
|
+
const root = await mkdtemp(join6(tmpdir(), "teammy-agent-"));
|
|
3348
|
+
try {
|
|
3349
|
+
await materializeWorkspace(join6(root, "workspaces"), reader, signal);
|
|
3350
|
+
if (context?.baseline)
|
|
3351
|
+
await materializeWorkspace(
|
|
3352
|
+
join6(root, "baseline/workspaces"),
|
|
3353
|
+
reader.forSnapshot(context.baseline),
|
|
3354
|
+
signal
|
|
3355
|
+
);
|
|
3356
|
+
const { command, args } = await sandboxCommand(root, Boolean(context?.baseline));
|
|
3357
|
+
signal.throwIfAborted();
|
|
3358
|
+
const child = spawn(command, args, {
|
|
3359
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3360
|
+
detached: true,
|
|
3361
|
+
env: { PATH: "/usr/bin:/bin" }
|
|
3362
|
+
});
|
|
3363
|
+
const channel = new AgentChannel(child.stdout, child.stdin);
|
|
3364
|
+
const active = new AbortController();
|
|
3365
|
+
const requestSignal = AbortSignal.any([signal, active.signal]);
|
|
3366
|
+
let failure, text, rounds = 0;
|
|
3367
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
3368
|
+
const stop = () => {
|
|
3369
|
+
active.abort();
|
|
3370
|
+
if (child.pid) {
|
|
3371
|
+
try {
|
|
3372
|
+
process.kill(-child.pid, "SIGKILL");
|
|
3373
|
+
} catch {
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
const closed = new Promise((resolve4) => {
|
|
3378
|
+
child.once("error", () => {
|
|
3379
|
+
failure ??= new HubError(
|
|
3380
|
+
503,
|
|
3381
|
+
"AI_SANDBOX_UNAVAILABLE",
|
|
3382
|
+
"Could not start Bubblewrap; check the Linux worker setup."
|
|
3383
|
+
);
|
|
3384
|
+
});
|
|
3385
|
+
child.once("close", (code) => {
|
|
3386
|
+
if (code !== 0 && !signal.aborted)
|
|
3387
|
+
failure ??= new HubError(
|
|
3388
|
+
503,
|
|
3389
|
+
"AI_SANDBOX_FAILED",
|
|
3390
|
+
"Sandbox exited unsuccessfully; check worker isolation support."
|
|
3391
|
+
);
|
|
3392
|
+
active.abort();
|
|
3393
|
+
resolve4();
|
|
3394
|
+
});
|
|
3395
|
+
});
|
|
3396
|
+
child.stderr.resume();
|
|
3397
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
3398
|
+
const pending = /* @__PURE__ */ new Set();
|
|
3399
|
+
const handle = async (message) => {
|
|
3400
|
+
if (message.type === "progress") {
|
|
3401
|
+
if (runtime.config.tools.some((tool) => tool === message.tool))
|
|
3402
|
+
progress("tool", `\u4F7F\u7528 ${message.tool}\u3002`);
|
|
3403
|
+
} else if (message.type === "result") {
|
|
3404
|
+
if (typeof message.text !== "string" || text !== void 0)
|
|
3405
|
+
throw new Error("Invalid result.");
|
|
3406
|
+
text = message.text;
|
|
3407
|
+
} else if (message.type === "failed") {
|
|
3408
|
+
throw new HubError(422, "AI_RESULT_INVALID", "Agent did not complete analysis.");
|
|
3409
|
+
} else if (message.type === "request") {
|
|
3410
|
+
let value;
|
|
3411
|
+
if (message.method === "model") {
|
|
3412
|
+
if (++rounds > runtime.config.maxRounds)
|
|
3413
|
+
throw new HubError(413, "AI_ROUND_LIMIT", "Analysis exceeded its model round budget.");
|
|
3414
|
+
progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${rounds} \u8F6E\u3002`);
|
|
3415
|
+
const response = await modelCall(
|
|
3416
|
+
profile,
|
|
3417
|
+
key,
|
|
3418
|
+
message.input,
|
|
3419
|
+
requestSignal,
|
|
3420
|
+
{ timeoutMs: runtime.config.requestTimeoutMs },
|
|
3421
|
+
runtime.config.thinkingLevel
|
|
3422
|
+
);
|
|
3423
|
+
value = response.message;
|
|
3424
|
+
usage.inputTokens += response.message.usage.input;
|
|
3425
|
+
usage.outputTokens += response.message.usage.output;
|
|
3426
|
+
} else if (message.method === "query_tasks" && runtime.config.tools.includes("query_tasks")) {
|
|
3427
|
+
value = queryTasks(context, reader.snapshot.owner, message.input);
|
|
3428
|
+
} else throw new HubError(403, "AI_TOOL_UNAVAILABLE", "Tool is unavailable for this job.");
|
|
3429
|
+
await channel.send({ type: "response", id: message.id, value });
|
|
3430
|
+
} else throw new Error("Invalid sandbox message.");
|
|
3431
|
+
};
|
|
3432
|
+
try {
|
|
3433
|
+
const { model } = modelAccess(
|
|
3434
|
+
{ ...profile, baseUrl: "http://model.invalid" },
|
|
3435
|
+
"",
|
|
3436
|
+
runtime.config.thinkingLevel !== "off"
|
|
3437
|
+
);
|
|
3438
|
+
await channel.send({ type: "start", config: { model, runtime, systemPrompt, input } });
|
|
3439
|
+
if (signal.aborted) stop();
|
|
3440
|
+
for await (const message of channel.messages()) {
|
|
3441
|
+
const task = handle(message).catch((error) => {
|
|
3442
|
+
failure ??= error;
|
|
3443
|
+
stop();
|
|
3444
|
+
});
|
|
3445
|
+
pending.add(task);
|
|
3446
|
+
void task.finally(() => pending.delete(task));
|
|
3447
|
+
}
|
|
3448
|
+
await closed;
|
|
3449
|
+
await Promise.all(pending);
|
|
3450
|
+
signal.throwIfAborted();
|
|
3451
|
+
if (failure) throw failure;
|
|
3452
|
+
if (text === void 0) throw new HubError(422, "AI_RESULT_EMPTY", "No analysis result.");
|
|
3453
|
+
return { text, usage };
|
|
3454
|
+
} catch (error) {
|
|
3455
|
+
stop();
|
|
3456
|
+
await closed;
|
|
3457
|
+
signal.throwIfAborted();
|
|
3458
|
+
throw failure ?? error;
|
|
3459
|
+
} finally {
|
|
3460
|
+
signal.removeEventListener("abort", stop);
|
|
3461
|
+
stop();
|
|
3462
|
+
channel.close();
|
|
3463
|
+
await closed;
|
|
3464
|
+
await Promise.allSettled(pending);
|
|
3465
|
+
}
|
|
3466
|
+
} finally {
|
|
3467
|
+
await rm(root, { recursive: true, force: true });
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
2603
3471
|
// apps/server/ai/pi-runner.ts
|
|
2604
|
-
async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input) {
|
|
3472
|
+
async function runAgent(profile, reader, key, signal, progress, runtime, systemPrompt, input, explorer, context = null) {
|
|
3473
|
+
if (runtime.config.execution === "native-sandbox")
|
|
3474
|
+
return runNativeAgent(
|
|
3475
|
+
profile,
|
|
3476
|
+
reader,
|
|
3477
|
+
key,
|
|
3478
|
+
signal,
|
|
3479
|
+
progress,
|
|
3480
|
+
runtime,
|
|
3481
|
+
systemPrompt,
|
|
3482
|
+
typeof input === "string" ? input : JSON.stringify(input),
|
|
3483
|
+
context
|
|
3484
|
+
);
|
|
2605
3485
|
const {
|
|
2606
3486
|
createAgentSession,
|
|
2607
3487
|
createExtensionRuntime,
|
|
@@ -2609,7 +3489,7 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2609
3489
|
SessionManager,
|
|
2610
3490
|
SettingsManager
|
|
2611
3491
|
} = await import("@earendil-works/pi-coding-agent");
|
|
2612
|
-
const cwd = await
|
|
3492
|
+
const cwd = await mkdtemp2(join7(tmpdir2(), "teammy-ai-run-"));
|
|
2613
3493
|
try {
|
|
2614
3494
|
const { models, model } = modelAccess(profile, key, runtime.config.thinkingLevel !== "off");
|
|
2615
3495
|
const modelRuntime = await ModelRuntime.create({
|
|
@@ -2650,29 +3530,29 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2650
3530
|
cacheWarming: "off"
|
|
2651
3531
|
}),
|
|
2652
3532
|
tools: runtime.config.tools,
|
|
2653
|
-
customTools: analysisTools(reader, signal, progress, runtime)
|
|
3533
|
+
customTools: analysisTools(reader, signal, progress, runtime, explorer)
|
|
2654
3534
|
});
|
|
2655
3535
|
let requests = 0, budgetError;
|
|
2656
3536
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
2657
|
-
session.agent.streamFunction = (_model,
|
|
3537
|
+
session.agent.streamFunction = (_model, context2, options) => {
|
|
2658
3538
|
signal.throwIfAborted();
|
|
2659
|
-
if (++requests > runtime.config.maxRounds
|
|
3539
|
+
if (++requests > runtime.config.maxRounds) {
|
|
2660
3540
|
budgetError = new HubError(
|
|
2661
3541
|
413,
|
|
2662
|
-
"
|
|
2663
|
-
"Analysis exceeded its
|
|
3542
|
+
"AI_ROUND_LIMIT",
|
|
3543
|
+
"Analysis exceeded its model round budget."
|
|
2664
3544
|
);
|
|
2665
3545
|
throw budgetError;
|
|
2666
3546
|
}
|
|
2667
3547
|
progress("model", `\u6A21\u578B\u5206\u6790\u7B2C ${requests} \u8F6E\u3002`);
|
|
2668
|
-
return models.streamSimple(model,
|
|
3548
|
+
return models.streamSimple(model, context2, {
|
|
2669
3549
|
...options,
|
|
2670
3550
|
...runtime.config.thinkingLevel !== "off" ? { reasoning: runtime.config.thinkingLevel } : {},
|
|
2671
3551
|
signal: options?.signal ? AbortSignal.any([signal, options.signal]) : signal,
|
|
2672
3552
|
maxRetries: 0,
|
|
2673
|
-
maxTokens: profile.maxOutputTokens,
|
|
2674
3553
|
timeoutMs: runtime.config.requestTimeoutMs,
|
|
2675
|
-
transport: "sse"
|
|
3554
|
+
transport: "sse",
|
|
3555
|
+
onPayload: async (payload, model2) => withoutOutputLimits(await options?.onPayload?.(payload, model2) ?? payload)
|
|
2676
3556
|
});
|
|
2677
3557
|
};
|
|
2678
3558
|
const unsubscribe = session.subscribe((event) => {
|
|
@@ -2699,25 +3579,25 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2699
3579
|
session.dispose();
|
|
2700
3580
|
}
|
|
2701
3581
|
} finally {
|
|
2702
|
-
await
|
|
3582
|
+
await rm2(cwd, { recursive: true, force: true });
|
|
2703
3583
|
}
|
|
2704
3584
|
}
|
|
2705
3585
|
|
|
2706
3586
|
// apps/server/ai/results.ts
|
|
2707
|
-
import { z as
|
|
2708
|
-
var SummaryOutputSchema =
|
|
2709
|
-
summary:
|
|
2710
|
-
findings:
|
|
2711
|
-
uncertainties:
|
|
2712
|
-
citations:
|
|
2713
|
-
|
|
2714
|
-
path:
|
|
2715
|
-
startLine:
|
|
2716
|
-
endLine:
|
|
3587
|
+
import { z as z11 } from "zod";
|
|
3588
|
+
var SummaryOutputSchema = z11.object({
|
|
3589
|
+
summary: z11.string(),
|
|
3590
|
+
findings: z11.array(z11.string()),
|
|
3591
|
+
uncertainties: z11.array(z11.string()).optional(),
|
|
3592
|
+
citations: z11.array(
|
|
3593
|
+
z11.object({
|
|
3594
|
+
path: z11.string(),
|
|
3595
|
+
startLine: z11.number().int().positive(),
|
|
3596
|
+
endLine: z11.number().int().positive()
|
|
2717
3597
|
}).strict()
|
|
2718
|
-
)
|
|
3598
|
+
)
|
|
2719
3599
|
}).strict();
|
|
2720
|
-
function validateResult(text, reader) {
|
|
3600
|
+
function validateResult(text, reader, native = false) {
|
|
2721
3601
|
let output;
|
|
2722
3602
|
try {
|
|
2723
3603
|
output = SummaryOutputSchema.parse(
|
|
@@ -2730,29 +3610,29 @@ function validateResult(text, reader) {
|
|
|
2730
3610
|
"Analysis did not produce the required JSON result."
|
|
2731
3611
|
);
|
|
2732
3612
|
}
|
|
2733
|
-
const citations = output.citations.
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
return { ...c, sha256: file.sha256 };
|
|
3613
|
+
const citations = output.citations.flatMap((c) => {
|
|
3614
|
+
const path = native ? memberSourcePath(c.path, reader.snapshot.owner) : c.path;
|
|
3615
|
+
const file = reader.snapshot.files.find((f) => f.path === path);
|
|
3616
|
+
if (c.endLine < c.startLine)
|
|
3617
|
+
throw new HubError(422, "AI_CITATION_INVALID", "Citation line range must be ordered.");
|
|
3618
|
+
if (!file) {
|
|
3619
|
+
reader.limitations.add(`\u5F15\u7528\u672A\u80FD\u5173\u8054\u5230\u672C\u8F6E\u8D44\u6599\uFF1A${c.path}`);
|
|
3620
|
+
return [];
|
|
3621
|
+
}
|
|
3622
|
+
return [{ ...c, path: file.path, sha256: file.sha256 }];
|
|
2744
3623
|
});
|
|
2745
3624
|
return AiResultSchema.parse({
|
|
2746
3625
|
...output,
|
|
2747
3626
|
citations,
|
|
2748
|
-
|
|
3627
|
+
uncertainties: output.uncertainties ?? [],
|
|
3628
|
+
coverage: coverage(reader, native)
|
|
2749
3629
|
});
|
|
2750
3630
|
}
|
|
2751
|
-
function coverage(reader) {
|
|
3631
|
+
function coverage(reader, native = false) {
|
|
2752
3632
|
return {
|
|
2753
3633
|
totalFiles: reader.snapshot.files.length,
|
|
2754
3634
|
readFiles: [...reader.readFiles],
|
|
2755
|
-
|
|
3635
|
+
...native ? { accessTracking: "unavailable" } : {},
|
|
2756
3636
|
limitations: [...reader.limitations]
|
|
2757
3637
|
};
|
|
2758
3638
|
}
|
|
@@ -2769,25 +3649,23 @@ async function analyzeDirectory(job, reader, key, signal, progress, runtime) {
|
|
|
2769
3649
|
workflowPrompt(runtime, SummaryOutputSchema),
|
|
2770
3650
|
{
|
|
2771
3651
|
owner: job.snapshot.owner,
|
|
3652
|
+
workspace: `/workspaces/${job.snapshot.owner}/`,
|
|
2772
3653
|
scope: job.snapshot.path || "/",
|
|
2773
3654
|
revision: job.snapshot.revision,
|
|
2774
3655
|
totalFiles: job.snapshot.files.length,
|
|
2775
3656
|
question: job.request.question
|
|
2776
3657
|
}
|
|
2777
3658
|
);
|
|
2778
|
-
return {
|
|
3659
|
+
return {
|
|
3660
|
+
result: validateResult(output.text, reader, runtime.config.execution === "native-sandbox"),
|
|
3661
|
+
usage: output.usage
|
|
3662
|
+
};
|
|
2779
3663
|
}
|
|
2780
3664
|
|
|
2781
3665
|
// apps/server/ai/workflows/file-summary.ts
|
|
2782
3666
|
async function summarizeFile(job, reader, key, signal, runtime) {
|
|
2783
3667
|
const path = job.snapshot.path;
|
|
2784
|
-
const body = await reader.whole(
|
|
2785
|
-
path,
|
|
2786
|
-
Math.min(
|
|
2787
|
-
runtime.config.maxReadBytes,
|
|
2788
|
-
Math.max(1024, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
|
|
2789
|
-
)
|
|
2790
|
-
);
|
|
3668
|
+
const body = await reader.whole(path);
|
|
2791
3669
|
const context = {
|
|
2792
3670
|
systemPrompt: workflowPrompt(runtime, SummaryOutputSchema),
|
|
2793
3671
|
messages: [
|
|
@@ -2802,13 +3680,6 @@ async function summarizeFile(job, reader, key, signal, runtime) {
|
|
|
2802
3680
|
}
|
|
2803
3681
|
]
|
|
2804
3682
|
};
|
|
2805
|
-
if (Buffer.byteLength(JSON.stringify(context)) + job.profile.maxOutputTokens > job.profile.contextWindow) {
|
|
2806
|
-
throw new HubError(
|
|
2807
|
-
413,
|
|
2808
|
-
"AI_CONTEXT_OR_ROUND_LIMIT",
|
|
2809
|
-
"File exceeds the analysis context budget."
|
|
2810
|
-
);
|
|
2811
|
-
}
|
|
2812
3683
|
const { message } = await modelCall(
|
|
2813
3684
|
job.profile,
|
|
2814
3685
|
key,
|
|
@@ -2826,61 +3697,81 @@ async function summarizeFile(job, reader, key, signal, runtime) {
|
|
|
2826
3697
|
}
|
|
2827
3698
|
|
|
2828
3699
|
// apps/server/ai/workflows/task-bootstrap.ts
|
|
2829
|
-
import { z as
|
|
2830
|
-
var
|
|
2831
|
-
tasks:
|
|
3700
|
+
import { z as z12 } from "zod";
|
|
3701
|
+
var LegacyTaskOutputSchema = z12.object({
|
|
3702
|
+
tasks: z12.array(
|
|
3703
|
+
TaskFieldsSchema.extend({
|
|
3704
|
+
taskId: z12.uuid().nullable().optional(),
|
|
3705
|
+
sources: z12.array(z12.string())
|
|
3706
|
+
})
|
|
3707
|
+
),
|
|
3708
|
+
uncertainties: z12.array(z12.string()).optional()
|
|
3709
|
+
}).strict();
|
|
3710
|
+
var LegacyPatchOutputSchema = z12.object({
|
|
3711
|
+
upsert: z12.array(
|
|
3712
|
+
TaskFieldsSchema.extend({ taskId: z12.uuid().nullable(), sources: z12.array(z12.string()) })
|
|
3713
|
+
),
|
|
3714
|
+
archive: z12.array(z12.uuid()),
|
|
3715
|
+
uncertainties: z12.array(z12.string()).optional()
|
|
3716
|
+
}).strict();
|
|
3717
|
+
var TaskOutputSchema = z12.object({
|
|
3718
|
+
upsert: z12.array(
|
|
2832
3719
|
TaskFieldsSchema.extend({
|
|
2833
|
-
taskId:
|
|
2834
|
-
sources:
|
|
3720
|
+
taskId: z12.uuid().nullable(),
|
|
3721
|
+
sources: z12.array(z12.string()),
|
|
3722
|
+
needsConfirmation: z12.boolean()
|
|
2835
3723
|
})
|
|
2836
|
-
)
|
|
2837
|
-
|
|
3724
|
+
),
|
|
3725
|
+
archive: z12.array(z12.uuid())
|
|
2838
3726
|
}).strict();
|
|
2839
|
-
function validateTasks(text, reader, context) {
|
|
3727
|
+
function validateTasks(text, reader, context, patch = true, native = false) {
|
|
2840
3728
|
let output;
|
|
2841
3729
|
try {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3730
|
+
const json = JSON.parse(text.trim().replace(/^```(?:json)?\s*\n([\s\S]*)\n```$/, "$1"));
|
|
3731
|
+
if (patch) {
|
|
3732
|
+
const parsed = native ? TaskOutputSchema.parse(json) : LegacyPatchOutputSchema.parse(json);
|
|
3733
|
+
output = {
|
|
3734
|
+
tasks: parsed.upsert,
|
|
3735
|
+
archive: parsed.archive,
|
|
3736
|
+
uncertainties: native ? void 0 : parsed.uncertainties
|
|
3737
|
+
};
|
|
3738
|
+
} else output = LegacyTaskOutputSchema.parse(json);
|
|
2845
3739
|
} catch {
|
|
2846
|
-
throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce
|
|
3740
|
+
throw new HubError(422, "AI_RESULT_INVALID", "Analysis did not produce valid task changes.");
|
|
2847
3741
|
}
|
|
2848
|
-
if (!reader.readFiles.size)
|
|
2849
|
-
throw new HubError(
|
|
2850
|
-
422,
|
|
2851
|
-
"AI_TASK_SOURCE_INVALID",
|
|
2852
|
-
"Read workspace material before deciding whether it contains tasks."
|
|
2853
|
-
);
|
|
2854
|
-
const keys = output.tasks.map((task) => JSON.stringify([task.title, [...task.sources].sort()]));
|
|
2855
|
-
if (new Set(keys).size !== keys.length)
|
|
2856
|
-
throw new HubError(422, "AI_TASK_DUPLICATE", "Task list contains identical entries.");
|
|
2857
3742
|
const previous = new Map(context?.tasks.map((task) => [task.taskId, task]));
|
|
2858
3743
|
const used = /* @__PURE__ */ new Set();
|
|
2859
|
-
for (const
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
return current && old.sources.some((source) => source.path === path || source.sha256 === current.sha256);
|
|
2865
|
-
}))
|
|
3744
|
+
for (const id of [
|
|
3745
|
+
...output.tasks.flatMap((task) => task.taskId ? [task.taskId] : []),
|
|
3746
|
+
...output.archive ?? []
|
|
3747
|
+
]) {
|
|
3748
|
+
if (!previous.has(id) || used.has(id))
|
|
2866
3749
|
throw new HubError(
|
|
2867
3750
|
422,
|
|
2868
3751
|
"AI_TASK_ID_INVALID",
|
|
2869
|
-
"Existing task identity must be unique and
|
|
3752
|
+
"Existing task identity must be unique and belong to this member."
|
|
2870
3753
|
);
|
|
2871
|
-
used.add(
|
|
3754
|
+
used.add(id);
|
|
2872
3755
|
}
|
|
2873
3756
|
return TaskAnalysisResultSchema.parse({
|
|
2874
3757
|
kind: "task-bootstrap",
|
|
3758
|
+
...patch ? { updateMode: "patch", archive: output.archive } : {},
|
|
2875
3759
|
tasks: output.tasks.map((task) => ({
|
|
2876
3760
|
...task,
|
|
2877
|
-
sources: task.sources.
|
|
3761
|
+
sources: [...new Set(task.sources)].flatMap((path) => {
|
|
3762
|
+
const relative = native ? memberSourcePath(path, reader.snapshot.owner) : path;
|
|
3763
|
+
const source = reader.source(relative ?? path);
|
|
3764
|
+
return source ? [source] : [];
|
|
3765
|
+
})
|
|
2878
3766
|
})),
|
|
2879
|
-
uncertainties: output.uncertainties,
|
|
2880
|
-
coverage: coverage(reader)
|
|
3767
|
+
...!native ? { uncertainties: output.uncertainties ?? [] } : {},
|
|
3768
|
+
coverage: coverage(reader, native)
|
|
2881
3769
|
});
|
|
2882
3770
|
}
|
|
2883
3771
|
async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
|
|
3772
|
+
const patch = runtime.config.version >= 7;
|
|
3773
|
+
const native = runtime.config.execution === "native-sandbox";
|
|
3774
|
+
const explorer = patch && !native ? new TaskExplorer(reader, job.taskContext, signal) : void 0;
|
|
2884
3775
|
const output = await runAgent(
|
|
2885
3776
|
job.profile,
|
|
2886
3777
|
reader,
|
|
@@ -2888,15 +3779,28 @@ async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
|
|
|
2888
3779
|
signal,
|
|
2889
3780
|
progress,
|
|
2890
3781
|
runtime,
|
|
2891
|
-
workflowPrompt(
|
|
2892
|
-
|
|
3782
|
+
workflowPrompt(
|
|
3783
|
+
runtime,
|
|
3784
|
+
native ? TaskOutputSchema : patch ? LegacyPatchOutputSchema : LegacyTaskOutputSchema
|
|
3785
|
+
),
|
|
3786
|
+
native ? await taskInput(reader, job.taskContext, signal) : explorer?.overview() ?? {
|
|
2893
3787
|
owner: job.snapshot.owner,
|
|
2894
3788
|
revision: job.snapshot.revision,
|
|
2895
|
-
inventory: reader.list(
|
|
2896
|
-
previous: job.taskContext
|
|
2897
|
-
|
|
3789
|
+
inventory: reader.list(),
|
|
3790
|
+
previous: job.taskContext ? {
|
|
3791
|
+
previousRevision: job.taskContext.previousRevision,
|
|
3792
|
+
changes: job.taskContext.changes,
|
|
3793
|
+
tasks: job.taskContext.tasks.filter((task) => task.active !== false)
|
|
3794
|
+
} : null,
|
|
3795
|
+
projectReferences: job.snapshot.project ? reader.projectReader().list() : null
|
|
3796
|
+
},
|
|
3797
|
+
explorer,
|
|
3798
|
+
job.taskContext
|
|
2898
3799
|
);
|
|
2899
|
-
return {
|
|
3800
|
+
return {
|
|
3801
|
+
result: validateTasks(output.text, reader, job.taskContext, patch, native),
|
|
3802
|
+
usage: output.usage
|
|
3803
|
+
};
|
|
2900
3804
|
}
|
|
2901
3805
|
|
|
2902
3806
|
// apps/server/ai/bootstrap.ts
|
|
@@ -2913,20 +3817,24 @@ function requestTaskBootstrap(hub, session, owner, operationId) {
|
|
|
2913
3817
|
return { jobId: job.id, empty: false };
|
|
2914
3818
|
}
|
|
2915
3819
|
function scheduleTaskBootstraps(hub, at = Date.now()) {
|
|
3820
|
+
const jobs = new AiJobs(hub);
|
|
3821
|
+
jobs.cancelStaleTaskJobs();
|
|
2916
3822
|
const candidates = hub.db.prepare(
|
|
2917
|
-
`SELECT m.workspace_id,m.member_id,r.created_at
|
|
3823
|
+
`SELECT m.workspace_id,m.member_id,MAX(r.created_at,COALESCE(pr.created_at,r.created_at)) created_at
|
|
2918
3824
|
FROM members m JOIN projects p ON p.id=m.workspace_id JOIN users u ON u.id=m.member_id
|
|
2919
3825
|
JOIN file_revisions r ON r.workspace_id=m.workspace_id AND r.owner=m.member_id AND r.revision=m.file_revision
|
|
3826
|
+
LEFT JOIN project_files pf ON pf.workspace_id=m.workspace_id
|
|
3827
|
+
LEFT JOIN file_revisions pr ON pr.workspace_id=m.workspace_id AND pr.owner='_project' AND pr.revision=pf.file_revision
|
|
2920
3828
|
LEFT JOIN task_board_members b ON b.workspace_id=m.workspace_id AND b.owner=m.member_id LEFT JOIN ai_jobs j ON j.id=b.job_id
|
|
2921
3829
|
WHERE m.enabled=1 AND u.enabled=1 AND (
|
|
2922
3830
|
(b.job_id IS NULL AND EXISTS(SELECT 1 FROM current_files f WHERE f.workspace_id=m.workspace_id AND f.owner=m.member_id))
|
|
2923
|
-
OR (j.status IN ('succeeded','failed','cancelled') AND json_extract(j.snapshot_json,'$.revision') < m.file_revision)
|
|
3831
|
+
OR (j.status IN ('succeeded','failed','cancelled') AND (json_extract(j.snapshot_json,'$.revision') < m.file_revision OR COALESCE(json_extract(j.snapshot_json,'$.project.revision'),0) < COALESCE(pf.file_revision,0)))
|
|
2924
3832
|
)
|
|
2925
3833
|
ORDER BY r.created_at,m.workspace_id,m.member_id`
|
|
2926
3834
|
).all();
|
|
2927
3835
|
if (!candidates.length) return 0;
|
|
2928
3836
|
const runtime = loadWorkflow("task-bootstrap");
|
|
2929
|
-
const defaults = new AiDefaults(hub)
|
|
3837
|
+
const defaults = new AiDefaults(hub);
|
|
2930
3838
|
let queued = 0;
|
|
2931
3839
|
for (const row of candidates) {
|
|
2932
3840
|
if (at - Date.parse(String(row.created_at)) < runtime.config.settleSeconds * 1e3) continue;
|
|
@@ -2985,8 +3893,7 @@ async function runAiCycle(hub, stop) {
|
|
|
2985
3893
|
if (!requesterActive())
|
|
2986
3894
|
throw new HubError(403, "AI_REQUESTER_DISABLED", "Requester is disabled.");
|
|
2987
3895
|
if (job.request.workflow === "task-bootstrap") {
|
|
2988
|
-
|
|
2989
|
-
if (Number(current?.file_revision) !== job.snapshot.revision)
|
|
3896
|
+
if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
|
|
2990
3897
|
throw new HubError(
|
|
2991
3898
|
409,
|
|
2992
3899
|
"AI_SOURCE_CHANGED",
|
|
@@ -2998,9 +3905,10 @@ async function runAiCycle(hub, stop) {
|
|
|
2998
3905
|
job,
|
|
2999
3906
|
{
|
|
3000
3907
|
kind: "task-bootstrap",
|
|
3908
|
+
...runtime.config.version >= 7 ? { updateMode: "patch", archive: [] } : {},
|
|
3001
3909
|
tasks: [],
|
|
3002
|
-
uncertainties: [],
|
|
3003
|
-
coverage: { totalFiles: 0, readFiles: [],
|
|
3910
|
+
...runtime.config.execution !== "native-sandbox" ? { uncertainties: [] } : {},
|
|
3911
|
+
coverage: { totalFiles: 0, readFiles: [], limitations: [] }
|
|
3004
3912
|
},
|
|
3005
3913
|
null,
|
|
3006
3914
|
{ inputTokens: 0, outputTokens: 0 }
|
|
@@ -3017,10 +3925,7 @@ async function runAiCycle(hub, stop) {
|
|
|
3017
3925
|
hub,
|
|
3018
3926
|
{ workspaceId: job.workspace, memberId: job.requester },
|
|
3019
3927
|
job.snapshot,
|
|
3020
|
-
|
|
3021
|
-
runtime.config.maxReadBytes,
|
|
3022
|
-
Math.max(2048, job.profile.contextWindow - job.profile.maxOutputTokens - 4e3)
|
|
3023
|
-
)
|
|
3928
|
+
runtime.config.maxReadBytes
|
|
3024
3929
|
);
|
|
3025
3930
|
const key = config.secret(profile.id);
|
|
3026
3931
|
const output = job.request.workflow === "file-summary" ? await summarizeFile(job, reader, key, signal, runtime) : await (job.request.workflow === "task-bootstrap" ? bootstrapTasks : analyzeDirectory)(
|
|
@@ -3079,11 +3984,11 @@ async function runAiWorker(directory, once = false) {
|
|
|
3079
3984
|
}
|
|
3080
3985
|
|
|
3081
3986
|
// apps/server/main.ts
|
|
3082
|
-
import { fileURLToPath as
|
|
3987
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3083
3988
|
import { parseArgs } from "node:util";
|
|
3084
3989
|
|
|
3085
3990
|
// apps/server/ai/job-routes.ts
|
|
3086
|
-
import { z as
|
|
3991
|
+
import { z as z13 } from "zod";
|
|
3087
3992
|
|
|
3088
3993
|
// apps/server/web-auth.ts
|
|
3089
3994
|
function cookieToken(request) {
|
|
@@ -3129,10 +4034,10 @@ function authorizeRead(store, request) {
|
|
|
3129
4034
|
// apps/server/ai/job-routes.ts
|
|
3130
4035
|
function registerAiJobRoutes(server, hub) {
|
|
3131
4036
|
const jobs = new AiJobs(hub);
|
|
3132
|
-
const id = (params) =>
|
|
4037
|
+
const id = (params) => z13.object({ id: z13.uuid() }).parse(params).id;
|
|
3133
4038
|
server.get("/ai/jobs", async (req) => {
|
|
3134
|
-
const session = authorizeWeb(hub, req), query =
|
|
3135
|
-
after:
|
|
4039
|
+
const session = authorizeWeb(hub, req), query = z13.object({
|
|
4040
|
+
after: z13.string().regex(/^\d{0,15}$/).default("")
|
|
3136
4041
|
}).parse(req.query);
|
|
3137
4042
|
return jobs.list(session.workspaceId, query.after);
|
|
3138
4043
|
});
|
|
@@ -3145,7 +4050,7 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3145
4050
|
async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
|
|
3146
4051
|
);
|
|
3147
4052
|
server.get("/ai/jobs/:id/events", async (req) => {
|
|
3148
|
-
const query =
|
|
4053
|
+
const query = z13.object({ after: z13.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
|
|
3149
4054
|
return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
|
|
3150
4055
|
});
|
|
3151
4056
|
server.post("/ai/jobs/:id/cancel", async (req) => {
|
|
@@ -3157,13 +4062,13 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3157
4062
|
return jobs.retry(
|
|
3158
4063
|
authorizeWeb(hub, req),
|
|
3159
4064
|
id(req.params),
|
|
3160
|
-
|
|
4065
|
+
z13.object({ operationId: z13.uuid() }).strict().parse(req.body).operationId
|
|
3161
4066
|
);
|
|
3162
4067
|
});
|
|
3163
4068
|
server.get("/ai/jobs/:id/source", async (req) => {
|
|
3164
4069
|
const session = authorizeWeb(hub, req), jobId = id(req.params);
|
|
3165
4070
|
jobs.get(session.workspaceId, jobId);
|
|
3166
|
-
const query =
|
|
4071
|
+
const query = z13.object({ path: z13.string().max(1024), startLine: z13.coerce.number().int().min(1).default(1) }).parse(req.query);
|
|
3167
4072
|
const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
|
|
3168
4073
|
if (!row) throw new HubError(404, "AI_JOB_NOT_FOUND", "Task not found.");
|
|
3169
4074
|
return new SnapshotReader(hub, session, JSON.parse(String(row.snapshot_json))).read(
|
|
@@ -3175,10 +4080,10 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3175
4080
|
}
|
|
3176
4081
|
|
|
3177
4082
|
// apps/server/ai/task-routes.ts
|
|
3178
|
-
import { z as
|
|
4083
|
+
import { z as z14 } from "zod";
|
|
3179
4084
|
function registerTaskRoutes(server, hub) {
|
|
3180
4085
|
const store = new TaskStore(hub);
|
|
3181
|
-
const id = (params) =>
|
|
4086
|
+
const id = (params) => z14.object({ id: z14.uuid() }).parse(params).id;
|
|
3182
4087
|
server.get("/tasks", async (req) => store.board(authorizeWeb(hub, req)));
|
|
3183
4088
|
server.post("/tasks/bootstrap", async (req) => {
|
|
3184
4089
|
webMutation(req);
|
|
@@ -3253,7 +4158,7 @@ function registerAiRoutes(server, store) {
|
|
|
3253
4158
|
profile,
|
|
3254
4159
|
config.secret(profile.id),
|
|
3255
4160
|
mode,
|
|
3256
|
-
AbortSignal.any([controller.signal, AbortSignal.timeout(
|
|
4161
|
+
AbortSignal.any([controller.signal, AbortSignal.timeout(3e5)])
|
|
3257
4162
|
);
|
|
3258
4163
|
} catch (e) {
|
|
3259
4164
|
if (e instanceof HubError) throw e;
|
|
@@ -3270,11 +4175,11 @@ function registerAiRoutes(server, store) {
|
|
|
3270
4175
|
}
|
|
3271
4176
|
|
|
3272
4177
|
// apps/server/static.ts
|
|
3273
|
-
import { join as
|
|
4178
|
+
import { join as join8 } from "node:path";
|
|
3274
4179
|
import fastifyStatic from "@fastify/static";
|
|
3275
4180
|
function registerWeb(server, root) {
|
|
3276
4181
|
server.register(fastifyStatic, {
|
|
3277
|
-
root:
|
|
4182
|
+
root: join8(root, "assets"),
|
|
3278
4183
|
prefix: "/assets/",
|
|
3279
4184
|
immutable: true,
|
|
3280
4185
|
maxAge: "1y",
|
|
@@ -3312,20 +4217,20 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
3312
4217
|
import Fastify from "fastify";
|
|
3313
4218
|
|
|
3314
4219
|
// packages/protocol/index.ts
|
|
3315
|
-
import { z as
|
|
4220
|
+
import { z as z15 } from "zod";
|
|
3316
4221
|
var PROTOCOL_VERSION = 1;
|
|
3317
|
-
var HealthSchema =
|
|
3318
|
-
var ServerInfoSchema =
|
|
3319
|
-
name:
|
|
3320
|
-
version:
|
|
3321
|
-
protocolVersion:
|
|
3322
|
-
capabilities:
|
|
4222
|
+
var HealthSchema = z15.object({ status: z15.literal("ok") });
|
|
4223
|
+
var ServerInfoSchema = z15.object({
|
|
4224
|
+
name: z15.literal("teammy"),
|
|
4225
|
+
version: z15.string().min(1),
|
|
4226
|
+
protocolVersion: z15.literal(PROTOCOL_VERSION),
|
|
4227
|
+
capabilities: z15.array(z15.string())
|
|
3323
4228
|
});
|
|
3324
|
-
var ErrorResponseSchema =
|
|
3325
|
-
error:
|
|
3326
|
-
code:
|
|
3327
|
-
message:
|
|
3328
|
-
requestId:
|
|
4229
|
+
var ErrorResponseSchema = z15.object({
|
|
4230
|
+
error: z15.object({
|
|
4231
|
+
code: z15.string(),
|
|
4232
|
+
message: z15.string(),
|
|
4233
|
+
requestId: z15.string()
|
|
3329
4234
|
})
|
|
3330
4235
|
});
|
|
3331
4236
|
|
|
@@ -3333,43 +4238,43 @@ var ErrorResponseSchema = z12.object({
|
|
|
3333
4238
|
import { ZodError } from "zod";
|
|
3334
4239
|
|
|
3335
4240
|
// packages/protocol/devices.ts
|
|
3336
|
-
import { z as
|
|
3337
|
-
var CredentialSchema =
|
|
3338
|
-
var EnrollmentSchema =
|
|
3339
|
-
operationId:
|
|
4241
|
+
import { z as z16 } from "zod";
|
|
4242
|
+
var CredentialSchema = z16.string().regex(/^tmy_[a-f0-9]{64}$/);
|
|
4243
|
+
var EnrollmentSchema = z16.object({
|
|
4244
|
+
operationId: z16.uuid(),
|
|
3340
4245
|
projectId: NameSchema,
|
|
3341
4246
|
memberId: NameSchema,
|
|
3342
4247
|
deviceId: DeviceIdSchema,
|
|
3343
4248
|
credential: CredentialSchema
|
|
3344
4249
|
}).strict();
|
|
3345
|
-
var EffectivePolicySchema =
|
|
3346
|
-
checkIntervalSeconds:
|
|
3347
|
-
fileSettleSeconds:
|
|
4250
|
+
var EffectivePolicySchema = z16.object({
|
|
4251
|
+
checkIntervalSeconds: z16.number().min(1).max(86400),
|
|
4252
|
+
fileSettleSeconds: z16.number().min(0).max(86400)
|
|
3348
4253
|
}).strict();
|
|
3349
|
-
var SyncSummarySchema =
|
|
3350
|
-
checkedAt:
|
|
3351
|
-
published:
|
|
3352
|
-
appliedFiles:
|
|
3353
|
-
pendingFiles:
|
|
3354
|
-
sessionSources:
|
|
3355
|
-
errors:
|
|
4254
|
+
var SyncSummarySchema = z16.object({
|
|
4255
|
+
checkedAt: z16.string(),
|
|
4256
|
+
published: z16.boolean(),
|
|
4257
|
+
appliedFiles: z16.number().int().nonnegative(),
|
|
4258
|
+
pendingFiles: z16.number().int().nonnegative(),
|
|
4259
|
+
sessionSources: z16.number().int().nonnegative(),
|
|
4260
|
+
errors: z16.array(z16.string().max(1e3)).max(20)
|
|
3356
4261
|
}).strict();
|
|
3357
|
-
var DeviceStatusSchema =
|
|
3358
|
-
clientVersion:
|
|
4262
|
+
var DeviceStatusSchema = z16.object({
|
|
4263
|
+
clientVersion: z16.string().max(64),
|
|
3359
4264
|
receivedRevision: RevisionSchema.nullable(),
|
|
3360
4265
|
appliedRevision: RevisionSchema.nullable(),
|
|
3361
4266
|
effectivePolicy: EffectivePolicySchema,
|
|
3362
|
-
overrides:
|
|
3363
|
-
configError:
|
|
3364
|
-
reportError:
|
|
4267
|
+
overrides: z16.array(z16.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
|
|
4268
|
+
configError: z16.string().max(2e3).nullable(),
|
|
4269
|
+
reportError: z16.string().max(2e3).nullable(),
|
|
3365
4270
|
lastSync: SyncSummarySchema.nullable()
|
|
3366
4271
|
}).strict();
|
|
3367
|
-
var WebDevicesSchema =
|
|
3368
|
-
devices:
|
|
3369
|
-
|
|
4272
|
+
var WebDevicesSchema = z16.object({
|
|
4273
|
+
devices: z16.array(
|
|
4274
|
+
z16.object({
|
|
3370
4275
|
memberId: NameSchema,
|
|
3371
|
-
bound:
|
|
3372
|
-
lastSeenAt:
|
|
4276
|
+
bound: z16.boolean(),
|
|
4277
|
+
lastSeenAt: z16.string().nullable(),
|
|
3373
4278
|
status: DeviceStatusSchema.nullable()
|
|
3374
4279
|
})
|
|
3375
4280
|
)
|
|
@@ -3407,7 +4312,7 @@ function registerControlRoutes(server, store) {
|
|
|
3407
4312
|
}
|
|
3408
4313
|
|
|
3409
4314
|
// apps/server/project-routes.ts
|
|
3410
|
-
import { z as
|
|
4315
|
+
import { z as z17 } from "zod";
|
|
3411
4316
|
function registerProjectRoutes(server, store) {
|
|
3412
4317
|
const project = new ProjectRepository(store), site = new SiteRepository(store);
|
|
3413
4318
|
server.get("/api/v1/projects", async (request) => ({
|
|
@@ -3454,7 +4359,7 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3454
4359
|
});
|
|
3455
4360
|
server.patch("/web/members/:id", async (request) => {
|
|
3456
4361
|
webMutation(request);
|
|
3457
|
-
const { id } =
|
|
4362
|
+
const { id } = z17.object({ id: NameSchema }).parse(request.params);
|
|
3458
4363
|
return project.updateMember(
|
|
3459
4364
|
authorizeWeb(store, request),
|
|
3460
4365
|
id,
|
|
@@ -3463,13 +4368,13 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3463
4368
|
});
|
|
3464
4369
|
server.post("/web/members/:id/device-reset", async (request) => {
|
|
3465
4370
|
webMutation(request);
|
|
3466
|
-
const { id } =
|
|
4371
|
+
const { id } = z17.object({ id: NameSchema }).parse(request.params);
|
|
3467
4372
|
return project.resetDevice(authorizeWeb(store, request), id);
|
|
3468
4373
|
});
|
|
3469
4374
|
}
|
|
3470
4375
|
|
|
3471
4376
|
// apps/server/site-routes.ts
|
|
3472
|
-
import { z as
|
|
4377
|
+
import { z as z18 } from "zod";
|
|
3473
4378
|
|
|
3474
4379
|
// apps/server/devices.ts
|
|
3475
4380
|
var DeviceRepository = class {
|
|
@@ -3487,7 +4392,7 @@ var DeviceRepository = class {
|
|
|
3487
4392
|
).get(projectId2, value.memberId);
|
|
3488
4393
|
if (!member)
|
|
3489
4394
|
throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
|
|
3490
|
-
const fingerprint =
|
|
4395
|
+
const fingerprint = digest(JSON.stringify(value));
|
|
3491
4396
|
const prior = this.hub.db.prepare(
|
|
3492
4397
|
"SELECT fingerprint FROM device_enrollments WHERE workspace_id=? AND member_id=? AND operation_id=?"
|
|
3493
4398
|
).get(projectId2, value.memberId, value.operationId);
|
|
@@ -3497,7 +4402,7 @@ var DeviceRepository = class {
|
|
|
3497
4402
|
"OPERATION_MISMATCH",
|
|
3498
4403
|
"Enrollment operation was reused with different content."
|
|
3499
4404
|
);
|
|
3500
|
-
const tokenHash =
|
|
4405
|
+
const tokenHash = digest(value.credential);
|
|
3501
4406
|
if (prior) {
|
|
3502
4407
|
if (member.token_hash !== tokenHash || member.device_id !== value.deviceId)
|
|
3503
4408
|
throw new HubError(
|
|
@@ -3545,7 +4450,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3545
4450
|
"/api/v1/web/session",
|
|
3546
4451
|
{ onRequest: async (request) => webMutation(request) },
|
|
3547
4452
|
async (request, reply) => {
|
|
3548
|
-
const { id } =
|
|
4453
|
+
const { id } = z18.object({ id: NameSchema }).strict().parse(request.body);
|
|
3549
4454
|
const { token, session } = site.login(id);
|
|
3550
4455
|
const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
|
|
3551
4456
|
reply.header(
|
|
@@ -3559,7 +4464,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3559
4464
|
server.patch("/api/v1/web/session", async (request) => {
|
|
3560
4465
|
webMutation(request);
|
|
3561
4466
|
const identity = site.identity(cookieToken(request));
|
|
3562
|
-
const { displayName } =
|
|
4467
|
+
const { displayName } = z18.object({ displayName: z18.string().trim().max(120) }).strict().parse(request.body);
|
|
3563
4468
|
return site.renameSelf(identity, displayName);
|
|
3564
4469
|
});
|
|
3565
4470
|
server.delete("/api/v1/web/session", async (request, reply) => {
|
|
@@ -3600,71 +4505,71 @@ function registerSiteRoutes(server, store) {
|
|
|
3600
4505
|
}
|
|
3601
4506
|
|
|
3602
4507
|
// apps/server/session-routes.ts
|
|
3603
|
-
import { z as
|
|
4508
|
+
import { z as z20 } from "zod";
|
|
3604
4509
|
|
|
3605
4510
|
// packages/protocol/sessions.ts
|
|
3606
|
-
import { z as
|
|
3607
|
-
var AgentSchema =
|
|
3608
|
-
var SessionSnapshotSchema =
|
|
3609
|
-
formatVersion:
|
|
3610
|
-
adapterVersion:
|
|
4511
|
+
import { z as z19 } from "zod";
|
|
4512
|
+
var AgentSchema = z19.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
|
|
4513
|
+
var SessionSnapshotSchema = z19.object({
|
|
4514
|
+
formatVersion: z19.literal(1),
|
|
4515
|
+
adapterVersion: z19.literal(1),
|
|
3611
4516
|
agent: AgentSchema,
|
|
3612
|
-
nativeId:
|
|
3613
|
-
cwd:
|
|
3614
|
-
messages:
|
|
3615
|
-
|
|
3616
|
-
id:
|
|
3617
|
-
role:
|
|
3618
|
-
text:
|
|
3619
|
-
source:
|
|
4517
|
+
nativeId: z19.string().min(1).max(512),
|
|
4518
|
+
cwd: z19.string().min(1).max(4096),
|
|
4519
|
+
messages: z19.array(
|
|
4520
|
+
z19.object({
|
|
4521
|
+
id: z19.string().min(1).max(1024),
|
|
4522
|
+
role: z19.enum(["user", "assistant"]),
|
|
4523
|
+
text: z19.string().min(1).max(2 * 1024 * 1024),
|
|
4524
|
+
source: z19.string().min(1).max(1024)
|
|
3620
4525
|
}).strict()
|
|
3621
4526
|
).max(5e4)
|
|
3622
4527
|
}).strict().refine(
|
|
3623
4528
|
(s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
|
|
3624
4529
|
"Duplicate message IDs."
|
|
3625
4530
|
);
|
|
3626
|
-
var SessionCommitSchema =
|
|
3627
|
-
operationId:
|
|
4531
|
+
var SessionCommitSchema = z19.object({
|
|
4532
|
+
operationId: z19.uuid(),
|
|
3628
4533
|
sessionKey: HashSchema,
|
|
3629
4534
|
parentRevision: RevisionSchema,
|
|
3630
4535
|
blob: BlobRefSchema
|
|
3631
4536
|
}).strict();
|
|
3632
|
-
var SessionInfoSchema =
|
|
4537
|
+
var SessionInfoSchema = z19.object({
|
|
3633
4538
|
owner: NameSchema,
|
|
3634
4539
|
sessionKey: HashSchema,
|
|
3635
4540
|
agent: AgentSchema,
|
|
3636
|
-
nativeId:
|
|
3637
|
-
cwd:
|
|
3638
|
-
title:
|
|
4541
|
+
nativeId: z19.string(),
|
|
4542
|
+
cwd: z19.string(),
|
|
4543
|
+
title: z19.string(),
|
|
3639
4544
|
revision: RevisionSchema,
|
|
3640
|
-
updatedAt:
|
|
3641
|
-
messageCount:
|
|
4545
|
+
updatedAt: z19.string(),
|
|
4546
|
+
messageCount: z19.number().int().nonnegative(),
|
|
3642
4547
|
blob: BlobRefSchema
|
|
3643
4548
|
});
|
|
3644
|
-
var SessionListSchema =
|
|
3645
|
-
sessions:
|
|
4549
|
+
var SessionListSchema = z19.object({
|
|
4550
|
+
sessions: z19.array(SessionInfoSchema),
|
|
3646
4551
|
nextCursor: RevisionSchema.nullable()
|
|
3647
4552
|
});
|
|
3648
|
-
var SessionStatsSchema =
|
|
3649
|
-
totalSessions:
|
|
3650
|
-
totalMessages:
|
|
3651
|
-
memberCount:
|
|
3652
|
-
updatedAt:
|
|
4553
|
+
var SessionStatsSchema = z19.object({
|
|
4554
|
+
totalSessions: z19.number().int().nonnegative(),
|
|
4555
|
+
totalMessages: z19.number().int().nonnegative(),
|
|
4556
|
+
memberCount: z19.number().int().nonnegative(),
|
|
4557
|
+
updatedAt: z19.string().nullable()
|
|
3653
4558
|
});
|
|
3654
|
-
var SessionScopeSchema =
|
|
4559
|
+
var SessionScopeSchema = z19.object({
|
|
3655
4560
|
owner: NameSchema.optional(),
|
|
3656
4561
|
sessionKey: HashSchema.optional()
|
|
3657
4562
|
}).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
|
|
3658
|
-
var SessionSearchSchema =
|
|
3659
|
-
matches:
|
|
3660
|
-
|
|
4563
|
+
var SessionSearchSchema = z19.object({
|
|
4564
|
+
matches: z19.array(
|
|
4565
|
+
z19.object({
|
|
3661
4566
|
owner: NameSchema,
|
|
3662
4567
|
sessionKey: HashSchema,
|
|
3663
4568
|
revision: RevisionSchema,
|
|
3664
|
-
messageId:
|
|
3665
|
-
role:
|
|
3666
|
-
snippet:
|
|
3667
|
-
title:
|
|
4569
|
+
messageId: z19.string(),
|
|
4570
|
+
role: z19.enum(["user", "assistant"]),
|
|
4571
|
+
snippet: z19.string(),
|
|
4572
|
+
title: z19.string()
|
|
3668
4573
|
})
|
|
3669
4574
|
),
|
|
3670
4575
|
nextCursor: RevisionSchema.nullable()
|
|
@@ -3835,21 +4740,21 @@ var SessionRepository = class {
|
|
|
3835
4740
|
nextCursor: rows.length > limit ? Number(rows[limit - 1].id) : null
|
|
3836
4741
|
};
|
|
3837
4742
|
}
|
|
3838
|
-
search(identity, query, after, limit,
|
|
3839
|
-
SessionScopeSchema.parse(
|
|
4743
|
+
search(identity, query, after, limit, scope2 = {}) {
|
|
4744
|
+
SessionScopeSchema.parse(scope2);
|
|
3840
4745
|
const lower = query.toLowerCase();
|
|
3841
4746
|
const indexed = [...lower].length >= 3;
|
|
3842
4747
|
const rows = this.hub.db.prepare(
|
|
3843
4748
|
`SELECT m.*,s.title,s.native_id FROM session_messages m JOIN sessions s USING(workspace_id,owner,session_key)
|
|
3844
4749
|
WHERE m.workspace_id=? AND m.id>? AND instr(m.search_text, ?)>0
|
|
3845
|
-
${
|
|
4750
|
+
${scope2.owner ? "AND m.owner=?" : ""} ${scope2.sessionKey ? "AND m.session_key=?" : ""}
|
|
3846
4751
|
${indexed ? "AND m.id IN (SELECT rowid FROM session_fts WHERE session_fts MATCH ?)" : ""} ORDER BY m.id LIMIT ?`
|
|
3847
4752
|
).all(
|
|
3848
4753
|
identity.workspaceId,
|
|
3849
4754
|
after,
|
|
3850
4755
|
lower,
|
|
3851
|
-
...
|
|
3852
|
-
...
|
|
4756
|
+
...scope2.owner ? [scope2.owner] : [],
|
|
4757
|
+
...scope2.sessionKey ? [scope2.sessionKey] : [],
|
|
3853
4758
|
...indexed ? [`"${lower.replaceAll('"', '""')}"`] : [],
|
|
3854
4759
|
limit + 1
|
|
3855
4760
|
);
|
|
@@ -3881,9 +4786,9 @@ function registerSessionRoutes(server, store) {
|
|
|
3881
4786
|
authorize(request);
|
|
3882
4787
|
}
|
|
3883
4788
|
};
|
|
3884
|
-
const pagination =
|
|
3885
|
-
after:
|
|
3886
|
-
limit:
|
|
4789
|
+
const pagination = z20.object({
|
|
4790
|
+
after: z20.coerce.number().pipe(RevisionSchema).default(0),
|
|
4791
|
+
limit: z20.coerce.number().int().min(1).max(100).default(50)
|
|
3887
4792
|
});
|
|
3888
4793
|
server.post(
|
|
3889
4794
|
"/sessions/commits",
|
|
@@ -3903,7 +4808,7 @@ function registerSessionRoutes(server, store) {
|
|
|
3903
4808
|
const q = pagination.extend({
|
|
3904
4809
|
owner: NameSchema.optional(),
|
|
3905
4810
|
sessionKey: HashSchema.optional(),
|
|
3906
|
-
q:
|
|
4811
|
+
q: z20.string().min(1).max(500).refine((s) => !s.includes("\0"))
|
|
3907
4812
|
}).parse(request.query);
|
|
3908
4813
|
return sessions.search(
|
|
3909
4814
|
authorizeRead(store, request),
|
|
@@ -3914,16 +4819,25 @@ function registerSessionRoutes(server, store) {
|
|
|
3914
4819
|
);
|
|
3915
4820
|
});
|
|
3916
4821
|
server.get("/sessions/:owner/:key", async (request) => {
|
|
3917
|
-
const p =
|
|
3918
|
-
const q =
|
|
4822
|
+
const p = z20.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
|
|
4823
|
+
const q = z20.object({ revision: z20.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
|
|
3919
4824
|
return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
|
|
3920
4825
|
});
|
|
3921
4826
|
}
|
|
3922
4827
|
|
|
3923
4828
|
// apps/server/file-routes.ts
|
|
3924
|
-
import { z as
|
|
4829
|
+
import { z as z21 } from "zod";
|
|
3925
4830
|
function registerFileRoutes(server, store) {
|
|
3926
4831
|
const files = new FileRepository(store);
|
|
4832
|
+
const shared = new SharedFilesRepository(store);
|
|
4833
|
+
server.get(
|
|
4834
|
+
"/project/shared-files",
|
|
4835
|
+
async (request) => shared.config(authorizeRead(store, request))
|
|
4836
|
+
);
|
|
4837
|
+
server.patch("/project/shared-files", async (request) => {
|
|
4838
|
+
webMutation(request);
|
|
4839
|
+
return shared.update(authorizeWeb(store, request), SharedFilesUpdateSchema.parse(request.body));
|
|
4840
|
+
});
|
|
3927
4841
|
const device = (request) => {
|
|
3928
4842
|
const value = request.headers["x-teammy-device"];
|
|
3929
4843
|
return typeof value === "string" ? value : void 0;
|
|
@@ -3943,13 +4857,13 @@ function registerFileRoutes(server, store) {
|
|
|
3943
4857
|
missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
|
|
3944
4858
|
}));
|
|
3945
4859
|
server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
|
|
3946
|
-
const { hash: hash2 } =
|
|
4860
|
+
const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
|
|
3947
4861
|
if (!Buffer.isBuffer(request.body))
|
|
3948
4862
|
throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
|
|
3949
4863
|
return files.upload(request.headers.authorization, device(request), hash2, request.body);
|
|
3950
4864
|
});
|
|
3951
4865
|
server.get("/blobs/:hash", async (request, reply) => {
|
|
3952
|
-
const { hash: hash2 } =
|
|
4866
|
+
const { hash: hash2 } = z21.object({ hash: HashSchema }).parse(request.params);
|
|
3953
4867
|
return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
|
|
3954
4868
|
});
|
|
3955
4869
|
server.post(
|
|
@@ -3961,12 +4875,16 @@ function registerFileRoutes(server, store) {
|
|
|
3961
4875
|
FileCommitSchema.parse(request.body)
|
|
3962
4876
|
)
|
|
3963
4877
|
);
|
|
4878
|
+
server.post("/files/_project/commits", guard, async (request) => {
|
|
4879
|
+
const { configRevision, ...batch } = SharedFileCommitSchema.parse(request.body);
|
|
4880
|
+
return files.commit(request.headers.authorization, device(request), batch, configRevision);
|
|
4881
|
+
});
|
|
3964
4882
|
server.get("/files/:owner/manifest", async (request) => {
|
|
3965
|
-
const { owner } =
|
|
3966
|
-
const query =
|
|
3967
|
-
revision:
|
|
3968
|
-
after:
|
|
3969
|
-
limit:
|
|
4883
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4884
|
+
const query = z21.object({
|
|
4885
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4886
|
+
after: z21.string().max(1024).default(""),
|
|
4887
|
+
limit: z21.coerce.number().int().min(1).max(500).default(200)
|
|
3970
4888
|
}).parse(request.query);
|
|
3971
4889
|
return files.manifest(
|
|
3972
4890
|
authorizeRead(store, request),
|
|
@@ -3977,28 +4895,28 @@ function registerFileRoutes(server, store) {
|
|
|
3977
4895
|
);
|
|
3978
4896
|
});
|
|
3979
4897
|
server.get("/files/:owner/entry", async (request) => {
|
|
3980
|
-
const { owner } =
|
|
3981
|
-
const q =
|
|
4898
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4899
|
+
const q = z21.object({
|
|
3982
4900
|
path: FilePathSchema,
|
|
3983
|
-
revision:
|
|
4901
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
|
|
3984
4902
|
}).parse(request.query);
|
|
3985
4903
|
return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
|
|
3986
4904
|
});
|
|
3987
4905
|
server.get("/files/:owner/tree", async (request) => {
|
|
3988
|
-
const { owner } =
|
|
3989
|
-
const q =
|
|
3990
|
-
path:
|
|
3991
|
-
revision:
|
|
3992
|
-
after:
|
|
3993
|
-
limit:
|
|
4906
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4907
|
+
const q = z21.object({
|
|
4908
|
+
path: z21.union([z21.literal(""), FilePathSchema]).default(""),
|
|
4909
|
+
revision: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4910
|
+
after: z21.string().max(255).default(""),
|
|
4911
|
+
limit: z21.coerce.number().int().min(1).max(200).default(100)
|
|
3994
4912
|
}).parse(request.query);
|
|
3995
4913
|
return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
|
|
3996
4914
|
});
|
|
3997
4915
|
server.get("/files/:owner/changes", async (request) => {
|
|
3998
|
-
const { owner } =
|
|
3999
|
-
const query =
|
|
4000
|
-
after:
|
|
4001
|
-
limit:
|
|
4916
|
+
const { owner } = z21.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4917
|
+
const query = z21.object({
|
|
4918
|
+
after: z21.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
4919
|
+
limit: z21.coerce.number().int().min(1).max(100).default(100)
|
|
4002
4920
|
}).parse(request.query);
|
|
4003
4921
|
return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
|
|
4004
4922
|
});
|
|
@@ -4050,6 +4968,7 @@ function buildServer(options) {
|
|
|
4050
4968
|
"identity",
|
|
4051
4969
|
"blobs",
|
|
4052
4970
|
"file-revisions",
|
|
4971
|
+
"project-files",
|
|
4053
4972
|
"session-revisions",
|
|
4054
4973
|
"session-search",
|
|
4055
4974
|
"project-config",
|
|
@@ -4066,14 +4985,14 @@ function buildServer(options) {
|
|
|
4066
4985
|
registerSiteRoutes(server, store);
|
|
4067
4986
|
registerProjectRoutes(server, store);
|
|
4068
4987
|
registerAiRoutes(server, store);
|
|
4069
|
-
const projectRoutes = async (
|
|
4070
|
-
registerProjectScopeRoutes(
|
|
4071
|
-
registerAiJobRoutes(
|
|
4072
|
-
registerTaskRoutes(
|
|
4073
|
-
registerControlRoutes(
|
|
4074
|
-
registerFileRoutes(
|
|
4075
|
-
registerSessionRoutes(
|
|
4076
|
-
|
|
4988
|
+
const projectRoutes = async (scope2) => {
|
|
4989
|
+
registerProjectScopeRoutes(scope2, store);
|
|
4990
|
+
registerAiJobRoutes(scope2, store);
|
|
4991
|
+
registerTaskRoutes(scope2, store);
|
|
4992
|
+
registerControlRoutes(scope2, store);
|
|
4993
|
+
registerFileRoutes(scope2, store);
|
|
4994
|
+
registerSessionRoutes(scope2, store);
|
|
4995
|
+
scope2.get("/members", async (request) => ({
|
|
4077
4996
|
members: store.members(authorizeDevice(store, request))
|
|
4078
4997
|
}));
|
|
4079
4998
|
};
|
|
@@ -4090,7 +5009,7 @@ function buildServer(options) {
|
|
|
4090
5009
|
|
|
4091
5010
|
// apps/server/main.ts
|
|
4092
5011
|
import { homedir } from "node:os";
|
|
4093
|
-
import { join as
|
|
5012
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
4094
5013
|
var help = `teammy-server \u2014 the Teammy collaboration hub
|
|
4095
5014
|
|
|
4096
5015
|
Usage: teammy-server [--host HOST] [--port PORT]
|
|
@@ -4115,13 +5034,13 @@ Open the server URL to initialize the Web dashboard and preset member IDs.
|
|
|
4115
5034
|
File synchronization, session search and manual AI analysis are available.
|
|
4116
5035
|
AI tasks require a separate ai-worker process using the same data directory.
|
|
4117
5036
|
`;
|
|
4118
|
-
async function runServerCli(args,
|
|
5037
|
+
async function runServerCli(args, version2) {
|
|
4119
5038
|
try {
|
|
4120
5039
|
if (args[0] === "ai-worker") {
|
|
4121
5040
|
const { values: values2 } = parseArgs({
|
|
4122
5041
|
args: args.slice(1),
|
|
4123
5042
|
options: {
|
|
4124
|
-
"data-dir": { type: "string", default:
|
|
5043
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") },
|
|
4125
5044
|
once: { type: "boolean" }
|
|
4126
5045
|
}
|
|
4127
5046
|
});
|
|
@@ -4137,7 +5056,7 @@ async function runServerCli(args, version) {
|
|
|
4137
5056
|
options: {
|
|
4138
5057
|
workspace: { type: "string" },
|
|
4139
5058
|
member: { type: "string" },
|
|
4140
|
-
"data-dir": { type: "string", default:
|
|
5059
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
|
|
4141
5060
|
}
|
|
4142
5061
|
});
|
|
4143
5062
|
if (!values2.workspace || !values2.member)
|
|
@@ -4162,7 +5081,7 @@ async function runServerCli(args, version) {
|
|
|
4162
5081
|
version: { type: "boolean", short: "v" },
|
|
4163
5082
|
host: { type: "string", default: "127.0.0.1" },
|
|
4164
5083
|
port: { type: "string", default: "4317" },
|
|
4165
|
-
"data-dir": { type: "string", default:
|
|
5084
|
+
"data-dir": { type: "string", default: join9(homedir(), ".teammy", "server") }
|
|
4166
5085
|
}
|
|
4167
5086
|
});
|
|
4168
5087
|
if (values.help) {
|
|
@@ -4170,7 +5089,7 @@ async function runServerCli(args, version) {
|
|
|
4170
5089
|
return 0;
|
|
4171
5090
|
}
|
|
4172
5091
|
if (values.version) {
|
|
4173
|
-
process.stdout.write(`${
|
|
5092
|
+
process.stdout.write(`${version2}
|
|
4174
5093
|
`);
|
|
4175
5094
|
return 0;
|
|
4176
5095
|
}
|
|
@@ -4180,10 +5099,10 @@ async function runServerCli(args, version) {
|
|
|
4180
5099
|
}
|
|
4181
5100
|
if (!values.host?.trim()) throw new Error("Host must not be empty.");
|
|
4182
5101
|
const server = buildServer({
|
|
4183
|
-
version,
|
|
5102
|
+
version: version2,
|
|
4184
5103
|
logger: true,
|
|
4185
5104
|
dataDir: resolve3(values["data-dir"]),
|
|
4186
|
-
webDir:
|
|
5105
|
+
webDir: fileURLToPath3(new URL("./web/", import.meta.url))
|
|
4187
5106
|
});
|
|
4188
5107
|
try {
|
|
4189
5108
|
await server.listen({ host: values.host, port: Number(portText) });
|