@teammy/server 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,15 +1,92 @@
1
- // apps/server/ai/worker.ts
2
- import { setTimeout as delay } from "node:timers/promises";
1
+ // apps/server/ai/snapshots.ts
2
+ import { createHash as createHash4 } from "node:crypto";
3
3
 
4
- // apps/server/store.ts
5
- import { createHash, randomBytes } from "node:crypto";
6
- import { join } from "node:path";
7
- import { existsSync } from "node:fs";
8
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
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(dirname(path), { recursive: true, mode: 448 });
105
+ mkdirSync(dirname2(path), { recursive: true, mode: 448 });
29
106
  const db = new DatabaseSync(path);
30
107
  try {
31
108
  chmodSync(path, 384);
@@ -48,17 +125,71 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
48
125
  }
49
126
  }
50
127
 
51
- // packages/protocol/identity.ts
128
+ // packages/protocol/files.ts
52
129
  import { z } from "zod";
53
- var NameSchema = z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
54
- var DeviceIdSchema = z.uuid();
55
- var BindRequestSchema = z.object({ deviceId: DeviceIdSchema, projectId: NameSchema.optional() }).strict();
56
- var BindingSchema = z.object({
57
- workspaceId: NameSchema,
58
- memberId: NameSchema,
59
- deviceId: DeviceIdSchema
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 MembersSchema = z.object({ members: z.array(NameSchema) });
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
- // apps/server/session-schema.ts
76
- var sessionMigration = String.raw`
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
- // apps/server/device-schema.ts
112
- var deviceMigration = String.raw`
113
- 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));
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
- // apps/server/project-schema.ts
117
- var projectMigration = String.raw`
118
- ALTER TABLE members ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
119
- ALTER TABLE members ADD COLUMN role TEXT NOT NULL DEFAULT 'member';
120
- ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
121
- ALTER TABLE members ADD COLUMN member_revision INTEGER NOT NULL DEFAULT 1;
122
- UPDATE members SET enabled=0 WHERE token_hash IS NULL;
123
- 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);
124
- CREATE TABLE web_sessions(token_hash TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, expires_at INTEGER NOT NULL);
125
- `;
126
- var multiProjectMigration = String.raw`
127
- DROP TABLE project_settings;
128
- DROP TABLE web_sessions;
129
- ALTER TABLE members DROP COLUMN display_name;
130
- CREATE TABLE users (
131
- id TEXT PRIMARY KEY, display_name TEXT NOT NULL DEFAULT '',
132
- site_role TEXT NOT NULL DEFAULT 'member', enabled INTEGER NOT NULL DEFAULT 1,
133
- revision INTEGER NOT NULL DEFAULT 1
134
- );
135
- CREATE TABLE projects (
136
- id TEXT PRIMARY KEY, config_json TEXT NOT NULL,
137
- operation_id TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL
138
- );
139
- CREATE TABLE site_settings (
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/file-schema.ts
167
- var fileMigration = String.raw`
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 z3 } from "zod";
445
- var ServerUrlSchema = z3.string().url().max(2048).refine((value) => {
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 = z3.object({
450
- checkIntervalSeconds: z3.number().int().min(1).max(86400),
451
- fileSettleSeconds: z3.number().int().min(0).max(86400)
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 = z3.object({
455
- schemaVersion: z3.literal(1),
262
+ var ProjectConfigSchema = z4.object({
263
+ schemaVersion: z4.literal(1),
456
264
  projectId: NameSchema,
457
- projectName: z3.string().trim().min(1).max(120),
458
- description: z3.string().max(2e3),
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: z3.literal("workspace-and-sessions-v1"),
269
+ scopeProfile: z4.literal("workspace-and-sessions-v1"),
462
270
  sync: SyncPolicySchema
463
271
  }).strict();
464
- var SetupSchema = z3.object({
465
- operationId: z3.uuid(),
466
- projectName: z3.string().trim().min(1).max(120),
467
- description: z3.string().max(2e3).default(""),
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: z3.array(NameSchema).max(100).default([]),
278
+ members: z4.array(NameSchema).max(100).default([]),
471
279
  sync: SyncPolicySchema.default(DEFAULT_POLICY)
472
280
  }).strict();
473
- var SetupStatusSchema = z3.object({
474
- initialized: z3.boolean()
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 = z3.object({
288
+ var MemberSchema = z4.object({
481
289
  id: NameSchema,
482
- displayName: z3.string().max(120),
483
- enabled: z3.boolean(),
484
- accountEnabled: z3.boolean(),
290
+ displayName: z4.string().max(120),
291
+ enabled: z4.boolean(),
292
+ accountEnabled: z4.boolean(),
485
293
  revision: RevisionSchema,
486
- bound: z3.boolean(),
294
+ bound: z4.boolean(),
487
295
  fileRevision: RevisionSchema,
488
- lastPublishedAt: z3.string().nullable()
296
+ lastPublishedAt: z4.string().nullable()
489
297
  });
490
- var WebIdentitySchema = z3.object({
298
+ var WebIdentitySchema = z4.object({
491
299
  memberId: NameSchema,
492
- displayName: z3.string(),
493
- siteRole: z3.enum(["admin", "member"])
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 = z3.object({
306
+ var MemberCreateSchema = z4.object({
499
307
  id: NameSchema
500
308
  }).strict();
501
- var MemberUpdateSchema = z3.object({
309
+ var MemberUpdateSchema = z4.object({
502
310
  expectedRevision: RevisionSchema,
503
- displayName: z3.string().trim().max(120).optional(),
504
- enabled: z3.boolean().optional()
311
+ displayName: z4.string().trim().max(120).optional(),
312
+ enabled: z4.boolean().optional()
505
313
  }).strict();
506
- var ProjectCreateSchema = z3.object({
507
- operationId: z3.uuid(),
508
- projectName: z3.string().trim().min(1).max(120),
509
- description: z3.string().max(2e3).default("")
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 = z3.object({
319
+ var ProjectSummarySchema = z4.object({
512
320
  projectId: NameSchema,
513
- projectName: z3.string(),
514
- description: z3.string()
321
+ projectName: z4.string(),
322
+ description: z4.string()
515
323
  });
516
- var ProjectsSchema = z3.object({ projects: z3.array(ProjectSummarySchema) });
517
- var SiteConfigSchema = z3.object({
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 = z3.object({
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 = z3.object({
338
+ var UserSchema = z4.object({
531
339
  id: NameSchema,
532
- displayName: z3.string(),
533
- siteRole: z3.enum(["admin", "member"]),
534
- enabled: z3.boolean(),
340
+ displayName: z4.string(),
341
+ siteRole: z4.enum(["admin", "member"]),
342
+ enabled: z4.boolean(),
535
343
  revision: RevisionSchema
536
344
  });
537
- var UserCreateSchema = z3.object({
345
+ var UserCreateSchema = z4.object({
538
346
  id: NameSchema,
539
- displayName: z3.string().trim().max(120).default(""),
540
- siteRole: z3.enum(["admin", "member"]).default("member")
347
+ displayName: z4.string().trim().max(120).default(""),
348
+ siteRole: z4.enum(["admin", "member"]).default("member")
541
349
  }).strict();
542
- var UserUpdateSchema = z3.object({
350
+ var UserUpdateSchema = z4.object({
543
351
  expectedRevision: RevisionSchema,
544
- displayName: z3.string().trim().max(120).optional(),
545
- enabled: z3.boolean().optional(),
546
- siteRole: z3.enum(["admin", "member"]).optional()
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 as randomBytes2, createHash as createHash2 } from "node:crypto";
745
- var digest2 = (value) => createHash2("sha256").update(value).digest("hex");
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 = randomBytes2(32).toString("hex");
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(digest2(token), id, Date.now() + 7 * 864e5);
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(digest2(token), Date.now()) : void 0;
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(digest2(token));
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 = digest2(JSON.stringify([creator, value]));
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-${randomBytes3(8).toString("hex")}`,
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 = digest2(JSON.stringify(value));
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/ai/config.ts
1088
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
1089
- import { readFileSync, writeFileSync } from "node:fs";
1090
- import { join as join2 } from "node:path";
1091
- var AiConfig = class {
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
- key(create = false) {
1097
- const path = join2(this.hub.directory, "ai-secret.key");
1098
- if (create) {
1099
- try {
1100
- writeFileSync(path, randomBytes4(32), { flag: "wx", mode: 384 });
1101
- } catch (e) {
1102
- if (e.code !== "EEXIST") throw e;
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
- // apps/server/ai/defaults.ts
1160
- var AiDefaults = class {
1161
- constructor(hub) {
1162
- this.hub = hub;
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
- "AI_CONFIG_CONFLICT",
1196
- "Model default changed; reload before saving."
735
+ "STALE_CONFIG",
736
+ "Shared file settings changed; reload before saving."
1197
737
  );
1198
- if (input.profileId && !new AiConfig(this.hub).get(input.profileId).enabled)
1199
- throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1200
- this.hub.db.prepare(
1201
- `INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
1202
- ).run(scope, input.profileId, input.expectedRevision + 1);
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(join4(hub.directory, "blobs"));
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 (createHash4("sha256").update(bytes).digest("hex") !== HashSchema.parse(hash2))
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 = createHash4("sha256").update(JSON.stringify({ parentRevision: batch.parentRevision, changes })).digest("hex");
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 { workspaceId: workspace, memberId: owner } = identity;
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
- this.hub.db.prepare("UPDATE members SET file_revision=? WHERE workspace_id=? AND member_id=?").run(revision, workspace, owner);
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 createHash5("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
1040
+ return createHash4("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
1572
1041
  }
1573
- function captureSnapshot(hub, identity, selection, allowEmpty = false) {
1042
+ function captureScope(hub, identity, selection, allowEmpty = false) {
1574
1043
  const repository = new FileRepository(hub);
1575
1044
  const files = [];
1576
1045
  let cursor = "";
@@ -1600,158 +1069,832 @@ function captureSnapshot(hub, identity, selection, allowEmpty = false) {
1600
1069
  fingerprint: snapshotDigest(files)
1601
1070
  };
1602
1071
  }
1603
- var SnapshotReader = class {
1604
- constructor(hub, identity, snapshot, budget = 6e4) {
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 = 6e4, sharedBudget) {
1093
+ this.hub = hub;
1094
+ this.identity = identity;
1095
+ this.snapshot = snapshot;
1096
+ this.budget = sharedBudget ?? { remaining: budget };
1097
+ }
1098
+ hub;
1099
+ identity;
1100
+ snapshot;
1101
+ readFiles = /* @__PURE__ */ new Set();
1102
+ lineCounts = /* @__PURE__ */ new Map();
1103
+ readRanges = /* @__PURE__ */ new Map();
1104
+ limitations = /* @__PURE__ */ new Set();
1105
+ budget;
1106
+ commonReader;
1107
+ projectReader() {
1108
+ const project = this.snapshot.project;
1109
+ if (!project)
1110
+ throw new HubError(403, "AI_SCOPE_VIOLATION", "No project reference snapshot is available.");
1111
+ return this.commonReader ??= new _SnapshotReader(
1112
+ this.hub,
1113
+ this.identity,
1114
+ { ...project, owner: PROJECT_FILE_OWNER, kind: "directory", path: "" },
1115
+ 0,
1116
+ this.budget
1117
+ );
1118
+ }
1119
+ afterIndex(after) {
1120
+ if (!after) return 0;
1121
+ const index = this.snapshot.files.findIndex((f) => f.path === after);
1122
+ if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
1123
+ return index + 1;
1124
+ }
1125
+ fullyRead(path) {
1126
+ let end = 0;
1127
+ for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
1128
+ if (range[0] > end + 1) break;
1129
+ end = Math.max(end, range[1]);
1130
+ }
1131
+ return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
1132
+ }
1133
+ list(after = "", limit = 50) {
1134
+ const entries = this.snapshot.files.slice(
1135
+ this.afterIndex(after),
1136
+ this.afterIndex(after) + limit + 1
1137
+ );
1138
+ return {
1139
+ files: entries.slice(0, limit).map((f) => ({ path: f.path, size: f.size })),
1140
+ total: this.snapshot.files.length,
1141
+ nextCursor: entries.length > limit ? entries[limit - 1].path : null
1142
+ };
1143
+ }
1144
+ source(path) {
1145
+ const file = this.snapshot.files.find((entry) => entry.path === path);
1146
+ if (!file || !this.readFiles.has(path))
1147
+ throw new HubError(
1148
+ 422,
1149
+ "AI_TASK_SOURCE_INVALID",
1150
+ "Task source must be an actually read snapshot file."
1151
+ );
1152
+ const row = this.hub.db.prepare(
1153
+ "SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
1154
+ ).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
1155
+ if (!row)
1156
+ throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
1157
+ return { ...file, syncedAt: String(row.created_at) };
1158
+ }
1159
+ async text(path) {
1160
+ const entry = this.snapshot.files.find((f) => f.path === path);
1161
+ if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
1162
+ if (entry.size > 256 * 1024)
1163
+ throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
1164
+ const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
1165
+ if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
1166
+ throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
1167
+ let text;
1168
+ try {
1169
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1170
+ } catch {
1171
+ throw new HubError(422, "AI_TEXT_REQUIRED", "Only UTF-8 text files can be analysed.");
1172
+ }
1173
+ if (text.includes("\0"))
1174
+ throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
1175
+ this.lineCounts.set(path, text.split("\n").length);
1176
+ return text;
1177
+ }
1178
+ async read(path, startLine = 1, limit = 150) {
1179
+ const text = await this.text(path), lines = text.split("\n");
1180
+ if (startLine > lines.length)
1181
+ throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
1182
+ const selected = [];
1183
+ let size = 0;
1184
+ for (const line of lines.slice(startLine - 1, startLine - 1 + limit)) {
1185
+ const cost = Buffer.byteLength(line) + 1;
1186
+ if (size + cost > Math.min(12e3, this.budget.remaining)) break;
1187
+ selected.push(line);
1188
+ size += cost;
1189
+ }
1190
+ if (!selected.length) {
1191
+ this.limitations.add("\u8BFB\u53D6\u9884\u7B97\u6216\u5355\u884C\u957F\u5EA6\u9650\u5236\u963B\u6B62\u4E86\u7EE7\u7EED\u8BFB\u53D6\u3002");
1192
+ throw new HubError(413, "AI_READ_BUDGET", "Read budget exceeded or line is too long.");
1193
+ }
1194
+ this.budget.remaining -= size;
1195
+ this.readFiles.add(path);
1196
+ const endLine = startLine + selected.length - 1;
1197
+ this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
1198
+ return {
1199
+ path,
1200
+ startLine,
1201
+ endLine,
1202
+ totalLines: lines.length,
1203
+ content: selected.map((line, i) => `${startLine + i}: ${line}`).join("\n"),
1204
+ hasMore: endLine < lines.length
1205
+ };
1206
+ }
1207
+ async whole(path, budget) {
1208
+ const text = await this.text(path);
1209
+ if (Buffer.byteLength(text) > budget)
1210
+ throw new HubError(
1211
+ 413,
1212
+ "AI_CONTEXT_LIMIT",
1213
+ "File is too large for this model budget; use directory analysis to read portions."
1214
+ );
1215
+ this.readFiles.add(path);
1216
+ this.readRanges.set(path, [[1, text.split("\n").length]]);
1217
+ return text;
1218
+ }
1219
+ async search(query, after = "", limit = 50) {
1220
+ const files = this.snapshot.files.slice(this.afterIndex(after), this.afterIndex(after) + limit);
1221
+ const matches = [];
1222
+ let scannedBytes = 0;
1223
+ for (const file of files) {
1224
+ if (file.size > 256 * 1024) {
1225
+ this.limitations.add("\u90E8\u5206\u5927\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
1226
+ continue;
1227
+ }
1228
+ if (scannedBytes + file.size > 1024 * 1024) {
1229
+ return {
1230
+ matches,
1231
+ nextCursor: files[files.indexOf(file) - 1]?.path ?? after,
1232
+ limited: true
1233
+ };
1234
+ }
1235
+ scannedBytes += file.size;
1236
+ try {
1237
+ const lines = (await this.text(file.path)).split("\n");
1238
+ for (let i = 0; i < lines.length; i++)
1239
+ if (lines[i].toLowerCase().includes(query.toLowerCase())) {
1240
+ matches.push({ path: file.path, line: i + 1, excerpt: lines[i].slice(0, 400) });
1241
+ if (matches.length >= 30) return { matches, nextCursor: file.path, limited: true };
1242
+ }
1243
+ } catch (e) {
1244
+ if (e instanceof HubError && e.code === "AI_TEXT_REQUIRED") {
1245
+ this.limitations.add("\u90E8\u5206\u975E\u6587\u672C\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
1246
+ continue;
1247
+ }
1248
+ throw e;
1249
+ }
1250
+ }
1251
+ return {
1252
+ matches,
1253
+ nextCursor: files.length === limit ? files.at(-1).path : null,
1254
+ limited: false
1255
+ };
1256
+ }
1257
+ };
1258
+
1259
+ // apps/server/ai/worker.ts
1260
+ import { setTimeout as delay } from "node:timers/promises";
1261
+
1262
+ // apps/server/store.ts
1263
+ import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
1264
+ import { join as join3 } from "node:path";
1265
+ import { existsSync } from "node:fs";
1266
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
1267
+
1268
+ // apps/server/session-schema.ts
1269
+ var sessionMigration = String.raw`
1270
+ CREATE TABLE sessions (
1271
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1272
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
1273
+ agent TEXT NOT NULL, native_id TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL,
1274
+ revision INTEGER NOT NULL, updated_at TEXT NOT NULL, message_count INTEGER NOT NULL,
1275
+ hash TEXT NOT NULL, size INTEGER NOT NULL,
1276
+ UNIQUE(workspace_id, owner, session_key)
1277
+ );
1278
+ CREATE INDEX session_listing ON sessions(workspace_id, id);
1279
+ CREATE TABLE session_revisions (
1280
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
1281
+ revision INTEGER NOT NULL, info_json TEXT NOT NULL,
1282
+ PRIMARY KEY(workspace_id, owner, session_key, revision)
1283
+ );
1284
+ CREATE TABLE session_operations (
1285
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
1286
+ fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
1287
+ PRIMARY KEY(workspace_id, owner, operation_id)
1288
+ );
1289
+ CREATE TABLE session_messages (
1290
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1291
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
1292
+ revision INTEGER NOT NULL, message_id TEXT NOT NULL, role TEXT NOT NULL, body TEXT NOT NULL, search_text TEXT NOT NULL
1293
+ );
1294
+ CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
1295
+ CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
1296
+ `;
1297
+ var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
1298
+
1299
+ // apps/server/control-schema.ts
1300
+ var controlMigration = String.raw`
1301
+ 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));
1302
+ `;
1303
+
1304
+ // apps/server/device-schema.ts
1305
+ var deviceMigration = String.raw`
1306
+ 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));
1307
+ `;
1308
+
1309
+ // apps/server/project-schema.ts
1310
+ var projectMigration = String.raw`
1311
+ ALTER TABLE members ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
1312
+ ALTER TABLE members ADD COLUMN role TEXT NOT NULL DEFAULT 'member';
1313
+ ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
1314
+ ALTER TABLE members ADD COLUMN member_revision INTEGER NOT NULL DEFAULT 1;
1315
+ UPDATE members SET enabled=0 WHERE token_hash IS NULL;
1316
+ 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);
1317
+ CREATE TABLE web_sessions(token_hash TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, expires_at INTEGER NOT NULL);
1318
+ `;
1319
+ var multiProjectMigration = String.raw`
1320
+ DROP TABLE project_settings;
1321
+ DROP TABLE web_sessions;
1322
+ ALTER TABLE members DROP COLUMN display_name;
1323
+ CREATE TABLE users (
1324
+ id TEXT PRIMARY KEY, display_name TEXT NOT NULL DEFAULT '',
1325
+ site_role TEXT NOT NULL DEFAULT 'member', enabled INTEGER NOT NULL DEFAULT 1,
1326
+ revision INTEGER NOT NULL DEFAULT 1
1327
+ );
1328
+ CREATE TABLE projects (
1329
+ id TEXT PRIMARY KEY, config_json TEXT NOT NULL,
1330
+ operation_id TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL
1331
+ );
1332
+ CREATE TABLE site_settings (
1333
+ id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
1334
+ setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
1335
+ );
1336
+ CREATE TABLE web_sessions (
1337
+ token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
1338
+ );
1339
+ DROP TABLE ai_profiles;
1340
+ CREATE TABLE ai_profiles (
1341
+ id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
1342
+ );
1343
+ `;
1344
+ var sitePolicyMigration = `
1345
+ UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
1346
+ (SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
1347
+ '{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
1348
+ )));
1349
+ UPDATE projects SET config_json=json_set(config_json,
1350
+ '$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
1351
+ '$.configRevision',json_extract(config_json,'$.configRevision')+1)
1352
+ WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
1353
+ `;
1354
+ var equalMembershipMigration = `
1355
+ ALTER TABLE members DROP COLUMN role;
1356
+ ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
1357
+ `;
1358
+
1359
+ // apps/server/file-schema.ts
1360
+ var fileMigration = String.raw`
1361
+ ALTER TABLE members ADD COLUMN file_revision INTEGER NOT NULL DEFAULT 0;
1362
+ CREATE TABLE workspace_blobs (workspace_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(workspace_id, hash));
1363
+ CREATE TABLE file_operations (
1364
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
1365
+ fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
1366
+ PRIMARY KEY(workspace_id, owner, operation_id)
1367
+ );
1368
+ CREATE TABLE file_revisions (
1369
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, revision INTEGER NOT NULL,
1370
+ operation_id TEXT NOT NULL, created_at TEXT NOT NULL, changes_json TEXT NOT NULL,
1371
+ PRIMARY KEY(workspace_id, owner, revision)
1372
+ );
1373
+ CREATE TABLE current_files (
1374
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, path_key TEXT NOT NULL,
1375
+ hash TEXT NOT NULL, size INTEGER NOT NULL, revision INTEGER NOT NULL,
1376
+ PRIMARY KEY(workspace_id, owner, path), UNIQUE(workspace_id, owner, path_key)
1377
+ );
1378
+ CREATE TABLE file_events (
1379
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, revision INTEGER NOT NULL,
1380
+ hash TEXT, size INTEGER,
1381
+ PRIMARY KEY(workspace_id, owner, path, revision)
1382
+ );
1383
+ `;
1384
+
1385
+ // apps/server/ai/task-schema.ts
1386
+ var taskBoardMigration = `
1387
+ ALTER TABLE ai_jobs ADD COLUMN runtime_json TEXT;
1388
+ ALTER TABLE ai_jobs ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
1389
+ CREATE TABLE task_board_members (
1390
+ workspace_id TEXT NOT NULL, owner TEXT NOT NULL, job_id TEXT NOT NULL REFERENCES ai_jobs(id),
1391
+ PRIMARY KEY(workspace_id,owner)
1392
+ );
1393
+ CREATE TABLE task_cards (
1394
+ id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, owner TEXT NOT NULL,
1395
+ job_id TEXT NOT NULL REFERENCES ai_jobs(id), fields_json TEXT NOT NULL,
1396
+ sources_json TEXT NOT NULL, synced_at TEXT NOT NULL, correction_json TEXT,
1397
+ active INTEGER NOT NULL DEFAULT 1, needs_review INTEGER NOT NULL DEFAULT 0
1398
+ );
1399
+ CREATE INDEX task_cards_scope ON task_cards(workspace_id,owner,active);
1400
+ `;
1401
+ var taskMaintenanceMigration = `
1402
+ ALTER TABLE ai_jobs ADD COLUMN task_context_json TEXT;
1403
+ ALTER TABLE task_board_members ADD COLUMN applied_job_id TEXT REFERENCES ai_jobs(id);
1404
+ UPDATE task_board_members SET applied_job_id=(
1405
+ SELECT j.id FROM ai_jobs j
1406
+ WHERE j.workspace_id=task_board_members.workspace_id AND j.status='succeeded'
1407
+ AND json_extract(j.request_json,'$.workflow')='task-bootstrap'
1408
+ AND json_extract(j.snapshot_json,'$.owner')=task_board_members.owner
1409
+ ORDER BY j.rowid DESC LIMIT 1
1410
+ );
1411
+ `;
1412
+
1413
+ // apps/server/ai/schema.ts
1414
+ var aiConfigMigration = `
1415
+ CREATE TABLE ai_profiles (
1416
+ workspace_id TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL,
1417
+ config_json TEXT NOT NULL, secret TEXT,
1418
+ PRIMARY KEY(workspace_id,id)
1419
+ );
1420
+ `;
1421
+ var aiJobsMigration = `
1422
+ CREATE TABLE ai_jobs (
1423
+ id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, requester TEXT NOT NULL,
1424
+ request_json TEXT NOT NULL, profile_json TEXT NOT NULL, snapshot_json TEXT NOT NULL,
1425
+ prompt_version TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, recovery_count INTEGER NOT NULL DEFAULT 0,
1426
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, run_token TEXT, lease_until INTEGER,
1427
+ result_json TEXT, error TEXT
1428
+ );
1429
+ CREATE INDEX ai_jobs_queue ON ai_jobs(status,created_at);
1430
+ CREATE INDEX ai_jobs_workspace ON ai_jobs(workspace_id,id);
1431
+ 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));
1432
+ 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));
1433
+ 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);
1434
+ CREATE INDEX ai_events_job ON ai_events(job_id,seq);
1435
+ `;
1436
+ var aiDefaultsMigration = `
1437
+ CREATE TABLE ai_defaults (
1438
+ scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
1439
+ );
1440
+ `;
1441
+
1442
+ // apps/server/store.ts
1443
+ function digest2(token) {
1444
+ return createHash5("sha256").update(token).digest("hex");
1445
+ }
1446
+ var migrations = [
1447
+ String.raw`
1448
+ CREATE TABLE members (
1449
+ workspace_id TEXT NOT NULL,
1450
+ member_id TEXT NOT NULL,
1451
+ token_hash TEXT UNIQUE,
1452
+ device_id TEXT,
1453
+ PRIMARY KEY (workspace_id, member_id)
1454
+ );
1455
+ `,
1456
+ fileMigration,
1457
+ sessionMigration,
1458
+ projectMigration,
1459
+ deviceMigration,
1460
+ controlMigration,
1461
+ aiConfigMigration,
1462
+ aiJobsMigration,
1463
+ multiProjectMigration,
1464
+ aiDefaultsMigration,
1465
+ sitePolicyMigration,
1466
+ sessionOwnerIndexMigration,
1467
+ equalMembershipMigration,
1468
+ taskBoardMigration,
1469
+ taskMaintenanceMigration,
1470
+ sharedFilesMigration
1471
+ ];
1472
+ var HubStore = class {
1473
+ constructor(directory) {
1474
+ this.directory = directory;
1475
+ const path = join3(directory, "hub.sqlite3");
1476
+ if (existsSync(path)) {
1477
+ const existing = new DatabaseSync2(path, { readOnly: true });
1478
+ try {
1479
+ const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
1480
+ if (version > 0 && version < 9)
1481
+ throw new Error(
1482
+ "Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
1483
+ );
1484
+ } finally {
1485
+ existing.close();
1486
+ }
1487
+ }
1488
+ this.db = openDatabase(path, migrations);
1489
+ }
1490
+ directory;
1491
+ db;
1492
+ close() {
1493
+ this.db.close();
1494
+ }
1495
+ issueMember(workspaceId, memberId, reset = false) {
1496
+ NameSchema.parse(workspaceId);
1497
+ NameSchema.parse(memberId);
1498
+ const token = `tmy_${randomBytes3(32).toString("hex")}`;
1499
+ transaction(this.db, () => {
1500
+ this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
1501
+ const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
1502
+ if (exists && !reset)
1503
+ throw new HubError(
1504
+ 409,
1505
+ "MEMBER_EXISTS",
1506
+ "Member exists; use member reset to replace its credential and device binding."
1507
+ );
1508
+ if (!exists && reset) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
1509
+ this.db.prepare(
1510
+ `INSERT INTO members (workspace_id, member_id, token_hash) VALUES (?, ?, ?)
1511
+ ON CONFLICT(workspace_id, member_id) DO UPDATE SET token_hash=excluded.token_hash, device_id=NULL, enabled=1, member_revision=member_revision+1`
1512
+ ).run(workspaceId, memberId, digest2(token));
1513
+ });
1514
+ return token;
1515
+ }
1516
+ revokeMember(workspaceId, memberId) {
1517
+ const result = this.db.prepare(
1518
+ "UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
1519
+ ).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
1520
+ if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
1521
+ this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
1522
+ }
1523
+ identity(authorization) {
1524
+ const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
1525
+ const row = match && this.db.prepare(
1526
+ "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"
1527
+ ).get(digest2(match[1]));
1528
+ if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
1529
+ return {
1530
+ workspaceId: String(row.workspace_id),
1531
+ memberId: String(row.member_id),
1532
+ deviceId: row.device_id
1533
+ };
1534
+ }
1535
+ bind(authorization, deviceId, projectId2) {
1536
+ return transaction(this.db, () => {
1537
+ const identity = this.identity(authorization);
1538
+ if (projectId2 && projectId2 !== identity.workspaceId)
1539
+ throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
1540
+ if (identity.deviceId && identity.deviceId !== deviceId)
1541
+ throw new HubError(
1542
+ 409,
1543
+ "DEVICE_BOUND",
1544
+ "This member is bound to another device. An administrator must reset the binding."
1545
+ );
1546
+ this.db.prepare("UPDATE members SET device_id=? WHERE workspace_id=? AND member_id=?").run(deviceId, identity.workspaceId, identity.memberId);
1547
+ return BindingSchema.parse({ ...identity, deviceId });
1548
+ });
1549
+ }
1550
+ authorize(authorization, deviceId) {
1551
+ const identity = this.identity(authorization);
1552
+ if (!identity.deviceId || identity.deviceId !== deviceId)
1553
+ throw new HubError(
1554
+ 403,
1555
+ "DEVICE_MISMATCH",
1556
+ "Connect this device before using workspace APIs."
1557
+ );
1558
+ return BindingSchema.parse(identity);
1559
+ }
1560
+ members(identity) {
1561
+ 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));
1562
+ }
1563
+ };
1564
+
1565
+ // apps/server/ai/jobs.ts
1566
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
1567
+
1568
+ // packages/protocol/ai.ts
1569
+ import { z as z6 } from "zod";
1570
+
1571
+ // packages/protocol/tasks.ts
1572
+ import { z as z5 } from "zod";
1573
+ var TaskStatusSchema = z5.enum(["planned", "in_progress", "in_review", "completed"]);
1574
+ var TaskFieldsSchema = z5.object({
1575
+ title: z5.string().trim().min(1).max(200),
1576
+ goal: z5.string().trim().min(1).max(2e3).nullable(),
1577
+ status: TaskStatusSchema.nullable(),
1578
+ progress: z5.string().trim().min(1).max(2e3)
1579
+ }).strict();
1580
+ var TaskSourceSchema = z5.object({
1581
+ path: FilePathSchema,
1582
+ sha256: HashSchema,
1583
+ revision: RevisionSchema,
1584
+ syncedAt: z5.string()
1585
+ });
1586
+ var TaskCardSchema = TaskFieldsSchema.extend({
1587
+ id: z5.uuid(),
1588
+ owner: NameSchema,
1589
+ jobId: z5.uuid(),
1590
+ sources: z5.array(TaskSourceSchema).min(1),
1591
+ syncedAt: z5.string(),
1592
+ corrected: z5.boolean(),
1593
+ needsReview: z5.boolean()
1594
+ });
1595
+ var TaskCorrectionSchema = z5.object({
1596
+ title: TaskFieldsSchema.shape.title.optional(),
1597
+ status: TaskStatusSchema.nullable().optional()
1598
+ }).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
1599
+ var TaskBoardSchema = z5.object({
1600
+ tasks: z5.array(TaskCardSchema),
1601
+ members: z5.array(
1602
+ z5.object({
1603
+ owner: NameSchema,
1604
+ jobId: z5.uuid().nullable(),
1605
+ status: z5.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
1606
+ error: z5.string().nullable(),
1607
+ sourceChanged: z5.boolean(),
1608
+ partial: z5.boolean(),
1609
+ uncertainties: z5.array(z5.string())
1610
+ })
1611
+ ),
1612
+ modelAvailable: z5.boolean()
1613
+ });
1614
+ var TaskBootstrapRequestSchema = z5.object({ operationId: z5.uuid(), owner: NameSchema }).strict();
1615
+ var TaskBootstrapResponseSchema = z5.object({
1616
+ jobId: z5.uuid().nullable(),
1617
+ empty: z5.boolean()
1618
+ });
1619
+
1620
+ // packages/protocol/ai.ts
1621
+ var AiProfileSchema = z6.object({
1622
+ id: NameSchema,
1623
+ name: z6.string().trim().min(1).max(120),
1624
+ protocol: z6.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
1625
+ baseUrl: ServerUrlSchema,
1626
+ model: z6.string().trim().min(1).max(200),
1627
+ enabled: z6.boolean().default(true),
1628
+ contextWindow: z6.number().int().min(4096).max(2e6).default(32e3),
1629
+ maxOutputTokens: z6.number().int().min(128).max(16e3).default(2048)
1630
+ }).strict().refine(
1631
+ (p) => p.maxOutputTokens < p.contextWindow,
1632
+ "Output budget must be smaller than context window."
1633
+ );
1634
+ var AiPublicProfileSchema = AiProfileSchema.safeExtend({
1635
+ revision: z6.number().int(),
1636
+ keyConfigured: z6.boolean()
1637
+ });
1638
+ var AiProfileUpdateSchema = z6.object({
1639
+ expectedRevision: z6.number().int().nonnegative(),
1640
+ profile: AiProfileSchema,
1641
+ apiKey: z6.string().min(1).max(4096).nullable().optional()
1642
+ }).strict();
1643
+ var AiProfilesSchema = z6.object({ profiles: z6.array(AiPublicProfileSchema) });
1644
+ var AiDefaultUpdateSchema = z6.object({
1645
+ expectedRevision: z6.number().int().nonnegative(),
1646
+ profileId: NameSchema.nullable()
1647
+ }).strict();
1648
+ var AiDefaultSchema = z6.object({
1649
+ revision: z6.number().int().nonnegative(),
1650
+ profileId: NameSchema.nullable(),
1651
+ effectiveProfileId: NameSchema.nullable(),
1652
+ source: z6.enum(["site", "project", "none"]),
1653
+ available: z6.boolean()
1654
+ });
1655
+ var AiProbeSchema = z6.object({ mode: z6.enum(["text", "tools", "json"]) }).strict();
1656
+ var AiProbeResultSchema = z6.object({
1657
+ ok: z6.literal(true),
1658
+ mode: AiProbeSchema.shape.mode,
1659
+ streamed: z6.boolean(),
1660
+ inputTokens: z6.number(),
1661
+ outputTokens: z6.number()
1662
+ });
1663
+ var AiSelectionSchema = z6.object({
1664
+ owner: NameSchema,
1665
+ kind: z6.enum(["file", "directory"]),
1666
+ path: z6.union([z6.literal(""), FilePathSchema]),
1667
+ revision: z6.number().int().nonnegative().optional()
1668
+ }).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
1669
+ var AiJobCreateSchema = z6.object({
1670
+ operationId: z6.uuid(),
1671
+ profileId: NameSchema.optional(),
1672
+ workflow: z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
1673
+ selection: AiSelectionSchema,
1674
+ question: z6.string().trim().max(2e3).default("")
1675
+ }).strict();
1676
+ var AiCitationSchema = z6.object({
1677
+ path: FilePathSchema,
1678
+ startLine: z6.number().int().positive(),
1679
+ endLine: z6.number().int().positive(),
1680
+ sha256: HashSchema
1681
+ });
1682
+ var AiCoverageSchema = z6.object({
1683
+ totalFiles: z6.number().int(),
1684
+ readFiles: z6.array(FilePathSchema),
1685
+ partial: z6.boolean(),
1686
+ limitations: z6.array(z6.string())
1687
+ });
1688
+ var AiResultSchema = z6.object({
1689
+ summary: z6.string().min(1).max(2e4),
1690
+ findings: z6.array(z6.string().max(2e3)).max(30),
1691
+ uncertainties: z6.array(z6.string().max(2e3)).max(30),
1692
+ citations: z6.array(AiCitationSchema).max(50),
1693
+ coverage: AiCoverageSchema
1694
+ });
1695
+ var TaskAnalysisResultSchema = z6.object({
1696
+ kind: z6.literal("task-bootstrap"),
1697
+ tasks: z6.array(
1698
+ TaskFieldsSchema.extend({
1699
+ taskId: z6.uuid().nullable().optional(),
1700
+ sources: z6.array(TaskSourceSchema).min(1).max(50)
1701
+ })
1702
+ ).max(100),
1703
+ uncertainties: z6.array(z6.string().max(2e3)).max(50),
1704
+ coverage: AiCoverageSchema
1705
+ });
1706
+ var AiWorkflowResultSchema = z6.union([AiResultSchema, TaskAnalysisResultSchema]);
1707
+ var AiUsageSchema = z6.object({
1708
+ inputTokens: z6.number().nonnegative(),
1709
+ outputTokens: z6.number().nonnegative()
1710
+ });
1711
+ var AiJobSchema = z6.object({
1712
+ id: z6.uuid(),
1713
+ requester: NameSchema,
1714
+ workflow: AiJobCreateSchema.shape.workflow,
1715
+ question: z6.string(),
1716
+ profileId: NameSchema,
1717
+ model: z6.string(),
1718
+ profileRevision: z6.number().int(),
1719
+ promptVersion: z6.string(),
1720
+ origin: z6.enum(["manual", "automatic"]).default("manual"),
1721
+ status: z6.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
1722
+ createdAt: z6.string(),
1723
+ updatedAt: z6.string(),
1724
+ attemptCount: z6.number().int(),
1725
+ error: z6.string().nullable(),
1726
+ input: z6.object({
1727
+ owner: NameSchema,
1728
+ kind: z6.enum(["file", "directory"]),
1729
+ path: z6.string(),
1730
+ revision: z6.number().int(),
1731
+ fileCount: z6.number().int(),
1732
+ fingerprint: HashSchema
1733
+ }),
1734
+ result: AiWorkflowResultSchema.nullable(),
1735
+ sourceChanged: z6.boolean()
1736
+ });
1737
+ var AiJobsSchema = z6.object({
1738
+ jobs: z6.array(AiJobSchema),
1739
+ nextCursor: z6.string().nullable()
1740
+ });
1741
+ var AiEventSchema = z6.object({
1742
+ seq: z6.number().int(),
1743
+ attempt: z6.number().int(),
1744
+ kind: z6.string(),
1745
+ message: z6.string(),
1746
+ createdAt: z6.string()
1747
+ });
1748
+ var AiEventsSchema = z6.object({
1749
+ events: z6.array(AiEventSchema),
1750
+ nextCursor: z6.number().int()
1751
+ });
1752
+ var AiAttemptSchema = z6.object({
1753
+ number: z6.number().int(),
1754
+ status: z6.string(),
1755
+ startedAt: z6.string(),
1756
+ endedAt: z6.string().nullable(),
1757
+ error: z6.string().nullable(),
1758
+ usage: AiUsageSchema.nullable()
1759
+ });
1760
+ var AiJobDetailSchema = z6.object({ job: AiJobSchema, attempts: z6.array(AiAttemptSchema) });
1761
+
1762
+ // apps/server/ai/config.ts
1763
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
1764
+ import { readFileSync, writeFileSync } from "node:fs";
1765
+ import { join as join4 } from "node:path";
1766
+ var AiConfig = class {
1767
+ constructor(hub) {
1768
+ this.hub = hub;
1769
+ }
1770
+ hub;
1771
+ key(create = false) {
1772
+ const path = join4(this.hub.directory, "ai-secret.key");
1773
+ if (create) {
1774
+ try {
1775
+ writeFileSync(path, randomBytes4(32), { flag: "wx", mode: 384 });
1776
+ } catch (e) {
1777
+ if (e.code !== "EEXIST") throw e;
1778
+ }
1779
+ }
1780
+ const key = readFileSync(path);
1781
+ if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
1782
+ return key;
1783
+ }
1784
+ encrypt(value, id) {
1785
+ const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
1786
+ cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
1787
+ const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
1788
+ return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
1789
+ }
1790
+ list() {
1791
+ return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
1792
+ ...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
1793
+ revision: Number(row.revision),
1794
+ keyConfigured: !!row.secret
1795
+ }));
1796
+ }
1797
+ get(id) {
1798
+ const profile = this.list().find((p) => p.id === id);
1799
+ if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
1800
+ const { keyConfigured: _, ...result } = profile;
1801
+ return result;
1802
+ }
1803
+ secret(id) {
1804
+ const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
1805
+ if (!row?.secret) return "";
1806
+ const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
1807
+ const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
1808
+ cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
1809
+ cipher.setAuthTag(tag);
1810
+ return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
1811
+ }
1812
+ save(session, value) {
1813
+ requireSiteAdmin(session);
1814
+ const input = AiProfileUpdateSchema.parse(value), { profile } = input;
1815
+ return transaction(this.hub.db, () => {
1816
+ const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
1817
+ if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
1818
+ throw new HubError(
1819
+ 409,
1820
+ "AI_CONFIG_CONFLICT",
1821
+ "Model configuration changed; reload before saving."
1822
+ );
1823
+ if (!old && this.list().length >= 20)
1824
+ throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
1825
+ const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
1826
+ this.hub.db.prepare(
1827
+ "INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
1828
+ ).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
1829
+ return this.list().find((p) => p.id === profile.id);
1830
+ });
1831
+ }
1832
+ };
1833
+
1834
+ // apps/server/ai/defaults.ts
1835
+ var AiDefaults = class {
1836
+ constructor(hub) {
1605
1837
  this.hub = hub;
1606
- this.identity = identity;
1607
- this.snapshot = snapshot;
1608
- this.remaining = budget;
1609
1838
  }
1610
1839
  hub;
1611
- identity;
1612
- snapshot;
1613
- readFiles = /* @__PURE__ */ new Set();
1614
- lineCounts = /* @__PURE__ */ new Map();
1615
- readRanges = /* @__PURE__ */ new Map();
1616
- limitations = /* @__PURE__ */ new Set();
1617
- remaining;
1618
- afterIndex(after) {
1619
- if (!after) return 0;
1620
- const index = this.snapshot.files.findIndex((f) => f.path === after);
1621
- if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
1622
- return index + 1;
1623
- }
1624
- fullyRead(path) {
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);
1840
+ read(scope) {
1841
+ const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
1842
+ return {
1843
+ profileId: row?.profile_id ? String(row.profile_id) : null,
1844
+ revision: Number(row?.revision ?? 0)
1845
+ };
1631
1846
  }
1632
- list(after = "", limit = 50) {
1633
- const entries = this.snapshot.files.slice(
1634
- this.afterIndex(after),
1635
- this.afterIndex(after) + limit + 1
1636
- );
1847
+ view(projectId2) {
1848
+ const site = this.read("site");
1849
+ const own = projectId2 ? this.read(`project:${projectId2}`) : site;
1850
+ const effectiveProfileId = own.profileId ?? site.profileId;
1637
1851
  return {
1638
- files: entries.slice(0, limit).map((f) => ({ path: f.path, size: f.size })),
1639
- total: this.snapshot.files.length,
1640
- nextCursor: entries.length > limit ? entries[limit - 1].path : null
1852
+ ...own,
1853
+ effectiveProfileId,
1854
+ source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
1855
+ available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
1641
1856
  };
1642
1857
  }
1643
- source(path) {
1644
- const file = this.snapshot.files.find((entry) => entry.path === path);
1645
- if (!file || !this.readFiles.has(path))
1646
- throw new HubError(
1647
- 422,
1648
- "AI_TASK_SOURCE_INVALID",
1649
- "Task source must be an actually read snapshot file."
1650
- );
1651
- const row = this.hub.db.prepare(
1652
- "SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
1653
- ).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
1654
- if (!row)
1655
- throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
1656
- return { ...file, syncedAt: String(row.created_at) };
1858
+ site() {
1859
+ return this.view();
1657
1860
  }
1658
- async text(path) {
1659
- const entry = this.snapshot.files.find((f) => f.path === path);
1660
- 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
- const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
1664
- if (bytes.byteLength !== entry.size || createHash5("sha256").update(bytes).digest("hex") !== entry.sha256)
1665
- throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
1666
- let text;
1667
- try {
1668
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1669
- } catch {
1670
- throw new HubError(422, "AI_TEXT_REQUIRED", "Only UTF-8 text files can be analysed.");
1671
- }
1672
- if (text.includes("\0"))
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;
1861
+ project(projectId2) {
1862
+ return this.view(projectId2);
1676
1863
  }
1677
- async read(path, startLine = 1, limit = 150) {
1678
- const text = await this.text(path), lines = text.split("\n");
1679
- if (startLine > lines.length)
1680
- throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
1681
- const selected = [];
1682
- let size = 0;
1683
- for (const line of lines.slice(startLine - 1, startLine - 1 + limit)) {
1684
- const cost = Buffer.byteLength(line) + 1;
1685
- if (size + cost > Math.min(12e3, this.remaining)) break;
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;
1694
- this.readFiles.add(path);
1695
- const endLine = startLine + selected.length - 1;
1696
- this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
1697
- return {
1698
- path,
1699
- startLine,
1700
- endLine,
1701
- totalLines: lines.length,
1702
- content: selected.map((line, i) => `${startLine + i}: ${line}`).join("\n"),
1703
- hasMore: endLine < lines.length
1704
- };
1864
+ save(scope, value) {
1865
+ const input = AiDefaultUpdateSchema.parse(value);
1866
+ transaction(this.hub.db, () => {
1867
+ if (this.read(scope).revision !== input.expectedRevision)
1868
+ throw new HubError(
1869
+ 409,
1870
+ "AI_CONFIG_CONFLICT",
1871
+ "Model default changed; reload before saving."
1872
+ );
1873
+ if (input.profileId && !new AiConfig(this.hub).get(input.profileId).enabled)
1874
+ throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1875
+ this.hub.db.prepare(
1876
+ `INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
1877
+ ).run(scope, input.profileId, input.expectedRevision + 1);
1878
+ });
1705
1879
  }
1706
- async whole(path, budget) {
1707
- const text = await this.text(path);
1708
- if (Buffer.byteLength(text) > budget)
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
- );
1714
- this.readFiles.add(path);
1715
- this.readRanges.set(path, [[1, text.split("\n").length]]);
1716
- return text;
1880
+ saveSite(identity, value) {
1881
+ requireSiteAdmin(identity);
1882
+ this.save("site", value);
1883
+ return this.site();
1717
1884
  }
1718
- async search(query, after = "", limit = 50) {
1719
- const files = this.snapshot.files.slice(this.afterIndex(after), this.afterIndex(after) + limit);
1720
- const matches = [];
1721
- let scannedBytes = 0;
1722
- for (const file of files) {
1723
- if (file.size > 256 * 1024) {
1724
- this.limitations.add("\u90E8\u5206\u5927\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
1725
- continue;
1726
- }
1727
- if (scannedBytes + file.size > 1024 * 1024) {
1728
- return {
1729
- matches,
1730
- nextCursor: files[files.indexOf(file) - 1]?.path ?? after,
1731
- limited: true
1732
- };
1733
- }
1734
- scannedBytes += file.size;
1735
- try {
1736
- const lines = (await this.text(file.path)).split("\n");
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
- }
1742
- } catch (e) {
1743
- if (e instanceof HubError && e.code === "AI_TEXT_REQUIRED") {
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;
1748
- }
1749
- }
1750
- return {
1751
- matches,
1752
- nextCursor: files.length === limit ? files.at(-1).path : null,
1753
- limited: false
1754
- };
1885
+ saveProject(identity, value) {
1886
+ new ProjectRepository(this.hub).requireMember(identity);
1887
+ this.save(`project:${identity.workspaceId}`, value);
1888
+ return this.project(identity.workspaceId);
1889
+ }
1890
+ resolve(projectId2, explicit) {
1891
+ const id = explicit ?? this.project(projectId2).effectiveProfileId;
1892
+ if (!id)
1893
+ throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
1894
+ const profile = new AiConfig(this.hub).get(id);
1895
+ if (!profile.enabled)
1896
+ throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
1897
+ return profile;
1755
1898
  }
1756
1899
  };
1757
1900
 
@@ -1760,32 +1903,32 @@ import { createHash as createHash6 } from "node:crypto";
1760
1903
  import { readFileSync as readFileSync2 } from "node:fs";
1761
1904
  import { resolve as resolve2 } from "node:path";
1762
1905
  import { fileURLToPath } from "node:url";
1763
- import { z as z6 } from "zod";
1764
- var WorkflowNameSchema = z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
1765
- var ToolNameSchema = z6.enum(["list_files", "read_file", "search_files"]);
1766
- var ToolTextSchema = z6.object({ label: z6.string().min(1).max(100), description: z6.string().min(1).max(2e3) }).strict();
1767
- var RuntimeConfigSchema = z6.object({
1768
- version: z6.number().int().positive(),
1769
- thinkingLevel: z6.enum(["off", "minimal", "low", "medium", "high"]),
1770
- tools: z6.array(ToolNameSchema).max(3).refine((v) => new Set(v).size === v.length),
1771
- maxRounds: z6.number().int().min(1).max(100),
1772
- maxToolCalls: z6.number().int().min(1).max(500),
1773
- timeoutMs: z6.number().int().min(1e3).max(9e5),
1774
- requestTimeoutMs: z6.number().int().min(1e3).max(12e4),
1775
- maxReadBytes: z6.number().int().min(1024).max(2e6),
1776
- settleSeconds: z6.number().int().min(0).max(3600).optional()
1906
+ import { z as z7 } from "zod";
1907
+ var WorkflowNameSchema = z7.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
1908
+ var ToolNameSchema = z7.enum(["list_files", "read_file", "search_files"]);
1909
+ var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1).max(2e3) }).strict();
1910
+ var RuntimeConfigSchema = z7.object({
1911
+ version: z7.number().int().positive(),
1912
+ thinkingLevel: z7.enum(["off", "minimal", "low", "medium", "high"]),
1913
+ tools: z7.array(ToolNameSchema).max(3).refine((v) => new Set(v).size === v.length),
1914
+ maxRounds: z7.number().int().min(1).max(100),
1915
+ maxToolCalls: z7.number().int().min(1).max(500),
1916
+ timeoutMs: z7.number().int().min(1e3).max(9e5),
1917
+ requestTimeoutMs: z7.number().int().min(1e3).max(12e4),
1918
+ maxReadBytes: z7.number().int().min(1024).max(2e6),
1919
+ settleSeconds: z7.number().int().min(0).max(3600).optional()
1777
1920
  }).strict();
1778
- var WorkflowRuntimeSchema = z6.object({
1921
+ var WorkflowRuntimeSchema = z7.object({
1779
1922
  workflow: WorkflowNameSchema,
1780
- system: z6.string().min(1).max(32e3),
1781
- prompt: z6.string().min(1).max(32e3),
1923
+ system: z7.string().min(1).max(32e3),
1924
+ prompt: z7.string().min(1).max(32e3),
1782
1925
  config: RuntimeConfigSchema,
1783
- toolText: z6.object({
1926
+ toolText: z7.object({
1784
1927
  list_files: ToolTextSchema,
1785
1928
  read_file: ToolTextSchema,
1786
1929
  search_files: ToolTextSchema
1787
1930
  }).strict(),
1788
- fingerprint: z6.string().regex(/^[a-f0-9]{64}$/)
1931
+ fingerprint: z7.string().regex(/^[a-f0-9]{64}$/)
1789
1932
  }).strict();
1790
1933
  function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
1791
1934
  try {
@@ -1823,7 +1966,7 @@ function workflowPrompt(runtime, schema) {
1823
1966
  ${runtime.prompt}
1824
1967
 
1825
1968
  JSON Schema:
1826
- ${JSON.stringify(z6.toJSONSchema(schema))}`;
1969
+ ${JSON.stringify(z7.toJSONSchema(schema))}`;
1827
1970
  }
1828
1971
 
1829
1972
  // apps/server/ai/access.ts
@@ -1853,8 +1996,7 @@ function applyTaskResult(hub, job, result) {
1853
1996
  const current = hub.db.prepare("SELECT job_id FROM task_board_members WHERE workspace_id=? AND owner=?").get(job.workspace, job.snapshot.owner);
1854
1997
  if (current?.job_id !== job.id)
1855
1998
  throw new HubError(409, "AI_TASK_SUPERSEDED", "A newer workspace analysis exists.");
1856
- const head = hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(job.workspace, job.snapshot.owner);
1857
- if (Number(head?.file_revision) !== job.snapshot.revision)
1999
+ if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
1858
2000
  throw new HubError(
1859
2001
  409,
1860
2002
  "AI_SOURCE_CHANGED",
@@ -1954,7 +2096,7 @@ var TaskStore = class {
1954
2096
  jobId: row.job_id ?? null,
1955
2097
  status: row.status ?? (row.has_files ? "waiting" : "empty"),
1956
2098
  error: row.error ?? null,
1957
- sourceChanged: snapshot ? snapshot.revision !== Number(row.file_revision) : false,
2099
+ sourceChanged: snapshot ? !taskSnapshotCurrent(this.hub, session.workspaceId, snapshot) : false,
1958
2100
  partial: result?.coverage.partial ?? false,
1959
2101
  uncertainties: result ? [...result.uncertainties, ...result.coverage.limitations] : []
1960
2102
  };
@@ -2023,7 +2165,10 @@ var AiJobs = class {
2023
2165
  this.hub,
2024
2166
  { workspaceId: String(row.workspace_id), memberId: String(row.requester) },
2025
2167
  { owner: snapshot.owner, kind: snapshot.kind, path: snapshot.path },
2026
- request.workflow === "task-bootstrap"
2168
+ {
2169
+ allowEmpty: request.workflow === "task-bootstrap",
2170
+ projectReferences: request.workflow === "task-bootstrap"
2171
+ }
2027
2172
  ).fingerprint !== snapshot.fingerprint;
2028
2173
  } catch {
2029
2174
  sourceChanged = true;
@@ -2144,18 +2289,21 @@ var AiJobs = class {
2144
2289
  "Task bootstrap uses the current complete member workspace without an additional question."
2145
2290
  );
2146
2291
  const id = this.operation(session, input.operationId, { action: "create", ...input }, () => {
2147
- const snapshot = captureSnapshot(
2148
- this.hub,
2149
- session,
2150
- input.selection,
2151
- input.workflow === "task-bootstrap"
2152
- );
2292
+ const snapshot = captureSnapshot(this.hub, session, input.selection, {
2293
+ allowEmpty: input.workflow === "task-bootstrap",
2294
+ projectReferences: input.workflow === "task-bootstrap"
2295
+ });
2153
2296
  if (input.workflow === "task-bootstrap") {
2154
2297
  if (origin === "automatic") {
2155
2298
  const published = this.hub.db.prepare(
2156
- "SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
2157
- ).get(session.workspaceId, snapshot.owner, snapshot.revision);
2158
- if (!published || (scheduledAt ?? Date.now()) - Date.parse(String(published.created_at)) < resource.config.settleSeconds * 1e3)
2299
+ "SELECT MAX(created_at) created_at FROM file_revisions WHERE workspace_id=? AND ((owner=? AND revision=?) OR (owner='_project' AND revision=?))"
2300
+ ).get(
2301
+ session.workspaceId,
2302
+ snapshot.owner,
2303
+ snapshot.revision,
2304
+ snapshot.project?.revision ?? 0
2305
+ );
2306
+ if (!published?.created_at || (scheduledAt ?? Date.now()) - Date.parse(String(published.created_at)) < resource.config.settleSeconds * 1e3)
2159
2307
  throw new HubError(
2160
2308
  409,
2161
2309
  "AI_TASK_UNSETTLED",
@@ -2169,7 +2317,7 @@ var AiJobs = class {
2169
2317
  if (latest) {
2170
2318
  const previous = JSON.parse(String(latest.snapshot_json));
2171
2319
  if (["queued", "running"].includes(String(latest.status))) {
2172
- if (origin === "manual" && previous.revision === snapshot.revision)
2320
+ if (origin === "manual" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
2173
2321
  return String(latest.id);
2174
2322
  throw new HubError(
2175
2323
  409,
@@ -2177,7 +2325,7 @@ var AiJobs = class {
2177
2325
  "This member already has an analysis in progress."
2178
2326
  );
2179
2327
  }
2180
- if (origin === "automatic" && previous.revision === snapshot.revision)
2328
+ if (origin === "automatic" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
2181
2329
  throw new HubError(
2182
2330
  409,
2183
2331
  "AI_TASK_CURRENT",
@@ -2368,7 +2516,7 @@ import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi
2368
2516
 
2369
2517
  // apps/server/ai/tools.ts
2370
2518
  import { Type } from "@earendil-works/pi-ai";
2371
- import { z as z7 } from "zod";
2519
+ import { z as z8 } from "zod";
2372
2520
  function analysisTools(reader, signal, progress, runtime) {
2373
2521
  let calls = 0;
2374
2522
  const guard = () => {
@@ -2387,15 +2535,21 @@ function analysisTools(reader, signal, progress, runtime) {
2387
2535
  name: "list_files",
2388
2536
  ...runtime.toolText.list_files,
2389
2537
  parameters: Type.Object(
2390
- { after: Type.Optional(Type.String({ maxLength: 1024 })) },
2538
+ {
2539
+ after: Type.Optional(Type.String({ maxLength: 1024 })),
2540
+ scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")]))
2541
+ },
2391
2542
  { additionalProperties: false }
2392
2543
  ),
2393
2544
  executionMode: "sequential",
2394
2545
  async execute(_id, params) {
2395
2546
  guard();
2396
- const p = z7.object({ after: z7.string().max(1024).default("") }).strict().parse(params);
2547
+ const p = z8.object({
2548
+ after: z8.string().max(1024).default(""),
2549
+ scope: z8.enum(["member", "project"]).default("member")
2550
+ }).strict().parse(params);
2397
2551
  progress("list", "\u6D4F\u89C8\u6587\u4EF6\u6E05\u5355\u3002");
2398
- return reply(reader.list(p.after, 30));
2552
+ return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after, 30));
2399
2553
  }
2400
2554
  },
2401
2555
  {
@@ -2404,6 +2558,7 @@ function analysisTools(reader, signal, progress, runtime) {
2404
2558
  parameters: Type.Object(
2405
2559
  {
2406
2560
  path: Type.String(),
2561
+ scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")])),
2407
2562
  startLine: Type.Optional(Type.Integer({ minimum: 1 })),
2408
2563
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 150 }))
2409
2564
  },
@@ -2412,13 +2567,20 @@ function analysisTools(reader, signal, progress, runtime) {
2412
2567
  executionMode: "sequential",
2413
2568
  async execute(_id, params) {
2414
2569
  guard();
2415
- const p = z7.object({
2570
+ const p = z8.object({
2416
2571
  path: FilePathSchema,
2417
- startLine: z7.number().int().min(1).max(1e6).default(1),
2418
- limit: z7.number().int().min(1).max(150).default(150)
2572
+ scope: z8.enum(["member", "project"]).default("member"),
2573
+ startLine: z8.number().int().min(1).max(1e6).default(1),
2574
+ limit: z8.number().int().min(1).max(150).default(150)
2419
2575
  }).strict().parse(params);
2420
2576
  progress("read", `\u8BFB\u53D6 ${p.path}\uFF0C\u7B2C ${p.startLine} \u884C\u8D77\u3002`);
2421
- return reply(await reader.read(p.path, p.startLine, p.limit));
2577
+ return reply(
2578
+ await (p.scope === "project" ? reader.projectReader() : reader).read(
2579
+ p.path,
2580
+ p.startLine,
2581
+ p.limit
2582
+ )
2583
+ );
2422
2584
  }
2423
2585
  },
2424
2586
  {
@@ -2434,7 +2596,7 @@ function analysisTools(reader, signal, progress, runtime) {
2434
2596
  executionMode: "sequential",
2435
2597
  async execute(_id, params) {
2436
2598
  guard();
2437
- const p = z7.object({ query: z7.string().min(1).max(200), after: z7.string().max(1024).default("") }).strict().parse(params);
2599
+ const p = z8.object({ query: z8.string().min(1).max(200), after: z8.string().max(1024).default("") }).strict().parse(params);
2438
2600
  progress("search", "\u641C\u7D22\u5FEB\u7167\u6B63\u6587\u3002");
2439
2601
  return reply(await reader.search(p.query, p.after));
2440
2602
  }
@@ -2704,16 +2866,16 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
2704
2866
  }
2705
2867
 
2706
2868
  // apps/server/ai/results.ts
2707
- import { z as z8 } from "zod";
2708
- var SummaryOutputSchema = z8.object({
2709
- summary: z8.string().min(1).max(2e4),
2710
- findings: z8.array(z8.string().max(2e3)).max(30),
2711
- uncertainties: z8.array(z8.string().max(2e3)).max(30),
2712
- citations: z8.array(
2713
- z8.object({
2869
+ import { z as z9 } from "zod";
2870
+ var SummaryOutputSchema = z9.object({
2871
+ summary: z9.string().min(1).max(2e4),
2872
+ findings: z9.array(z9.string().max(2e3)).max(30),
2873
+ uncertainties: z9.array(z9.string().max(2e3)).max(30),
2874
+ citations: z9.array(
2875
+ z9.object({
2714
2876
  path: FilePathSchema,
2715
- startLine: z8.number().int().positive(),
2716
- endLine: z8.number().int().positive()
2877
+ startLine: z9.number().int().positive(),
2878
+ endLine: z9.number().int().positive()
2717
2879
  }).strict()
2718
2880
  ).min(1).max(50)
2719
2881
  }).strict();
@@ -2826,15 +2988,15 @@ async function summarizeFile(job, reader, key, signal, runtime) {
2826
2988
  }
2827
2989
 
2828
2990
  // apps/server/ai/workflows/task-bootstrap.ts
2829
- import { z as z9 } from "zod";
2830
- var TaskOutputSchema = z9.object({
2831
- tasks: z9.array(
2991
+ import { z as z10 } from "zod";
2992
+ var TaskOutputSchema = z10.object({
2993
+ tasks: z10.array(
2832
2994
  TaskFieldsSchema.extend({
2833
- taskId: z9.uuid().nullable().optional(),
2834
- sources: z9.array(FilePathSchema).min(1).max(50).refine((v) => new Set(v).size === v.length)
2995
+ taskId: z10.uuid().nullable().optional(),
2996
+ sources: z10.array(FilePathSchema).min(1).max(50).refine((v) => new Set(v).size === v.length)
2835
2997
  })
2836
2998
  ).max(100),
2837
- uncertainties: z9.array(z9.string().max(2e3)).max(50)
2999
+ uncertainties: z10.array(z10.string().max(2e3)).max(50)
2838
3000
  }).strict();
2839
3001
  function validateTasks(text, reader, context) {
2840
3002
  let output;
@@ -2893,7 +3055,8 @@ async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
2893
3055
  owner: job.snapshot.owner,
2894
3056
  revision: job.snapshot.revision,
2895
3057
  inventory: reader.list("", 100),
2896
- previous: job.taskContext
3058
+ previous: job.taskContext,
3059
+ projectReferences: job.snapshot.project ? reader.projectReader().list("", 100) : null
2897
3060
  }
2898
3061
  );
2899
3062
  return { result: validateTasks(output.text, reader, job.taskContext), usage: output.usage };
@@ -2914,13 +3077,15 @@ function requestTaskBootstrap(hub, session, owner, operationId) {
2914
3077
  }
2915
3078
  function scheduleTaskBootstraps(hub, at = Date.now()) {
2916
3079
  const candidates = hub.db.prepare(
2917
- `SELECT m.workspace_id,m.member_id,r.created_at
3080
+ `SELECT m.workspace_id,m.member_id,MAX(r.created_at,COALESCE(pr.created_at,r.created_at)) created_at
2918
3081
  FROM members m JOIN projects p ON p.id=m.workspace_id JOIN users u ON u.id=m.member_id
2919
3082
  JOIN file_revisions r ON r.workspace_id=m.workspace_id AND r.owner=m.member_id AND r.revision=m.file_revision
3083
+ LEFT JOIN project_files pf ON pf.workspace_id=m.workspace_id
3084
+ LEFT JOIN file_revisions pr ON pr.workspace_id=m.workspace_id AND pr.owner='_project' AND pr.revision=pf.file_revision
2920
3085
  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
3086
  WHERE m.enabled=1 AND u.enabled=1 AND (
2922
3087
  (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)
3088
+ 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
3089
  )
2925
3090
  ORDER BY r.created_at,m.workspace_id,m.member_id`
2926
3091
  ).all();
@@ -2985,8 +3150,7 @@ async function runAiCycle(hub, stop) {
2985
3150
  if (!requesterActive())
2986
3151
  throw new HubError(403, "AI_REQUESTER_DISABLED", "Requester is disabled.");
2987
3152
  if (job.request.workflow === "task-bootstrap") {
2988
- const current = hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(job.workspace, job.snapshot.owner);
2989
- if (Number(current?.file_revision) !== job.snapshot.revision)
3153
+ if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
2990
3154
  throw new HubError(
2991
3155
  409,
2992
3156
  "AI_SOURCE_CHANGED",
@@ -3083,7 +3247,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
3083
3247
  import { parseArgs } from "node:util";
3084
3248
 
3085
3249
  // apps/server/ai/job-routes.ts
3086
- import { z as z10 } from "zod";
3250
+ import { z as z11 } from "zod";
3087
3251
 
3088
3252
  // apps/server/web-auth.ts
3089
3253
  function cookieToken(request) {
@@ -3129,10 +3293,10 @@ function authorizeRead(store, request) {
3129
3293
  // apps/server/ai/job-routes.ts
3130
3294
  function registerAiJobRoutes(server, hub) {
3131
3295
  const jobs = new AiJobs(hub);
3132
- const id = (params) => z10.object({ id: z10.uuid() }).parse(params).id;
3296
+ const id = (params) => z11.object({ id: z11.uuid() }).parse(params).id;
3133
3297
  server.get("/ai/jobs", async (req) => {
3134
- const session = authorizeWeb(hub, req), query = z10.object({
3135
- after: z10.string().regex(/^\d{0,15}$/).default("")
3298
+ const session = authorizeWeb(hub, req), query = z11.object({
3299
+ after: z11.string().regex(/^\d{0,15}$/).default("")
3136
3300
  }).parse(req.query);
3137
3301
  return jobs.list(session.workspaceId, query.after);
3138
3302
  });
@@ -3145,7 +3309,7 @@ function registerAiJobRoutes(server, hub) {
3145
3309
  async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
3146
3310
  );
3147
3311
  server.get("/ai/jobs/:id/events", async (req) => {
3148
- const query = z10.object({ after: z10.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
3312
+ const query = z11.object({ after: z11.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
3149
3313
  return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
3150
3314
  });
3151
3315
  server.post("/ai/jobs/:id/cancel", async (req) => {
@@ -3157,13 +3321,13 @@ function registerAiJobRoutes(server, hub) {
3157
3321
  return jobs.retry(
3158
3322
  authorizeWeb(hub, req),
3159
3323
  id(req.params),
3160
- z10.object({ operationId: z10.uuid() }).strict().parse(req.body).operationId
3324
+ z11.object({ operationId: z11.uuid() }).strict().parse(req.body).operationId
3161
3325
  );
3162
3326
  });
3163
3327
  server.get("/ai/jobs/:id/source", async (req) => {
3164
3328
  const session = authorizeWeb(hub, req), jobId = id(req.params);
3165
3329
  jobs.get(session.workspaceId, jobId);
3166
- const query = z10.object({ path: z10.string().max(1024), startLine: z10.coerce.number().int().min(1).default(1) }).parse(req.query);
3330
+ const query = z11.object({ path: z11.string().max(1024), startLine: z11.coerce.number().int().min(1).default(1) }).parse(req.query);
3167
3331
  const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
3168
3332
  if (!row) throw new HubError(404, "AI_JOB_NOT_FOUND", "Task not found.");
3169
3333
  return new SnapshotReader(hub, session, JSON.parse(String(row.snapshot_json))).read(
@@ -3175,10 +3339,10 @@ function registerAiJobRoutes(server, hub) {
3175
3339
  }
3176
3340
 
3177
3341
  // apps/server/ai/task-routes.ts
3178
- import { z as z11 } from "zod";
3342
+ import { z as z12 } from "zod";
3179
3343
  function registerTaskRoutes(server, hub) {
3180
3344
  const store = new TaskStore(hub);
3181
- const id = (params) => z11.object({ id: z11.uuid() }).parse(params).id;
3345
+ const id = (params) => z12.object({ id: z12.uuid() }).parse(params).id;
3182
3346
  server.get("/tasks", async (req) => store.board(authorizeWeb(hub, req)));
3183
3347
  server.post("/tasks/bootstrap", async (req) => {
3184
3348
  webMutation(req);
@@ -3312,20 +3476,20 @@ import { randomUUID as randomUUID4 } from "node:crypto";
3312
3476
  import Fastify from "fastify";
3313
3477
 
3314
3478
  // packages/protocol/index.ts
3315
- import { z as z12 } from "zod";
3479
+ import { z as z13 } from "zod";
3316
3480
  var PROTOCOL_VERSION = 1;
3317
- var HealthSchema = z12.object({ status: z12.literal("ok") });
3318
- var ServerInfoSchema = z12.object({
3319
- name: z12.literal("teammy"),
3320
- version: z12.string().min(1),
3321
- protocolVersion: z12.literal(PROTOCOL_VERSION),
3322
- capabilities: z12.array(z12.string())
3481
+ var HealthSchema = z13.object({ status: z13.literal("ok") });
3482
+ var ServerInfoSchema = z13.object({
3483
+ name: z13.literal("teammy"),
3484
+ version: z13.string().min(1),
3485
+ protocolVersion: z13.literal(PROTOCOL_VERSION),
3486
+ capabilities: z13.array(z13.string())
3323
3487
  });
3324
- var ErrorResponseSchema = z12.object({
3325
- error: z12.object({
3326
- code: z12.string(),
3327
- message: z12.string(),
3328
- requestId: z12.string()
3488
+ var ErrorResponseSchema = z13.object({
3489
+ error: z13.object({
3490
+ code: z13.string(),
3491
+ message: z13.string(),
3492
+ requestId: z13.string()
3329
3493
  })
3330
3494
  });
3331
3495
 
@@ -3333,43 +3497,43 @@ var ErrorResponseSchema = z12.object({
3333
3497
  import { ZodError } from "zod";
3334
3498
 
3335
3499
  // packages/protocol/devices.ts
3336
- import { z as z13 } from "zod";
3337
- var CredentialSchema = z13.string().regex(/^tmy_[a-f0-9]{64}$/);
3338
- var EnrollmentSchema = z13.object({
3339
- operationId: z13.uuid(),
3500
+ import { z as z14 } from "zod";
3501
+ var CredentialSchema = z14.string().regex(/^tmy_[a-f0-9]{64}$/);
3502
+ var EnrollmentSchema = z14.object({
3503
+ operationId: z14.uuid(),
3340
3504
  projectId: NameSchema,
3341
3505
  memberId: NameSchema,
3342
3506
  deviceId: DeviceIdSchema,
3343
3507
  credential: CredentialSchema
3344
3508
  }).strict();
3345
- var EffectivePolicySchema = z13.object({
3346
- checkIntervalSeconds: z13.number().min(1).max(86400),
3347
- fileSettleSeconds: z13.number().min(0).max(86400)
3509
+ var EffectivePolicySchema = z14.object({
3510
+ checkIntervalSeconds: z14.number().min(1).max(86400),
3511
+ fileSettleSeconds: z14.number().min(0).max(86400)
3348
3512
  }).strict();
3349
- var SyncSummarySchema = z13.object({
3350
- checkedAt: z13.string(),
3351
- published: z13.boolean(),
3352
- appliedFiles: z13.number().int().nonnegative(),
3353
- pendingFiles: z13.number().int().nonnegative(),
3354
- sessionSources: z13.number().int().nonnegative(),
3355
- errors: z13.array(z13.string().max(1e3)).max(20)
3513
+ var SyncSummarySchema = z14.object({
3514
+ checkedAt: z14.string(),
3515
+ published: z14.boolean(),
3516
+ appliedFiles: z14.number().int().nonnegative(),
3517
+ pendingFiles: z14.number().int().nonnegative(),
3518
+ sessionSources: z14.number().int().nonnegative(),
3519
+ errors: z14.array(z14.string().max(1e3)).max(20)
3356
3520
  }).strict();
3357
- var DeviceStatusSchema = z13.object({
3358
- clientVersion: z13.string().max(64),
3521
+ var DeviceStatusSchema = z14.object({
3522
+ clientVersion: z14.string().max(64),
3359
3523
  receivedRevision: RevisionSchema.nullable(),
3360
3524
  appliedRevision: RevisionSchema.nullable(),
3361
3525
  effectivePolicy: EffectivePolicySchema,
3362
- overrides: z13.array(z13.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
3363
- configError: z13.string().max(2e3).nullable(),
3364
- reportError: z13.string().max(2e3).nullable(),
3526
+ overrides: z14.array(z14.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
3527
+ configError: z14.string().max(2e3).nullable(),
3528
+ reportError: z14.string().max(2e3).nullable(),
3365
3529
  lastSync: SyncSummarySchema.nullable()
3366
3530
  }).strict();
3367
- var WebDevicesSchema = z13.object({
3368
- devices: z13.array(
3369
- z13.object({
3531
+ var WebDevicesSchema = z14.object({
3532
+ devices: z14.array(
3533
+ z14.object({
3370
3534
  memberId: NameSchema,
3371
- bound: z13.boolean(),
3372
- lastSeenAt: z13.string().nullable(),
3535
+ bound: z14.boolean(),
3536
+ lastSeenAt: z14.string().nullable(),
3373
3537
  status: DeviceStatusSchema.nullable()
3374
3538
  })
3375
3539
  )
@@ -3407,7 +3571,7 @@ function registerControlRoutes(server, store) {
3407
3571
  }
3408
3572
 
3409
3573
  // apps/server/project-routes.ts
3410
- import { z as z14 } from "zod";
3574
+ import { z as z15 } from "zod";
3411
3575
  function registerProjectRoutes(server, store) {
3412
3576
  const project = new ProjectRepository(store), site = new SiteRepository(store);
3413
3577
  server.get("/api/v1/projects", async (request) => ({
@@ -3454,7 +3618,7 @@ function registerProjectScopeRoutes(server, store) {
3454
3618
  });
3455
3619
  server.patch("/web/members/:id", async (request) => {
3456
3620
  webMutation(request);
3457
- const { id } = z14.object({ id: NameSchema }).parse(request.params);
3621
+ const { id } = z15.object({ id: NameSchema }).parse(request.params);
3458
3622
  return project.updateMember(
3459
3623
  authorizeWeb(store, request),
3460
3624
  id,
@@ -3463,13 +3627,13 @@ function registerProjectScopeRoutes(server, store) {
3463
3627
  });
3464
3628
  server.post("/web/members/:id/device-reset", async (request) => {
3465
3629
  webMutation(request);
3466
- const { id } = z14.object({ id: NameSchema }).parse(request.params);
3630
+ const { id } = z15.object({ id: NameSchema }).parse(request.params);
3467
3631
  return project.resetDevice(authorizeWeb(store, request), id);
3468
3632
  });
3469
3633
  }
3470
3634
 
3471
3635
  // apps/server/site-routes.ts
3472
- import { z as z15 } from "zod";
3636
+ import { z as z16 } from "zod";
3473
3637
 
3474
3638
  // apps/server/devices.ts
3475
3639
  var DeviceRepository = class {
@@ -3487,7 +3651,7 @@ var DeviceRepository = class {
3487
3651
  ).get(projectId2, value.memberId);
3488
3652
  if (!member)
3489
3653
  throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
3490
- const fingerprint = digest2(JSON.stringify(value));
3654
+ const fingerprint = digest(JSON.stringify(value));
3491
3655
  const prior = this.hub.db.prepare(
3492
3656
  "SELECT fingerprint FROM device_enrollments WHERE workspace_id=? AND member_id=? AND operation_id=?"
3493
3657
  ).get(projectId2, value.memberId, value.operationId);
@@ -3497,7 +3661,7 @@ var DeviceRepository = class {
3497
3661
  "OPERATION_MISMATCH",
3498
3662
  "Enrollment operation was reused with different content."
3499
3663
  );
3500
- const tokenHash = digest2(value.credential);
3664
+ const tokenHash = digest(value.credential);
3501
3665
  if (prior) {
3502
3666
  if (member.token_hash !== tokenHash || member.device_id !== value.deviceId)
3503
3667
  throw new HubError(
@@ -3545,7 +3709,7 @@ function registerSiteRoutes(server, store) {
3545
3709
  "/api/v1/web/session",
3546
3710
  { onRequest: async (request) => webMutation(request) },
3547
3711
  async (request, reply) => {
3548
- const { id } = z15.object({ id: NameSchema }).strict().parse(request.body);
3712
+ const { id } = z16.object({ id: NameSchema }).strict().parse(request.body);
3549
3713
  const { token, session } = site.login(id);
3550
3714
  const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
3551
3715
  reply.header(
@@ -3559,7 +3723,7 @@ function registerSiteRoutes(server, store) {
3559
3723
  server.patch("/api/v1/web/session", async (request) => {
3560
3724
  webMutation(request);
3561
3725
  const identity = site.identity(cookieToken(request));
3562
- const { displayName } = z15.object({ displayName: z15.string().trim().max(120) }).strict().parse(request.body);
3726
+ const { displayName } = z16.object({ displayName: z16.string().trim().max(120) }).strict().parse(request.body);
3563
3727
  return site.renameSelf(identity, displayName);
3564
3728
  });
3565
3729
  server.delete("/api/v1/web/session", async (request, reply) => {
@@ -3600,71 +3764,71 @@ function registerSiteRoutes(server, store) {
3600
3764
  }
3601
3765
 
3602
3766
  // apps/server/session-routes.ts
3603
- import { z as z17 } from "zod";
3767
+ import { z as z18 } from "zod";
3604
3768
 
3605
3769
  // packages/protocol/sessions.ts
3606
- import { z as z16 } from "zod";
3607
- var AgentSchema = z16.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
3608
- var SessionSnapshotSchema = z16.object({
3609
- formatVersion: z16.literal(1),
3610
- adapterVersion: z16.literal(1),
3770
+ import { z as z17 } from "zod";
3771
+ var AgentSchema = z17.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
3772
+ var SessionSnapshotSchema = z17.object({
3773
+ formatVersion: z17.literal(1),
3774
+ adapterVersion: z17.literal(1),
3611
3775
  agent: AgentSchema,
3612
- nativeId: z16.string().min(1).max(512),
3613
- cwd: z16.string().min(1).max(4096),
3614
- messages: z16.array(
3615
- z16.object({
3616
- id: z16.string().min(1).max(1024),
3617
- role: z16.enum(["user", "assistant"]),
3618
- text: z16.string().min(1).max(2 * 1024 * 1024),
3619
- source: z16.string().min(1).max(1024)
3776
+ nativeId: z17.string().min(1).max(512),
3777
+ cwd: z17.string().min(1).max(4096),
3778
+ messages: z17.array(
3779
+ z17.object({
3780
+ id: z17.string().min(1).max(1024),
3781
+ role: z17.enum(["user", "assistant"]),
3782
+ text: z17.string().min(1).max(2 * 1024 * 1024),
3783
+ source: z17.string().min(1).max(1024)
3620
3784
  }).strict()
3621
3785
  ).max(5e4)
3622
3786
  }).strict().refine(
3623
3787
  (s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
3624
3788
  "Duplicate message IDs."
3625
3789
  );
3626
- var SessionCommitSchema = z16.object({
3627
- operationId: z16.uuid(),
3790
+ var SessionCommitSchema = z17.object({
3791
+ operationId: z17.uuid(),
3628
3792
  sessionKey: HashSchema,
3629
3793
  parentRevision: RevisionSchema,
3630
3794
  blob: BlobRefSchema
3631
3795
  }).strict();
3632
- var SessionInfoSchema = z16.object({
3796
+ var SessionInfoSchema = z17.object({
3633
3797
  owner: NameSchema,
3634
3798
  sessionKey: HashSchema,
3635
3799
  agent: AgentSchema,
3636
- nativeId: z16.string(),
3637
- cwd: z16.string(),
3638
- title: z16.string(),
3800
+ nativeId: z17.string(),
3801
+ cwd: z17.string(),
3802
+ title: z17.string(),
3639
3803
  revision: RevisionSchema,
3640
- updatedAt: z16.string(),
3641
- messageCount: z16.number().int().nonnegative(),
3804
+ updatedAt: z17.string(),
3805
+ messageCount: z17.number().int().nonnegative(),
3642
3806
  blob: BlobRefSchema
3643
3807
  });
3644
- var SessionListSchema = z16.object({
3645
- sessions: z16.array(SessionInfoSchema),
3808
+ var SessionListSchema = z17.object({
3809
+ sessions: z17.array(SessionInfoSchema),
3646
3810
  nextCursor: RevisionSchema.nullable()
3647
3811
  });
3648
- var SessionStatsSchema = z16.object({
3649
- totalSessions: z16.number().int().nonnegative(),
3650
- totalMessages: z16.number().int().nonnegative(),
3651
- memberCount: z16.number().int().nonnegative(),
3652
- updatedAt: z16.string().nullable()
3812
+ var SessionStatsSchema = z17.object({
3813
+ totalSessions: z17.number().int().nonnegative(),
3814
+ totalMessages: z17.number().int().nonnegative(),
3815
+ memberCount: z17.number().int().nonnegative(),
3816
+ updatedAt: z17.string().nullable()
3653
3817
  });
3654
- var SessionScopeSchema = z16.object({
3818
+ var SessionScopeSchema = z17.object({
3655
3819
  owner: NameSchema.optional(),
3656
3820
  sessionKey: HashSchema.optional()
3657
3821
  }).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
3658
- var SessionSearchSchema = z16.object({
3659
- matches: z16.array(
3660
- z16.object({
3822
+ var SessionSearchSchema = z17.object({
3823
+ matches: z17.array(
3824
+ z17.object({
3661
3825
  owner: NameSchema,
3662
3826
  sessionKey: HashSchema,
3663
3827
  revision: RevisionSchema,
3664
- messageId: z16.string(),
3665
- role: z16.enum(["user", "assistant"]),
3666
- snippet: z16.string(),
3667
- title: z16.string()
3828
+ messageId: z17.string(),
3829
+ role: z17.enum(["user", "assistant"]),
3830
+ snippet: z17.string(),
3831
+ title: z17.string()
3668
3832
  })
3669
3833
  ),
3670
3834
  nextCursor: RevisionSchema.nullable()
@@ -3881,9 +4045,9 @@ function registerSessionRoutes(server, store) {
3881
4045
  authorize(request);
3882
4046
  }
3883
4047
  };
3884
- const pagination = z17.object({
3885
- after: z17.coerce.number().pipe(RevisionSchema).default(0),
3886
- limit: z17.coerce.number().int().min(1).max(100).default(50)
4048
+ const pagination = z18.object({
4049
+ after: z18.coerce.number().pipe(RevisionSchema).default(0),
4050
+ limit: z18.coerce.number().int().min(1).max(100).default(50)
3887
4051
  });
3888
4052
  server.post(
3889
4053
  "/sessions/commits",
@@ -3903,7 +4067,7 @@ function registerSessionRoutes(server, store) {
3903
4067
  const q = pagination.extend({
3904
4068
  owner: NameSchema.optional(),
3905
4069
  sessionKey: HashSchema.optional(),
3906
- q: z17.string().min(1).max(500).refine((s) => !s.includes("\0"))
4070
+ q: z18.string().min(1).max(500).refine((s) => !s.includes("\0"))
3907
4071
  }).parse(request.query);
3908
4072
  return sessions.search(
3909
4073
  authorizeRead(store, request),
@@ -3914,16 +4078,25 @@ function registerSessionRoutes(server, store) {
3914
4078
  );
3915
4079
  });
3916
4080
  server.get("/sessions/:owner/:key", async (request) => {
3917
- const p = z17.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
3918
- const q = z17.object({ revision: z17.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
4081
+ const p = z18.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
4082
+ const q = z18.object({ revision: z18.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
3919
4083
  return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
3920
4084
  });
3921
4085
  }
3922
4086
 
3923
4087
  // apps/server/file-routes.ts
3924
- import { z as z18 } from "zod";
4088
+ import { z as z19 } from "zod";
3925
4089
  function registerFileRoutes(server, store) {
3926
4090
  const files = new FileRepository(store);
4091
+ const shared = new SharedFilesRepository(store);
4092
+ server.get(
4093
+ "/project/shared-files",
4094
+ async (request) => shared.config(authorizeRead(store, request))
4095
+ );
4096
+ server.patch("/project/shared-files", async (request) => {
4097
+ webMutation(request);
4098
+ return shared.update(authorizeWeb(store, request), SharedFilesUpdateSchema.parse(request.body));
4099
+ });
3927
4100
  const device = (request) => {
3928
4101
  const value = request.headers["x-teammy-device"];
3929
4102
  return typeof value === "string" ? value : void 0;
@@ -3943,13 +4116,13 @@ function registerFileRoutes(server, store) {
3943
4116
  missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
3944
4117
  }));
3945
4118
  server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
3946
- const { hash: hash2 } = z18.object({ hash: HashSchema }).parse(request.params);
4119
+ const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
3947
4120
  if (!Buffer.isBuffer(request.body))
3948
4121
  throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
3949
4122
  return files.upload(request.headers.authorization, device(request), hash2, request.body);
3950
4123
  });
3951
4124
  server.get("/blobs/:hash", async (request, reply) => {
3952
- const { hash: hash2 } = z18.object({ hash: HashSchema }).parse(request.params);
4125
+ const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
3953
4126
  return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
3954
4127
  });
3955
4128
  server.post(
@@ -3961,12 +4134,16 @@ function registerFileRoutes(server, store) {
3961
4134
  FileCommitSchema.parse(request.body)
3962
4135
  )
3963
4136
  );
4137
+ server.post("/files/_project/commits", guard, async (request) => {
4138
+ const { configRevision, ...batch } = SharedFileCommitSchema.parse(request.body);
4139
+ return files.commit(request.headers.authorization, device(request), batch, configRevision);
4140
+ });
3964
4141
  server.get("/files/:owner/manifest", async (request) => {
3965
- const { owner } = z18.object({ owner: NameSchema }).parse(request.params);
3966
- const query = z18.object({
3967
- revision: z18.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
3968
- after: z18.string().max(1024).default(""),
3969
- limit: z18.coerce.number().int().min(1).max(500).default(200)
4142
+ const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4143
+ const query = z19.object({
4144
+ revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4145
+ after: z19.string().max(1024).default(""),
4146
+ limit: z19.coerce.number().int().min(1).max(500).default(200)
3970
4147
  }).parse(request.query);
3971
4148
  return files.manifest(
3972
4149
  authorizeRead(store, request),
@@ -3977,28 +4154,28 @@ function registerFileRoutes(server, store) {
3977
4154
  );
3978
4155
  });
3979
4156
  server.get("/files/:owner/entry", async (request) => {
3980
- const { owner } = z18.object({ owner: NameSchema }).parse(request.params);
3981
- const q = z18.object({
4157
+ const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4158
+ const q = z19.object({
3982
4159
  path: FilePathSchema,
3983
- revision: z18.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
4160
+ revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
3984
4161
  }).parse(request.query);
3985
4162
  return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
3986
4163
  });
3987
4164
  server.get("/files/:owner/tree", async (request) => {
3988
- const { owner } = z18.object({ owner: NameSchema }).parse(request.params);
3989
- const q = z18.object({
3990
- path: z18.union([z18.literal(""), FilePathSchema]).default(""),
3991
- revision: z18.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
3992
- after: z18.string().max(255).default(""),
3993
- limit: z18.coerce.number().int().min(1).max(200).default(100)
4165
+ const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4166
+ const q = z19.object({
4167
+ path: z19.union([z19.literal(""), FilePathSchema]).default(""),
4168
+ revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
4169
+ after: z19.string().max(255).default(""),
4170
+ limit: z19.coerce.number().int().min(1).max(200).default(100)
3994
4171
  }).parse(request.query);
3995
4172
  return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
3996
4173
  });
3997
4174
  server.get("/files/:owner/changes", async (request) => {
3998
- const { owner } = z18.object({ owner: NameSchema }).parse(request.params);
3999
- const query = z18.object({
4000
- after: z18.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
4001
- limit: z18.coerce.number().int().min(1).max(100).default(100)
4175
+ const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
4176
+ const query = z19.object({
4177
+ after: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
4178
+ limit: z19.coerce.number().int().min(1).max(100).default(100)
4002
4179
  }).parse(request.query);
4003
4180
  return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
4004
4181
  });
@@ -4050,6 +4227,7 @@ function buildServer(options) {
4050
4227
  "identity",
4051
4228
  "blobs",
4052
4229
  "file-revisions",
4230
+ "project-files",
4053
4231
  "session-revisions",
4054
4232
  "session-search",
4055
4233
  "project-config",