@teammy/server 0.0.7 → 0.0.9

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