@teammy/server 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/main.js +1355 -1177
- package/dist/runtime/shared/tools.json +2 -2
- package/dist/runtime/workflows/task-bootstrap/config.json +1 -1
- package/dist/runtime/workflows/task-bootstrap/prompt.md +1 -1
- package/dist/web/assets/Dashboard-CsQ-GV-q.js +1 -0
- package/dist/web/assets/Files-cG0tkAfP.js +1 -0
- package/dist/web/assets/Markdown-Bt-6EDRz.js +29 -0
- package/dist/web/assets/Sessions-B7ABJvRH.js +1 -0
- package/dist/web/assets/{index-DT_BrKO8.js → index-f4fiI3WA.js} +5 -3
- package/dist/web/assets/index-hjEc8Pme.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/Dashboard-DXshk6qj.js +0 -1
- package/dist/web/assets/Files-BiIfupOU.js +0 -1
- package/dist/web/assets/Markdown-DOAYIfza.js +0 -29
- package/dist/web/assets/Sessions-BhnUEPaJ.js +0 -1
- package/dist/web/assets/index-BrRuqgnh.css +0 -1
package/dist/main.js
CHANGED
|
@@ -1,15 +1,92 @@
|
|
|
1
|
-
// apps/server/ai/
|
|
2
|
-
import {
|
|
1
|
+
// apps/server/ai/snapshots.ts
|
|
2
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3
3
|
|
|
4
|
-
// apps/server/
|
|
5
|
-
import { createHash
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// apps/server/files.ts
|
|
5
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// packages/storage/local-blob-store.ts
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
function hash(content) {
|
|
13
|
+
return createHash("sha256").update(content).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function hasCode(error, code) {
|
|
16
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
17
|
+
}
|
|
18
|
+
var BlobIntegrityError = class extends Error {
|
|
19
|
+
constructor(sha256) {
|
|
20
|
+
super(`Stored content failed integrity verification: ${sha256}`);
|
|
21
|
+
this.sha256 = sha256;
|
|
22
|
+
this.name = "BlobIntegrityError";
|
|
23
|
+
}
|
|
24
|
+
sha256;
|
|
25
|
+
};
|
|
26
|
+
var LocalBlobStore = class {
|
|
27
|
+
root;
|
|
28
|
+
constructor(root) {
|
|
29
|
+
this.root = resolve(root);
|
|
30
|
+
}
|
|
31
|
+
async put(content) {
|
|
32
|
+
const bytes = Buffer.from(content);
|
|
33
|
+
const sha256 = hash(bytes);
|
|
34
|
+
const target = this.pathFor(sha256);
|
|
35
|
+
await mkdir(dirname(target), { recursive: true, mode: 448 });
|
|
36
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
37
|
+
const file = await open(temporary, "wx", 384);
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
await file.writeFile(bytes);
|
|
41
|
+
await file.sync();
|
|
42
|
+
} finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await link(temporary, target);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
49
|
+
await this.get(sha256);
|
|
50
|
+
}
|
|
51
|
+
if (process.platform !== "win32") {
|
|
52
|
+
let directory = dirname(target);
|
|
53
|
+
for (; ; ) {
|
|
54
|
+
const handle = await open(directory, "r");
|
|
55
|
+
try {
|
|
56
|
+
await handle.sync();
|
|
57
|
+
} finally {
|
|
58
|
+
await handle.close();
|
|
59
|
+
}
|
|
60
|
+
if (directory === dirname(this.root)) break;
|
|
61
|
+
directory = dirname(directory);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await unlink(temporary);
|
|
66
|
+
}
|
|
67
|
+
return { sha256, size: bytes.byteLength };
|
|
68
|
+
}
|
|
69
|
+
async get(sha256) {
|
|
70
|
+
const target = this.pathFor(sha256);
|
|
71
|
+
let content;
|
|
72
|
+
try {
|
|
73
|
+
content = await readFile(target);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (hasCode(error, "ENOENT")) return void 0;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
if (hash(content) !== sha256) throw new BlobIntegrityError(sha256);
|
|
79
|
+
return content;
|
|
80
|
+
}
|
|
81
|
+
pathFor(sha256) {
|
|
82
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
83
|
+
return join(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
9
86
|
|
|
10
87
|
// packages/state/sqlite.ts
|
|
11
88
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
89
|
+
import { dirname as dirname2 } from "node:path";
|
|
13
90
|
import { DatabaseSync } from "node:sqlite";
|
|
14
91
|
function transaction(db, action) {
|
|
15
92
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -25,7 +102,7 @@ function transaction(db, action) {
|
|
|
25
102
|
function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
26
103
|
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
|
|
27
104
|
throw new Error("Invalid SQLite busy timeout.");
|
|
28
|
-
mkdirSync(
|
|
105
|
+
mkdirSync(dirname2(path), { recursive: true, mode: 448 });
|
|
29
106
|
const db = new DatabaseSync(path);
|
|
30
107
|
try {
|
|
31
108
|
chmodSync(path, 384);
|
|
@@ -48,17 +125,71 @@ function openDatabase(path, migrations2, busyTimeoutMs = 5e3) {
|
|
|
48
125
|
}
|
|
49
126
|
}
|
|
50
127
|
|
|
51
|
-
// packages/protocol/
|
|
128
|
+
// packages/protocol/files.ts
|
|
52
129
|
import { z } from "zod";
|
|
53
|
-
var
|
|
54
|
-
var
|
|
55
|
-
var
|
|
56
|
-
var
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
var MAX_BLOB_BYTES = 10 * 1024 * 1024;
|
|
131
|
+
var MAX_BATCH_FILES = 200;
|
|
132
|
+
var HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
133
|
+
var RevisionSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
134
|
+
var FilePathSchema = z.string().min(1).max(1024).refine((path) => {
|
|
135
|
+
if (path !== path.normalize("NFC") || /[\\\x00-\x1f\x7f:<>"|?*]/.test(path)) return false;
|
|
136
|
+
return path.split("/").every(
|
|
137
|
+
(part) => part.length > 0 && part.length <= 255 && part !== "." && part !== ".." && !/[. ]$/.test(part) && !/^\.teammy(?:-|$)/i.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
|
|
138
|
+
);
|
|
139
|
+
}, "Path must be a portable, normalized relative file path.");
|
|
140
|
+
var BlobRefSchema = z.object({
|
|
141
|
+
sha256: HashSchema,
|
|
142
|
+
size: z.number().int().min(0).max(MAX_BLOB_BYTES)
|
|
143
|
+
});
|
|
144
|
+
var FileChangeSchema = z.discriminatedUnion("kind", [
|
|
145
|
+
BlobRefSchema.extend({ kind: z.literal("upsert"), path: FilePathSchema }).strict(),
|
|
146
|
+
z.object({ kind: z.literal("delete"), path: FilePathSchema }).strict()
|
|
147
|
+
]);
|
|
148
|
+
var FileCommitSchema = z.object({
|
|
149
|
+
operationId: z.uuid(),
|
|
150
|
+
parentRevision: RevisionSchema,
|
|
151
|
+
changes: z.array(FileChangeSchema).min(1).max(MAX_BATCH_FILES)
|
|
152
|
+
}).strict().refine(
|
|
153
|
+
(batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
|
|
154
|
+
"Duplicate paths in a batch."
|
|
155
|
+
);
|
|
156
|
+
var CommitResultSchema = z.object({ operationId: z.uuid(), revision: RevisionSchema });
|
|
157
|
+
var FileEntrySchema = BlobRefSchema.extend({
|
|
158
|
+
path: FilePathSchema,
|
|
159
|
+
revision: RevisionSchema
|
|
160
|
+
});
|
|
161
|
+
var ManifestSchema = z.object({
|
|
162
|
+
revision: RevisionSchema,
|
|
163
|
+
files: z.array(FileEntrySchema),
|
|
164
|
+
nextCursor: z.string().nullable()
|
|
165
|
+
});
|
|
166
|
+
var FileRevisionSchema = z.object({
|
|
167
|
+
revision: RevisionSchema,
|
|
168
|
+
operationId: z.uuid(),
|
|
169
|
+
createdAt: z.string(),
|
|
170
|
+
changes: z.array(FileChangeSchema)
|
|
171
|
+
});
|
|
172
|
+
var ChangesSchema = z.object({
|
|
173
|
+
headRevision: RevisionSchema,
|
|
174
|
+
revisions: z.array(FileRevisionSchema)
|
|
175
|
+
});
|
|
176
|
+
var BlobCheckSchema = z.object({ hashes: z.array(HashSchema).max(MAX_BATCH_FILES) }).strict();
|
|
177
|
+
var MissingBlobsSchema = z.object({ missing: z.array(HashSchema) });
|
|
178
|
+
var FileTreeEntrySchema = z.discriminatedUnion("kind", [
|
|
179
|
+
FileEntrySchema.extend({ kind: z.literal("file") }),
|
|
180
|
+
z.object({
|
|
181
|
+
kind: z.literal("directory"),
|
|
182
|
+
path: FilePathSchema,
|
|
183
|
+
fileCount: z.number().int().positive()
|
|
184
|
+
})
|
|
185
|
+
]);
|
|
186
|
+
var FileTreeSchema = z.object({
|
|
187
|
+
revision: RevisionSchema,
|
|
188
|
+
path: z.union([z.literal(""), FilePathSchema]),
|
|
189
|
+
entries: z.array(FileTreeEntrySchema),
|
|
190
|
+
nextCursor: z.string().nullable()
|
|
60
191
|
});
|
|
61
|
-
var
|
|
192
|
+
var FileSelectionSchema = z.object({ revision: RevisionSchema, file: FileEntrySchema });
|
|
62
193
|
|
|
63
194
|
// apps/server/errors.ts
|
|
64
195
|
var HubError = class extends Error {
|
|
@@ -72,454 +203,131 @@ var HubError = class extends Error {
|
|
|
72
203
|
code;
|
|
73
204
|
};
|
|
74
205
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
CREATE TABLE sessions (
|
|
78
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
80
|
-
agent TEXT NOT NULL, native_id TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL,
|
|
81
|
-
revision INTEGER NOT NULL, updated_at TEXT NOT NULL, message_count INTEGER NOT NULL,
|
|
82
|
-
hash TEXT NOT NULL, size INTEGER NOT NULL,
|
|
83
|
-
UNIQUE(workspace_id, owner, session_key)
|
|
84
|
-
);
|
|
85
|
-
CREATE INDEX session_listing ON sessions(workspace_id, id);
|
|
86
|
-
CREATE TABLE session_revisions (
|
|
87
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
88
|
-
revision INTEGER NOT NULL, info_json TEXT NOT NULL,
|
|
89
|
-
PRIMARY KEY(workspace_id, owner, session_key, revision)
|
|
90
|
-
);
|
|
91
|
-
CREATE TABLE session_operations (
|
|
92
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
93
|
-
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
94
|
-
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
95
|
-
);
|
|
96
|
-
CREATE TABLE session_messages (
|
|
97
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
99
|
-
revision INTEGER NOT NULL, message_id TEXT NOT NULL, role TEXT NOT NULL, body TEXT NOT NULL, search_text TEXT NOT NULL
|
|
100
|
-
);
|
|
101
|
-
CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
|
|
102
|
-
CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
|
|
103
|
-
`;
|
|
104
|
-
var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
|
|
105
|
-
|
|
106
|
-
// apps/server/control-schema.ts
|
|
107
|
-
var controlMigration = String.raw`
|
|
108
|
-
CREATE TABLE device_status(workspace_id TEXT NOT NULL,member_id TEXT NOT NULL,device_id TEXT NOT NULL,seen_at TEXT NOT NULL,status_json TEXT NOT NULL,PRIMARY KEY(workspace_id,member_id));
|
|
109
|
-
`;
|
|
206
|
+
// packages/protocol/shared-files.ts
|
|
207
|
+
import { z as z3 } from "zod";
|
|
110
208
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
209
|
+
// packages/protocol/identity.ts
|
|
210
|
+
import { z as z2 } from "zod";
|
|
211
|
+
var NameSchema = z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
|
|
212
|
+
var DeviceIdSchema = z2.uuid();
|
|
213
|
+
var BindRequestSchema = z2.object({ deviceId: DeviceIdSchema, projectId: NameSchema.optional() }).strict();
|
|
214
|
+
var BindingSchema = z2.object({
|
|
215
|
+
workspaceId: NameSchema,
|
|
216
|
+
memberId: NameSchema,
|
|
217
|
+
deviceId: DeviceIdSchema
|
|
218
|
+
});
|
|
219
|
+
var MembersSchema = z2.object({ members: z2.array(NameSchema) });
|
|
115
220
|
|
|
116
|
-
//
|
|
117
|
-
var
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
|
|
141
|
-
setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
|
|
142
|
-
);
|
|
143
|
-
CREATE TABLE web_sessions (
|
|
144
|
-
token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
|
|
145
|
-
);
|
|
146
|
-
DROP TABLE ai_profiles;
|
|
147
|
-
CREATE TABLE ai_profiles (
|
|
148
|
-
id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
|
|
221
|
+
// packages/protocol/shared-files.ts
|
|
222
|
+
var PROJECT_FILE_OWNER = "_project";
|
|
223
|
+
var FileOwnerSchema = z3.union([NameSchema, z3.literal(PROJECT_FILE_OWNER)]);
|
|
224
|
+
var SharedPathSchema = z3.string().max(255).refine((path) => {
|
|
225
|
+
const directory = path.endsWith("/");
|
|
226
|
+
const name = directory ? path.slice(0, -1) : path;
|
|
227
|
+
return FilePathSchema.safeParse(name).success && !name.includes("/") && !name.startsWith(".") && !/^_?teammy/i.test(name) && (directory ? name.startsWith("_") : name.includes("."));
|
|
228
|
+
}, "Use a root file (README.md) or an underscore-prefixed directory (_template/).");
|
|
229
|
+
var SharedFilesConfigSchema = z3.object({
|
|
230
|
+
maintainer: NameSchema.nullable(),
|
|
231
|
+
paths: z3.array(SharedPathSchema).max(30).refine((paths2) => new Set(paths2.map((p) => p.toLowerCase())).size === paths2.length),
|
|
232
|
+
revision: RevisionSchema
|
|
233
|
+
}).strict();
|
|
234
|
+
var SharedFilesUpdateSchema = SharedFilesConfigSchema.omit({ revision: true }).extend({ expectedRevision: RevisionSchema }).strict();
|
|
235
|
+
var SharedFileCommitSchema = FileCommitSchema.safeExtend({
|
|
236
|
+
configRevision: RevisionSchema
|
|
237
|
+
});
|
|
238
|
+
var DEFAULT_SHARED_PATHS = ["README.md", "_template/"];
|
|
239
|
+
function privateWorkspacePath(path) {
|
|
240
|
+
return path.split("/").some((part) => /^\.teammy(?:[-.]|$)/i.test(part));
|
|
241
|
+
}
|
|
242
|
+
function sharedPathAllowed(path, paths2) {
|
|
243
|
+
return !privateWorkspacePath(path) && paths2.some(
|
|
244
|
+
(entry) => entry.endsWith("/") ? path === entry.slice(0, -1) || path.startsWith(entry) : path === entry
|
|
149
245
|
);
|
|
150
|
-
|
|
151
|
-
var sitePolicyMigration = `
|
|
152
|
-
UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
|
|
153
|
-
(SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
|
|
154
|
-
'{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
|
|
155
|
-
)));
|
|
156
|
-
UPDATE projects SET config_json=json_set(config_json,
|
|
157
|
-
'$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
|
|
158
|
-
'$.configRevision',json_extract(config_json,'$.configRevision')+1)
|
|
159
|
-
WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
|
|
160
|
-
`;
|
|
161
|
-
var equalMembershipMigration = `
|
|
162
|
-
ALTER TABLE members DROP COLUMN role;
|
|
163
|
-
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
164
|
-
`;
|
|
246
|
+
}
|
|
165
247
|
|
|
166
|
-
// apps/server/
|
|
167
|
-
|
|
168
|
-
ALTER TABLE members ADD COLUMN file_revision INTEGER NOT NULL DEFAULT 0;
|
|
169
|
-
CREATE TABLE workspace_blobs (workspace_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(workspace_id, hash));
|
|
170
|
-
CREATE TABLE file_operations (
|
|
171
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
172
|
-
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
173
|
-
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
174
|
-
);
|
|
175
|
-
CREATE TABLE file_revisions (
|
|
176
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
177
|
-
operation_id TEXT NOT NULL, created_at TEXT NOT NULL, changes_json TEXT NOT NULL,
|
|
178
|
-
PRIMARY KEY(workspace_id, owner, revision)
|
|
179
|
-
);
|
|
180
|
-
CREATE TABLE current_files (
|
|
181
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, path_key TEXT NOT NULL,
|
|
182
|
-
hash TEXT NOT NULL, size INTEGER NOT NULL, revision INTEGER NOT NULL,
|
|
183
|
-
PRIMARY KEY(workspace_id, owner, path), UNIQUE(workspace_id, owner, path_key)
|
|
184
|
-
);
|
|
185
|
-
CREATE TABLE file_events (
|
|
186
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
187
|
-
hash TEXT, size INTEGER,
|
|
188
|
-
PRIMARY KEY(workspace_id, owner, path, revision)
|
|
189
|
-
);
|
|
190
|
-
`;
|
|
191
|
-
|
|
192
|
-
// apps/server/ai/task-schema.ts
|
|
193
|
-
var taskBoardMigration = `
|
|
194
|
-
ALTER TABLE ai_jobs ADD COLUMN runtime_json TEXT;
|
|
195
|
-
ALTER TABLE ai_jobs ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
|
196
|
-
CREATE TABLE task_board_members (
|
|
197
|
-
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, job_id TEXT NOT NULL REFERENCES ai_jobs(id),
|
|
198
|
-
PRIMARY KEY(workspace_id,owner)
|
|
199
|
-
);
|
|
200
|
-
CREATE TABLE task_cards (
|
|
201
|
-
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, owner TEXT NOT NULL,
|
|
202
|
-
job_id TEXT NOT NULL REFERENCES ai_jobs(id), fields_json TEXT NOT NULL,
|
|
203
|
-
sources_json TEXT NOT NULL, synced_at TEXT NOT NULL, correction_json TEXT,
|
|
204
|
-
active INTEGER NOT NULL DEFAULT 1, needs_review INTEGER NOT NULL DEFAULT 0
|
|
205
|
-
);
|
|
206
|
-
CREATE INDEX task_cards_scope ON task_cards(workspace_id,owner,active);
|
|
207
|
-
`;
|
|
208
|
-
var taskMaintenanceMigration = `
|
|
209
|
-
ALTER TABLE ai_jobs ADD COLUMN task_context_json TEXT;
|
|
210
|
-
ALTER TABLE task_board_members ADD COLUMN applied_job_id TEXT REFERENCES ai_jobs(id);
|
|
211
|
-
UPDATE task_board_members SET applied_job_id=(
|
|
212
|
-
SELECT j.id FROM ai_jobs j
|
|
213
|
-
WHERE j.workspace_id=task_board_members.workspace_id AND j.status='succeeded'
|
|
214
|
-
AND json_extract(j.request_json,'$.workflow')='task-bootstrap'
|
|
215
|
-
AND json_extract(j.snapshot_json,'$.owner')=task_board_members.owner
|
|
216
|
-
ORDER BY j.rowid DESC LIMIT 1
|
|
217
|
-
);
|
|
218
|
-
`;
|
|
219
|
-
|
|
220
|
-
// apps/server/ai/schema.ts
|
|
221
|
-
var aiConfigMigration = `
|
|
222
|
-
CREATE TABLE ai_profiles (
|
|
223
|
-
workspace_id TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
224
|
-
config_json TEXT NOT NULL, secret TEXT,
|
|
225
|
-
PRIMARY KEY(workspace_id,id)
|
|
226
|
-
);
|
|
227
|
-
`;
|
|
228
|
-
var aiJobsMigration = `
|
|
229
|
-
CREATE TABLE ai_jobs (
|
|
230
|
-
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, requester TEXT NOT NULL,
|
|
231
|
-
request_json TEXT NOT NULL, profile_json TEXT NOT NULL, snapshot_json TEXT NOT NULL,
|
|
232
|
-
prompt_version TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, recovery_count INTEGER NOT NULL DEFAULT 0,
|
|
233
|
-
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, run_token TEXT, lease_until INTEGER,
|
|
234
|
-
result_json TEXT, error TEXT
|
|
235
|
-
);
|
|
236
|
-
CREATE INDEX ai_jobs_queue ON ai_jobs(status,created_at);
|
|
237
|
-
CREATE INDEX ai_jobs_workspace ON ai_jobs(workspace_id,id);
|
|
238
|
-
CREATE TABLE ai_operations(workspace_id TEXT NOT NULL,requester TEXT NOT NULL,operation_id TEXT NOT NULL,fingerprint TEXT NOT NULL,job_id TEXT NOT NULL REFERENCES ai_jobs(id),PRIMARY KEY(workspace_id,requester,operation_id));
|
|
239
|
-
CREATE TABLE ai_attempts(job_id TEXT NOT NULL REFERENCES ai_jobs(id),number INTEGER NOT NULL,status TEXT NOT NULL,started_at TEXT NOT NULL,ended_at TEXT,error TEXT,usage_json TEXT,PRIMARY KEY(job_id,number));
|
|
240
|
-
CREATE TABLE ai_events(seq INTEGER PRIMARY KEY AUTOINCREMENT,job_id TEXT NOT NULL REFERENCES ai_jobs(id),attempt INTEGER NOT NULL,kind TEXT NOT NULL,message TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
241
|
-
CREATE INDEX ai_events_job ON ai_events(job_id,seq);
|
|
242
|
-
`;
|
|
243
|
-
var aiDefaultsMigration = `
|
|
244
|
-
CREATE TABLE ai_defaults (
|
|
245
|
-
scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
|
|
246
|
-
);
|
|
247
|
-
`;
|
|
248
|
-
|
|
249
|
-
// apps/server/store.ts
|
|
250
|
-
function digest(token) {
|
|
251
|
-
return createHash("sha256").update(token).digest("hex");
|
|
252
|
-
}
|
|
253
|
-
var migrations = [
|
|
254
|
-
String.raw`
|
|
255
|
-
CREATE TABLE members (
|
|
256
|
-
workspace_id TEXT NOT NULL,
|
|
257
|
-
member_id TEXT NOT NULL,
|
|
258
|
-
token_hash TEXT UNIQUE,
|
|
259
|
-
device_id TEXT,
|
|
260
|
-
PRIMARY KEY (workspace_id, member_id)
|
|
261
|
-
);
|
|
262
|
-
`,
|
|
263
|
-
fileMigration,
|
|
264
|
-
sessionMigration,
|
|
265
|
-
projectMigration,
|
|
266
|
-
deviceMigration,
|
|
267
|
-
controlMigration,
|
|
268
|
-
aiConfigMigration,
|
|
269
|
-
aiJobsMigration,
|
|
270
|
-
multiProjectMigration,
|
|
271
|
-
aiDefaultsMigration,
|
|
272
|
-
sitePolicyMigration,
|
|
273
|
-
sessionOwnerIndexMigration,
|
|
274
|
-
equalMembershipMigration,
|
|
275
|
-
taskBoardMigration,
|
|
276
|
-
taskMaintenanceMigration
|
|
277
|
-
];
|
|
278
|
-
var HubStore = class {
|
|
279
|
-
constructor(directory) {
|
|
280
|
-
this.directory = directory;
|
|
281
|
-
const path = join(directory, "hub.sqlite3");
|
|
282
|
-
if (existsSync(path)) {
|
|
283
|
-
const existing = new DatabaseSync2(path, { readOnly: true });
|
|
284
|
-
try {
|
|
285
|
-
const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
|
|
286
|
-
if (version > 0 && version < 9)
|
|
287
|
-
throw new Error(
|
|
288
|
-
"Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
|
|
289
|
-
);
|
|
290
|
-
} finally {
|
|
291
|
-
existing.close();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
this.db = openDatabase(path, migrations);
|
|
295
|
-
}
|
|
296
|
-
directory;
|
|
297
|
-
db;
|
|
298
|
-
close() {
|
|
299
|
-
this.db.close();
|
|
300
|
-
}
|
|
301
|
-
issueMember(workspaceId, memberId, reset = false) {
|
|
302
|
-
NameSchema.parse(workspaceId);
|
|
303
|
-
NameSchema.parse(memberId);
|
|
304
|
-
const token = `tmy_${randomBytes(32).toString("hex")}`;
|
|
305
|
-
transaction(this.db, () => {
|
|
306
|
-
this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
|
|
307
|
-
const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
|
|
308
|
-
if (exists && !reset)
|
|
309
|
-
throw new HubError(
|
|
310
|
-
409,
|
|
311
|
-
"MEMBER_EXISTS",
|
|
312
|
-
"Member exists; use member reset to replace its credential and device binding."
|
|
313
|
-
);
|
|
314
|
-
if (!exists && reset) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
315
|
-
this.db.prepare(
|
|
316
|
-
`INSERT INTO members (workspace_id, member_id, token_hash) VALUES (?, ?, ?)
|
|
317
|
-
ON CONFLICT(workspace_id, member_id) DO UPDATE SET token_hash=excluded.token_hash, device_id=NULL, enabled=1, member_revision=member_revision+1`
|
|
318
|
-
).run(workspaceId, memberId, digest(token));
|
|
319
|
-
});
|
|
320
|
-
return token;
|
|
321
|
-
}
|
|
322
|
-
revokeMember(workspaceId, memberId) {
|
|
323
|
-
const result = this.db.prepare(
|
|
324
|
-
"UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
|
|
325
|
-
).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
|
|
326
|
-
if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
327
|
-
this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
|
|
328
|
-
}
|
|
329
|
-
identity(authorization) {
|
|
330
|
-
const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
|
|
331
|
-
const row = match && this.db.prepare(
|
|
332
|
-
"SELECT m.workspace_id,m.member_id,m.device_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.token_hash=? AND m.enabled=1 AND u.enabled=1"
|
|
333
|
-
).get(digest(match[1]));
|
|
334
|
-
if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
|
|
335
|
-
return {
|
|
336
|
-
workspaceId: String(row.workspace_id),
|
|
337
|
-
memberId: String(row.member_id),
|
|
338
|
-
deviceId: row.device_id
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
bind(authorization, deviceId, projectId2) {
|
|
342
|
-
return transaction(this.db, () => {
|
|
343
|
-
const identity = this.identity(authorization);
|
|
344
|
-
if (projectId2 && projectId2 !== identity.workspaceId)
|
|
345
|
-
throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
|
|
346
|
-
if (identity.deviceId && identity.deviceId !== deviceId)
|
|
347
|
-
throw new HubError(
|
|
348
|
-
409,
|
|
349
|
-
"DEVICE_BOUND",
|
|
350
|
-
"This member is bound to another device. An administrator must reset the binding."
|
|
351
|
-
);
|
|
352
|
-
this.db.prepare("UPDATE members SET device_id=? WHERE workspace_id=? AND member_id=?").run(deviceId, identity.workspaceId, identity.memberId);
|
|
353
|
-
return BindingSchema.parse({ ...identity, deviceId });
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
authorize(authorization, deviceId) {
|
|
357
|
-
const identity = this.identity(authorization);
|
|
358
|
-
if (!identity.deviceId || identity.deviceId !== deviceId)
|
|
359
|
-
throw new HubError(
|
|
360
|
-
403,
|
|
361
|
-
"DEVICE_MISMATCH",
|
|
362
|
-
"Connect this device before using workspace APIs."
|
|
363
|
-
);
|
|
364
|
-
return BindingSchema.parse(identity);
|
|
365
|
-
}
|
|
366
|
-
members(identity) {
|
|
367
|
-
return this.db.prepare("SELECT member_id FROM members WHERE workspace_id=? ORDER BY member_id").all(identity.workspaceId).map((row) => String(row.member_id));
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
// apps/server/ai/jobs.ts
|
|
372
|
-
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
373
|
-
|
|
374
|
-
// packages/protocol/ai.ts
|
|
375
|
-
import { z as z5 } from "zod";
|
|
376
|
-
|
|
377
|
-
// packages/protocol/files.ts
|
|
378
|
-
import { z as z2 } from "zod";
|
|
379
|
-
var MAX_BLOB_BYTES = 10 * 1024 * 1024;
|
|
380
|
-
var MAX_BATCH_FILES = 200;
|
|
381
|
-
var HashSchema = z2.string().regex(/^[a-f0-9]{64}$/);
|
|
382
|
-
var RevisionSchema = z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
383
|
-
var FilePathSchema = z2.string().min(1).max(1024).refine((path) => {
|
|
384
|
-
if (path !== path.normalize("NFC") || /[\\\x00-\x1f\x7f:<>"|?*]/.test(path)) return false;
|
|
385
|
-
return path.split("/").every(
|
|
386
|
-
(part) => part.length > 0 && part.length <= 255 && part !== "." && part !== ".." && !/[. ]$/.test(part) && !/^\.teammy(?:-|$)/i.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)
|
|
387
|
-
);
|
|
388
|
-
}, "Path must be a portable, normalized relative file path.");
|
|
389
|
-
var BlobRefSchema = z2.object({
|
|
390
|
-
sha256: HashSchema,
|
|
391
|
-
size: z2.number().int().min(0).max(MAX_BLOB_BYTES)
|
|
392
|
-
});
|
|
393
|
-
var FileChangeSchema = z2.discriminatedUnion("kind", [
|
|
394
|
-
BlobRefSchema.extend({ kind: z2.literal("upsert"), path: FilePathSchema }).strict(),
|
|
395
|
-
z2.object({ kind: z2.literal("delete"), path: FilePathSchema }).strict()
|
|
396
|
-
]);
|
|
397
|
-
var FileCommitSchema = z2.object({
|
|
398
|
-
operationId: z2.uuid(),
|
|
399
|
-
parentRevision: RevisionSchema,
|
|
400
|
-
changes: z2.array(FileChangeSchema).min(1).max(MAX_BATCH_FILES)
|
|
401
|
-
}).strict().refine(
|
|
402
|
-
(batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
|
|
403
|
-
"Duplicate paths in a batch."
|
|
404
|
-
);
|
|
405
|
-
var CommitResultSchema = z2.object({ operationId: z2.uuid(), revision: RevisionSchema });
|
|
406
|
-
var FileEntrySchema = BlobRefSchema.extend({
|
|
407
|
-
path: FilePathSchema,
|
|
408
|
-
revision: RevisionSchema
|
|
409
|
-
});
|
|
410
|
-
var ManifestSchema = z2.object({
|
|
411
|
-
revision: RevisionSchema,
|
|
412
|
-
files: z2.array(FileEntrySchema),
|
|
413
|
-
nextCursor: z2.string().nullable()
|
|
414
|
-
});
|
|
415
|
-
var FileRevisionSchema = z2.object({
|
|
416
|
-
revision: RevisionSchema,
|
|
417
|
-
operationId: z2.uuid(),
|
|
418
|
-
createdAt: z2.string(),
|
|
419
|
-
changes: z2.array(FileChangeSchema)
|
|
420
|
-
});
|
|
421
|
-
var ChangesSchema = z2.object({
|
|
422
|
-
headRevision: RevisionSchema,
|
|
423
|
-
revisions: z2.array(FileRevisionSchema)
|
|
424
|
-
});
|
|
425
|
-
var BlobCheckSchema = z2.object({ hashes: z2.array(HashSchema).max(MAX_BATCH_FILES) }).strict();
|
|
426
|
-
var MissingBlobsSchema = z2.object({ missing: z2.array(HashSchema) });
|
|
427
|
-
var FileTreeEntrySchema = z2.discriminatedUnion("kind", [
|
|
428
|
-
FileEntrySchema.extend({ kind: z2.literal("file") }),
|
|
429
|
-
z2.object({
|
|
430
|
-
kind: z2.literal("directory"),
|
|
431
|
-
path: FilePathSchema,
|
|
432
|
-
fileCount: z2.number().int().positive()
|
|
433
|
-
})
|
|
434
|
-
]);
|
|
435
|
-
var FileTreeSchema = z2.object({
|
|
436
|
-
revision: RevisionSchema,
|
|
437
|
-
path: z2.union([z2.literal(""), FilePathSchema]),
|
|
438
|
-
entries: z2.array(FileTreeEntrySchema),
|
|
439
|
-
nextCursor: z2.string().nullable()
|
|
440
|
-
});
|
|
441
|
-
var FileSelectionSchema = z2.object({ revision: RevisionSchema, file: FileEntrySchema });
|
|
248
|
+
// apps/server/project.ts
|
|
249
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
442
250
|
|
|
443
251
|
// packages/protocol/project.ts
|
|
444
|
-
import { z as
|
|
445
|
-
var ServerUrlSchema =
|
|
252
|
+
import { z as z4 } from "zod";
|
|
253
|
+
var ServerUrlSchema = z4.string().url().max(2048).refine((value) => {
|
|
446
254
|
const url = new URL(value);
|
|
447
255
|
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
|
|
448
256
|
}, "Use an HTTP(S) server URL without credentials, query or fragment.");
|
|
449
|
-
var SyncPolicySchema =
|
|
450
|
-
checkIntervalSeconds:
|
|
451
|
-
fileSettleSeconds:
|
|
257
|
+
var SyncPolicySchema = z4.object({
|
|
258
|
+
checkIntervalSeconds: z4.number().int().min(1).max(86400),
|
|
259
|
+
fileSettleSeconds: z4.number().int().min(0).max(86400)
|
|
452
260
|
}).strict();
|
|
453
261
|
var DEFAULT_POLICY = { checkIntervalSeconds: 60, fileSettleSeconds: 60 };
|
|
454
|
-
var ProjectConfigSchema =
|
|
455
|
-
schemaVersion:
|
|
262
|
+
var ProjectConfigSchema = z4.object({
|
|
263
|
+
schemaVersion: z4.literal(1),
|
|
456
264
|
projectId: NameSchema,
|
|
457
|
-
projectName:
|
|
458
|
-
description:
|
|
265
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
266
|
+
description: z4.string().max(2e3),
|
|
459
267
|
publicServerUrl: ServerUrlSchema,
|
|
460
268
|
configRevision: RevisionSchema,
|
|
461
|
-
scopeProfile:
|
|
269
|
+
scopeProfile: z4.literal("workspace-and-sessions-v1"),
|
|
462
270
|
sync: SyncPolicySchema
|
|
463
271
|
}).strict();
|
|
464
|
-
var SetupSchema =
|
|
465
|
-
operationId:
|
|
466
|
-
projectName:
|
|
467
|
-
description:
|
|
272
|
+
var SetupSchema = z4.object({
|
|
273
|
+
operationId: z4.uuid(),
|
|
274
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
275
|
+
description: z4.string().max(2e3).default(""),
|
|
468
276
|
publicServerUrl: ServerUrlSchema,
|
|
469
277
|
adminId: NameSchema,
|
|
470
|
-
members:
|
|
278
|
+
members: z4.array(NameSchema).max(100).default([]),
|
|
471
279
|
sync: SyncPolicySchema.default(DEFAULT_POLICY)
|
|
472
280
|
}).strict();
|
|
473
|
-
var SetupStatusSchema =
|
|
474
|
-
initialized:
|
|
281
|
+
var SetupStatusSchema = z4.object({
|
|
282
|
+
initialized: z4.boolean()
|
|
475
283
|
});
|
|
476
284
|
var ConfigUpdateSchema = ProjectConfigSchema.pick({
|
|
477
285
|
projectName: true,
|
|
478
286
|
description: true
|
|
479
287
|
}).extend({ expectedRevision: RevisionSchema.optional() }).strict();
|
|
480
|
-
var MemberSchema =
|
|
288
|
+
var MemberSchema = z4.object({
|
|
481
289
|
id: NameSchema,
|
|
482
|
-
displayName:
|
|
483
|
-
enabled:
|
|
484
|
-
accountEnabled:
|
|
290
|
+
displayName: z4.string().max(120),
|
|
291
|
+
enabled: z4.boolean(),
|
|
292
|
+
accountEnabled: z4.boolean(),
|
|
485
293
|
revision: RevisionSchema,
|
|
486
|
-
bound:
|
|
294
|
+
bound: z4.boolean(),
|
|
487
295
|
fileRevision: RevisionSchema,
|
|
488
|
-
lastPublishedAt:
|
|
296
|
+
lastPublishedAt: z4.string().nullable()
|
|
489
297
|
});
|
|
490
|
-
var WebIdentitySchema =
|
|
298
|
+
var WebIdentitySchema = z4.object({
|
|
491
299
|
memberId: NameSchema,
|
|
492
|
-
displayName:
|
|
493
|
-
siteRole:
|
|
300
|
+
displayName: z4.string(),
|
|
301
|
+
siteRole: z4.enum(["admin", "member"])
|
|
494
302
|
});
|
|
495
303
|
var WebSessionSchema = WebIdentitySchema.extend({
|
|
496
304
|
workspaceId: NameSchema
|
|
497
305
|
});
|
|
498
|
-
var MemberCreateSchema =
|
|
306
|
+
var MemberCreateSchema = z4.object({
|
|
499
307
|
id: NameSchema
|
|
500
308
|
}).strict();
|
|
501
|
-
var MemberUpdateSchema =
|
|
309
|
+
var MemberUpdateSchema = z4.object({
|
|
502
310
|
expectedRevision: RevisionSchema,
|
|
503
|
-
displayName:
|
|
504
|
-
enabled:
|
|
311
|
+
displayName: z4.string().trim().max(120).optional(),
|
|
312
|
+
enabled: z4.boolean().optional()
|
|
505
313
|
}).strict();
|
|
506
|
-
var ProjectCreateSchema =
|
|
507
|
-
operationId:
|
|
508
|
-
projectName:
|
|
509
|
-
description:
|
|
314
|
+
var ProjectCreateSchema = z4.object({
|
|
315
|
+
operationId: z4.uuid(),
|
|
316
|
+
projectName: z4.string().trim().min(1).max(120),
|
|
317
|
+
description: z4.string().max(2e3).default("")
|
|
510
318
|
}).strict();
|
|
511
|
-
var ProjectSummarySchema =
|
|
319
|
+
var ProjectSummarySchema = z4.object({
|
|
512
320
|
projectId: NameSchema,
|
|
513
|
-
projectName:
|
|
514
|
-
description:
|
|
321
|
+
projectName: z4.string(),
|
|
322
|
+
description: z4.string()
|
|
515
323
|
});
|
|
516
|
-
var ProjectsSchema =
|
|
517
|
-
var SiteConfigSchema =
|
|
324
|
+
var ProjectsSchema = z4.object({ projects: z4.array(ProjectSummarySchema) });
|
|
325
|
+
var SiteConfigSchema = z4.object({
|
|
518
326
|
sync: SyncPolicySchema,
|
|
519
327
|
publicServerUrl: ServerUrlSchema,
|
|
520
328
|
revision: RevisionSchema
|
|
521
329
|
});
|
|
522
|
-
var SiteConfigUpdateSchema =
|
|
330
|
+
var SiteConfigUpdateSchema = z4.object({
|
|
523
331
|
publicServerUrl: ServerUrlSchema.optional(),
|
|
524
332
|
sync: SyncPolicySchema.optional(),
|
|
525
333
|
expectedRevision: RevisionSchema
|
|
@@ -527,222 +335,28 @@ var SiteConfigUpdateSchema = z3.object({
|
|
|
527
335
|
(v) => v.publicServerUrl !== void 0 || v.sync !== void 0,
|
|
528
336
|
"Provide a setting to update."
|
|
529
337
|
);
|
|
530
|
-
var UserSchema =
|
|
338
|
+
var UserSchema = z4.object({
|
|
531
339
|
id: NameSchema,
|
|
532
|
-
displayName:
|
|
533
|
-
siteRole:
|
|
534
|
-
enabled:
|
|
340
|
+
displayName: z4.string(),
|
|
341
|
+
siteRole: z4.enum(["admin", "member"]),
|
|
342
|
+
enabled: z4.boolean(),
|
|
535
343
|
revision: RevisionSchema
|
|
536
344
|
});
|
|
537
|
-
var UserCreateSchema =
|
|
345
|
+
var UserCreateSchema = z4.object({
|
|
538
346
|
id: NameSchema,
|
|
539
|
-
displayName:
|
|
540
|
-
siteRole:
|
|
347
|
+
displayName: z4.string().trim().max(120).default(""),
|
|
348
|
+
siteRole: z4.enum(["admin", "member"]).default("member")
|
|
541
349
|
}).strict();
|
|
542
|
-
var UserUpdateSchema =
|
|
350
|
+
var UserUpdateSchema = z4.object({
|
|
543
351
|
expectedRevision: RevisionSchema,
|
|
544
|
-
displayName:
|
|
545
|
-
enabled:
|
|
546
|
-
siteRole:
|
|
547
|
-
}).strict();
|
|
548
|
-
|
|
549
|
-
// packages/protocol/tasks.ts
|
|
550
|
-
import { z as z4 } from "zod";
|
|
551
|
-
var TaskStatusSchema = z4.enum(["planned", "in_progress", "in_review", "completed"]);
|
|
552
|
-
var TaskFieldsSchema = z4.object({
|
|
553
|
-
title: z4.string().trim().min(1).max(200),
|
|
554
|
-
goal: z4.string().trim().min(1).max(2e3).nullable(),
|
|
555
|
-
status: TaskStatusSchema.nullable(),
|
|
556
|
-
progress: z4.string().trim().min(1).max(2e3)
|
|
352
|
+
displayName: z4.string().trim().max(120).optional(),
|
|
353
|
+
enabled: z4.boolean().optional(),
|
|
354
|
+
siteRole: z4.enum(["admin", "member"]).optional()
|
|
557
355
|
}).strict();
|
|
558
|
-
var TaskSourceSchema = z4.object({
|
|
559
|
-
path: FilePathSchema,
|
|
560
|
-
sha256: HashSchema,
|
|
561
|
-
revision: RevisionSchema,
|
|
562
|
-
syncedAt: z4.string()
|
|
563
|
-
});
|
|
564
|
-
var TaskCardSchema = TaskFieldsSchema.extend({
|
|
565
|
-
id: z4.uuid(),
|
|
566
|
-
owner: NameSchema,
|
|
567
|
-
jobId: z4.uuid(),
|
|
568
|
-
sources: z4.array(TaskSourceSchema).min(1),
|
|
569
|
-
syncedAt: z4.string(),
|
|
570
|
-
corrected: z4.boolean(),
|
|
571
|
-
needsReview: z4.boolean()
|
|
572
|
-
});
|
|
573
|
-
var TaskCorrectionSchema = z4.object({
|
|
574
|
-
title: TaskFieldsSchema.shape.title.optional(),
|
|
575
|
-
status: TaskStatusSchema.nullable().optional()
|
|
576
|
-
}).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
|
|
577
|
-
var TaskBoardSchema = z4.object({
|
|
578
|
-
tasks: z4.array(TaskCardSchema),
|
|
579
|
-
members: z4.array(
|
|
580
|
-
z4.object({
|
|
581
|
-
owner: NameSchema,
|
|
582
|
-
jobId: z4.uuid().nullable(),
|
|
583
|
-
status: z4.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
|
|
584
|
-
error: z4.string().nullable(),
|
|
585
|
-
sourceChanged: z4.boolean(),
|
|
586
|
-
partial: z4.boolean(),
|
|
587
|
-
uncertainties: z4.array(z4.string())
|
|
588
|
-
})
|
|
589
|
-
),
|
|
590
|
-
modelAvailable: z4.boolean()
|
|
591
|
-
});
|
|
592
|
-
var TaskBootstrapRequestSchema = z4.object({ operationId: z4.uuid(), owner: NameSchema }).strict();
|
|
593
|
-
var TaskBootstrapResponseSchema = z4.object({
|
|
594
|
-
jobId: z4.uuid().nullable(),
|
|
595
|
-
empty: z4.boolean()
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// packages/protocol/ai.ts
|
|
599
|
-
var AiProfileSchema = z5.object({
|
|
600
|
-
id: NameSchema,
|
|
601
|
-
name: z5.string().trim().min(1).max(120),
|
|
602
|
-
protocol: z5.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
|
|
603
|
-
baseUrl: ServerUrlSchema,
|
|
604
|
-
model: z5.string().trim().min(1).max(200),
|
|
605
|
-
enabled: z5.boolean().default(true),
|
|
606
|
-
contextWindow: z5.number().int().min(4096).max(2e6).default(32e3),
|
|
607
|
-
maxOutputTokens: z5.number().int().min(128).max(16e3).default(2048)
|
|
608
|
-
}).strict().refine(
|
|
609
|
-
(p) => p.maxOutputTokens < p.contextWindow,
|
|
610
|
-
"Output budget must be smaller than context window."
|
|
611
|
-
);
|
|
612
|
-
var AiPublicProfileSchema = AiProfileSchema.safeExtend({
|
|
613
|
-
revision: z5.number().int(),
|
|
614
|
-
keyConfigured: z5.boolean()
|
|
615
|
-
});
|
|
616
|
-
var AiProfileUpdateSchema = z5.object({
|
|
617
|
-
expectedRevision: z5.number().int().nonnegative(),
|
|
618
|
-
profile: AiProfileSchema,
|
|
619
|
-
apiKey: z5.string().min(1).max(4096).nullable().optional()
|
|
620
|
-
}).strict();
|
|
621
|
-
var AiProfilesSchema = z5.object({ profiles: z5.array(AiPublicProfileSchema) });
|
|
622
|
-
var AiDefaultUpdateSchema = z5.object({
|
|
623
|
-
expectedRevision: z5.number().int().nonnegative(),
|
|
624
|
-
profileId: NameSchema.nullable()
|
|
625
|
-
}).strict();
|
|
626
|
-
var AiDefaultSchema = z5.object({
|
|
627
|
-
revision: z5.number().int().nonnegative(),
|
|
628
|
-
profileId: NameSchema.nullable(),
|
|
629
|
-
effectiveProfileId: NameSchema.nullable(),
|
|
630
|
-
source: z5.enum(["site", "project", "none"]),
|
|
631
|
-
available: z5.boolean()
|
|
632
|
-
});
|
|
633
|
-
var AiProbeSchema = z5.object({ mode: z5.enum(["text", "tools", "json"]) }).strict();
|
|
634
|
-
var AiProbeResultSchema = z5.object({
|
|
635
|
-
ok: z5.literal(true),
|
|
636
|
-
mode: AiProbeSchema.shape.mode,
|
|
637
|
-
streamed: z5.boolean(),
|
|
638
|
-
inputTokens: z5.number(),
|
|
639
|
-
outputTokens: z5.number()
|
|
640
|
-
});
|
|
641
|
-
var AiSelectionSchema = z5.object({
|
|
642
|
-
owner: NameSchema,
|
|
643
|
-
kind: z5.enum(["file", "directory"]),
|
|
644
|
-
path: z5.union([z5.literal(""), FilePathSchema]),
|
|
645
|
-
revision: z5.number().int().nonnegative().optional()
|
|
646
|
-
}).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
|
|
647
|
-
var AiJobCreateSchema = z5.object({
|
|
648
|
-
operationId: z5.uuid(),
|
|
649
|
-
profileId: NameSchema.optional(),
|
|
650
|
-
workflow: z5.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
|
|
651
|
-
selection: AiSelectionSchema,
|
|
652
|
-
question: z5.string().trim().max(2e3).default("")
|
|
653
|
-
}).strict();
|
|
654
|
-
var AiCitationSchema = z5.object({
|
|
655
|
-
path: FilePathSchema,
|
|
656
|
-
startLine: z5.number().int().positive(),
|
|
657
|
-
endLine: z5.number().int().positive(),
|
|
658
|
-
sha256: HashSchema
|
|
659
|
-
});
|
|
660
|
-
var AiCoverageSchema = z5.object({
|
|
661
|
-
totalFiles: z5.number().int(),
|
|
662
|
-
readFiles: z5.array(FilePathSchema),
|
|
663
|
-
partial: z5.boolean(),
|
|
664
|
-
limitations: z5.array(z5.string())
|
|
665
|
-
});
|
|
666
|
-
var AiResultSchema = z5.object({
|
|
667
|
-
summary: z5.string().min(1).max(2e4),
|
|
668
|
-
findings: z5.array(z5.string().max(2e3)).max(30),
|
|
669
|
-
uncertainties: z5.array(z5.string().max(2e3)).max(30),
|
|
670
|
-
citations: z5.array(AiCitationSchema).max(50),
|
|
671
|
-
coverage: AiCoverageSchema
|
|
672
|
-
});
|
|
673
|
-
var TaskAnalysisResultSchema = z5.object({
|
|
674
|
-
kind: z5.literal("task-bootstrap"),
|
|
675
|
-
tasks: z5.array(
|
|
676
|
-
TaskFieldsSchema.extend({
|
|
677
|
-
taskId: z5.uuid().nullable().optional(),
|
|
678
|
-
sources: z5.array(TaskSourceSchema).min(1).max(50)
|
|
679
|
-
})
|
|
680
|
-
).max(100),
|
|
681
|
-
uncertainties: z5.array(z5.string().max(2e3)).max(50),
|
|
682
|
-
coverage: AiCoverageSchema
|
|
683
|
-
});
|
|
684
|
-
var AiWorkflowResultSchema = z5.union([AiResultSchema, TaskAnalysisResultSchema]);
|
|
685
|
-
var AiUsageSchema = z5.object({
|
|
686
|
-
inputTokens: z5.number().nonnegative(),
|
|
687
|
-
outputTokens: z5.number().nonnegative()
|
|
688
|
-
});
|
|
689
|
-
var AiJobSchema = z5.object({
|
|
690
|
-
id: z5.uuid(),
|
|
691
|
-
requester: NameSchema,
|
|
692
|
-
workflow: AiJobCreateSchema.shape.workflow,
|
|
693
|
-
question: z5.string(),
|
|
694
|
-
profileId: NameSchema,
|
|
695
|
-
model: z5.string(),
|
|
696
|
-
profileRevision: z5.number().int(),
|
|
697
|
-
promptVersion: z5.string(),
|
|
698
|
-
origin: z5.enum(["manual", "automatic"]).default("manual"),
|
|
699
|
-
status: z5.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
|
|
700
|
-
createdAt: z5.string(),
|
|
701
|
-
updatedAt: z5.string(),
|
|
702
|
-
attemptCount: z5.number().int(),
|
|
703
|
-
error: z5.string().nullable(),
|
|
704
|
-
input: z5.object({
|
|
705
|
-
owner: NameSchema,
|
|
706
|
-
kind: z5.enum(["file", "directory"]),
|
|
707
|
-
path: z5.string(),
|
|
708
|
-
revision: z5.number().int(),
|
|
709
|
-
fileCount: z5.number().int(),
|
|
710
|
-
fingerprint: HashSchema
|
|
711
|
-
}),
|
|
712
|
-
result: AiWorkflowResultSchema.nullable(),
|
|
713
|
-
sourceChanged: z5.boolean()
|
|
714
|
-
});
|
|
715
|
-
var AiJobsSchema = z5.object({
|
|
716
|
-
jobs: z5.array(AiJobSchema),
|
|
717
|
-
nextCursor: z5.string().nullable()
|
|
718
|
-
});
|
|
719
|
-
var AiEventSchema = z5.object({
|
|
720
|
-
seq: z5.number().int(),
|
|
721
|
-
attempt: z5.number().int(),
|
|
722
|
-
kind: z5.string(),
|
|
723
|
-
message: z5.string(),
|
|
724
|
-
createdAt: z5.string()
|
|
725
|
-
});
|
|
726
|
-
var AiEventsSchema = z5.object({
|
|
727
|
-
events: z5.array(AiEventSchema),
|
|
728
|
-
nextCursor: z5.number().int()
|
|
729
|
-
});
|
|
730
|
-
var AiAttemptSchema = z5.object({
|
|
731
|
-
number: z5.number().int(),
|
|
732
|
-
status: z5.string(),
|
|
733
|
-
startedAt: z5.string(),
|
|
734
|
-
endedAt: z5.string().nullable(),
|
|
735
|
-
error: z5.string().nullable(),
|
|
736
|
-
usage: AiUsageSchema.nullable()
|
|
737
|
-
});
|
|
738
|
-
var AiJobDetailSchema = z5.object({ job: AiJobSchema, attempts: z5.array(AiAttemptSchema) });
|
|
739
|
-
|
|
740
|
-
// apps/server/project.ts
|
|
741
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
742
356
|
|
|
743
357
|
// apps/server/site.ts
|
|
744
|
-
import { randomBytes
|
|
745
|
-
var
|
|
358
|
+
import { randomBytes, createHash as createHash2 } from "node:crypto";
|
|
359
|
+
var digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
746
360
|
function requireSiteAdmin(identity) {
|
|
747
361
|
if (identity.siteRole !== "admin")
|
|
748
362
|
throw new HubError(403, "SITE_ADMIN_REQUIRED", "Site administrator access required.");
|
|
@@ -791,15 +405,15 @@ var SiteRepository = class {
|
|
|
791
405
|
const row = this.hub.db.prepare("SELECT 1 FROM users WHERE id=? AND enabled=1").get(id);
|
|
792
406
|
if (!row)
|
|
793
407
|
throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
|
|
794
|
-
const token =
|
|
408
|
+
const token = randomBytes(32).toString("hex");
|
|
795
409
|
this.hub.db.prepare("DELETE FROM web_sessions WHERE expires_at<=?").run(Date.now());
|
|
796
|
-
this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?)").run(
|
|
410
|
+
this.hub.db.prepare("INSERT INTO web_sessions VALUES (?,?,?)").run(digest(token), id, Date.now() + 7 * 864e5);
|
|
797
411
|
return { token, session: this.identity(token) };
|
|
798
412
|
}
|
|
799
413
|
identity(token) {
|
|
800
414
|
const row = token && /^[a-f0-9]{64}$/.test(token) ? this.hub.db.prepare(
|
|
801
415
|
`SELECT u.* FROM web_sessions s JOIN users u ON u.id=s.member_id WHERE s.token_hash=? AND s.expires_at>? AND u.enabled=1`
|
|
802
|
-
).get(
|
|
416
|
+
).get(digest(token), Date.now()) : void 0;
|
|
803
417
|
if (!row)
|
|
804
418
|
throw new HubError(401, "LOGIN_REQUIRED", "Please sign in with an existing member ID.");
|
|
805
419
|
return {
|
|
@@ -810,7 +424,7 @@ var SiteRepository = class {
|
|
|
810
424
|
}
|
|
811
425
|
logout(token) {
|
|
812
426
|
if (token)
|
|
813
|
-
this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(
|
|
427
|
+
this.hub.db.prepare("DELETE FROM web_sessions WHERE token_hash=?").run(digest(token));
|
|
814
428
|
}
|
|
815
429
|
users(identity) {
|
|
816
430
|
requireSiteAdmin(identity);
|
|
@@ -881,7 +495,7 @@ var ProjectRepository = class {
|
|
|
881
495
|
return ProjectConfigSchema.parse(JSON.parse(String(row.config_json)));
|
|
882
496
|
}
|
|
883
497
|
insertProject(value, creator, url) {
|
|
884
|
-
const fingerprint =
|
|
498
|
+
const fingerprint = digest(JSON.stringify([creator, value]));
|
|
885
499
|
const prior = this.hub.db.prepare("SELECT id,fingerprint FROM projects WHERE operation_id=?").get(value.operationId);
|
|
886
500
|
if (prior) {
|
|
887
501
|
if (prior.fingerprint !== fingerprint)
|
|
@@ -894,7 +508,7 @@ var ProjectRepository = class {
|
|
|
894
508
|
}
|
|
895
509
|
const config = ProjectConfigSchema.parse({
|
|
896
510
|
schemaVersion: 1,
|
|
897
|
-
projectId: `proj-${
|
|
511
|
+
projectId: `proj-${randomBytes2(8).toString("hex")}`,
|
|
898
512
|
projectName: value.projectName,
|
|
899
513
|
description: value.description,
|
|
900
514
|
publicServerUrl: url,
|
|
@@ -904,11 +518,12 @@ var ProjectRepository = class {
|
|
|
904
518
|
});
|
|
905
519
|
this.hub.db.prepare("INSERT INTO projects VALUES (?,?,?,?)").run(config.projectId, JSON.stringify(config), value.operationId, fingerprint);
|
|
906
520
|
this.hub.db.prepare("INSERT INTO members(workspace_id,member_id) VALUES (?,?)").run(config.projectId, creator);
|
|
521
|
+
this.hub.db.prepare("INSERT INTO project_files(workspace_id,maintainer,paths_json) VALUES (?,?,?)").run(config.projectId, creator, JSON.stringify(DEFAULT_SHARED_PATHS));
|
|
907
522
|
return config;
|
|
908
523
|
}
|
|
909
524
|
setup(input) {
|
|
910
525
|
const value = SetupSchema.parse(input);
|
|
911
|
-
const fingerprint =
|
|
526
|
+
const fingerprint = digest(JSON.stringify(value));
|
|
912
527
|
return transaction(this.hub.db, () => {
|
|
913
528
|
const prior = this.hub.db.prepare("SELECT setup_operation,setup_fingerprint FROM site_settings").get();
|
|
914
529
|
if (prior) {
|
|
@@ -1084,236 +699,60 @@ var ProjectRepository = class {
|
|
|
1084
699
|
}
|
|
1085
700
|
};
|
|
1086
701
|
|
|
1087
|
-
// apps/server/
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
702
|
+
// apps/server/shared-files.ts
|
|
703
|
+
var sharedFilesMigration = `
|
|
704
|
+
CREATE TABLE project_files (
|
|
705
|
+
workspace_id TEXT PRIMARY KEY, maintainer TEXT, paths_json TEXT NOT NULL,
|
|
706
|
+
config_revision INTEGER NOT NULL DEFAULT 1, file_revision INTEGER NOT NULL DEFAULT 0
|
|
707
|
+
);
|
|
708
|
+
INSERT INTO project_files(workspace_id,maintainer,paths_json)
|
|
709
|
+
SELECT p.id,(SELECT member_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=p.id AND m.enabled=1 AND u.enabled=1 ORDER BY m.rowid LIMIT 1),'["README.md","_template/"]' FROM projects p;
|
|
710
|
+
`;
|
|
711
|
+
var SharedFilesRepository = class {
|
|
1092
712
|
constructor(hub) {
|
|
1093
713
|
this.hub = hub;
|
|
1094
714
|
}
|
|
1095
715
|
hub;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const key = readFileSync(path);
|
|
1106
|
-
if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
|
|
1107
|
-
return key;
|
|
1108
|
-
}
|
|
1109
|
-
encrypt(value, id) {
|
|
1110
|
-
const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
|
|
1111
|
-
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1112
|
-
const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
1113
|
-
return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
|
|
1114
|
-
}
|
|
1115
|
-
list() {
|
|
1116
|
-
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
|
|
1117
|
-
...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
|
|
1118
|
-
revision: Number(row.revision),
|
|
1119
|
-
keyConfigured: !!row.secret
|
|
1120
|
-
}));
|
|
1121
|
-
}
|
|
1122
|
-
get(id) {
|
|
1123
|
-
const profile = this.list().find((p) => p.id === id);
|
|
1124
|
-
if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
|
|
1125
|
-
const { keyConfigured: _, ...result } = profile;
|
|
1126
|
-
return result;
|
|
1127
|
-
}
|
|
1128
|
-
secret(id) {
|
|
1129
|
-
const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
|
|
1130
|
-
if (!row?.secret) return "";
|
|
1131
|
-
const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
|
|
1132
|
-
const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
|
|
1133
|
-
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1134
|
-
cipher.setAuthTag(tag);
|
|
1135
|
-
return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
|
|
1136
|
-
}
|
|
1137
|
-
save(session, value) {
|
|
1138
|
-
requireSiteAdmin(session);
|
|
1139
|
-
const input = AiProfileUpdateSchema.parse(value), { profile } = input;
|
|
1140
|
-
return transaction(this.hub.db, () => {
|
|
1141
|
-
const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
|
|
1142
|
-
if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
|
|
1143
|
-
throw new HubError(
|
|
1144
|
-
409,
|
|
1145
|
-
"AI_CONFIG_CONFLICT",
|
|
1146
|
-
"Model configuration changed; reload before saving."
|
|
1147
|
-
);
|
|
1148
|
-
if (!old && this.list().length >= 20)
|
|
1149
|
-
throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
|
|
1150
|
-
const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
|
|
1151
|
-
this.hub.db.prepare(
|
|
1152
|
-
"INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
|
|
1153
|
-
).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
|
|
1154
|
-
return this.list().find((p) => p.id === profile.id);
|
|
716
|
+
config(identity) {
|
|
717
|
+
new ProjectRepository(this.hub).config(identity);
|
|
718
|
+
const row = this.hub.db.prepare("SELECT * FROM project_files WHERE workspace_id=?").get(identity.workspaceId);
|
|
719
|
+
if (!row)
|
|
720
|
+
throw new HubError(500, "SHARED_CONFIG_MISSING", "Project file settings are missing.");
|
|
721
|
+
return SharedFilesConfigSchema.parse({
|
|
722
|
+
maintainer: row.maintainer,
|
|
723
|
+
paths: JSON.parse(String(row.paths_json)),
|
|
724
|
+
revision: row.config_revision
|
|
1155
725
|
});
|
|
1156
726
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
hub;
|
|
1165
|
-
read(scope) {
|
|
1166
|
-
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
|
|
1167
|
-
return {
|
|
1168
|
-
profileId: row?.profile_id ? String(row.profile_id) : null,
|
|
1169
|
-
revision: Number(row?.revision ?? 0)
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
view(projectId2) {
|
|
1173
|
-
const site = this.read("site");
|
|
1174
|
-
const own = projectId2 ? this.read(`project:${projectId2}`) : site;
|
|
1175
|
-
const effectiveProfileId = own.profileId ?? site.profileId;
|
|
1176
|
-
return {
|
|
1177
|
-
...own,
|
|
1178
|
-
effectiveProfileId,
|
|
1179
|
-
source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
|
|
1180
|
-
available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
site() {
|
|
1184
|
-
return this.view();
|
|
1185
|
-
}
|
|
1186
|
-
project(projectId2) {
|
|
1187
|
-
return this.view(projectId2);
|
|
1188
|
-
}
|
|
1189
|
-
save(scope, value) {
|
|
1190
|
-
const input = AiDefaultUpdateSchema.parse(value);
|
|
1191
|
-
transaction(this.hub.db, () => {
|
|
1192
|
-
if (this.read(scope).revision !== input.expectedRevision)
|
|
727
|
+
update(identity, input) {
|
|
728
|
+
new ProjectRepository(this.hub).requireMember(identity);
|
|
729
|
+
const value = SharedFilesUpdateSchema.parse(input);
|
|
730
|
+
return transaction(this.hub.db, () => {
|
|
731
|
+
const current = this.config(identity);
|
|
732
|
+
if (current.revision !== value.expectedRevision)
|
|
1193
733
|
throw new HubError(
|
|
1194
734
|
409,
|
|
1195
|
-
"
|
|
1196
|
-
"
|
|
735
|
+
"STALE_CONFIG",
|
|
736
|
+
"Shared file settings changed; reload before saving."
|
|
1197
737
|
);
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
738
|
+
if (value.maintainer && !this.hub.db.prepare(
|
|
739
|
+
"SELECT 1 FROM members m JOIN users u ON u.id=m.member_id WHERE m.workspace_id=? AND m.member_id=? AND m.enabled=1 AND u.enabled=1"
|
|
740
|
+
).get(identity.workspaceId, value.maintainer))
|
|
741
|
+
throw new HubError(400, "INVALID_MAINTAINER", "Choose an active project member.");
|
|
742
|
+
if (current.maintainer !== value.maintainer || JSON.stringify(current.paths) !== JSON.stringify(value.paths))
|
|
743
|
+
this.hub.db.prepare(
|
|
744
|
+
"UPDATE project_files SET maintainer=?,paths_json=?,config_revision=config_revision+1 WHERE workspace_id=?"
|
|
745
|
+
).run(value.maintainer, JSON.stringify(value.paths), identity.workspaceId);
|
|
746
|
+
return this.config(identity);
|
|
1203
747
|
});
|
|
1204
748
|
}
|
|
1205
|
-
saveSite(identity, value) {
|
|
1206
|
-
requireSiteAdmin(identity);
|
|
1207
|
-
this.save("site", value);
|
|
1208
|
-
return this.site();
|
|
1209
|
-
}
|
|
1210
|
-
saveProject(identity, value) {
|
|
1211
|
-
new ProjectRepository(this.hub).requireMember(identity);
|
|
1212
|
-
this.save(`project:${identity.workspaceId}`, value);
|
|
1213
|
-
return this.project(identity.workspaceId);
|
|
1214
|
-
}
|
|
1215
|
-
resolve(projectId2, explicit) {
|
|
1216
|
-
const id = explicit ?? this.project(projectId2).effectiveProfileId;
|
|
1217
|
-
if (!id)
|
|
1218
|
-
throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
|
|
1219
|
-
const profile = new AiConfig(this.hub).get(id);
|
|
1220
|
-
if (!profile.enabled)
|
|
1221
|
-
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1222
|
-
return profile;
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
|
|
1226
|
-
// apps/server/ai/snapshots.ts
|
|
1227
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
1228
|
-
|
|
1229
|
-
// apps/server/files.ts
|
|
1230
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1231
|
-
import { join as join4 } from "node:path";
|
|
1232
|
-
|
|
1233
|
-
// packages/storage/local-blob-store.ts
|
|
1234
|
-
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
1235
|
-
import { link, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
1236
|
-
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
1237
|
-
function hash(content) {
|
|
1238
|
-
return createHash3("sha256").update(content).digest("hex");
|
|
1239
|
-
}
|
|
1240
|
-
function hasCode(error, code) {
|
|
1241
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
1242
|
-
}
|
|
1243
|
-
var BlobIntegrityError = class extends Error {
|
|
1244
|
-
constructor(sha256) {
|
|
1245
|
-
super(`Stored content failed integrity verification: ${sha256}`);
|
|
1246
|
-
this.sha256 = sha256;
|
|
1247
|
-
this.name = "BlobIntegrityError";
|
|
1248
|
-
}
|
|
1249
|
-
sha256;
|
|
1250
|
-
};
|
|
1251
|
-
var LocalBlobStore = class {
|
|
1252
|
-
root;
|
|
1253
|
-
constructor(root) {
|
|
1254
|
-
this.root = resolve(root);
|
|
1255
|
-
}
|
|
1256
|
-
async put(content) {
|
|
1257
|
-
const bytes = Buffer.from(content);
|
|
1258
|
-
const sha256 = hash(bytes);
|
|
1259
|
-
const target = this.pathFor(sha256);
|
|
1260
|
-
await mkdir(dirname2(target), { recursive: true, mode: 448 });
|
|
1261
|
-
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
1262
|
-
const file = await open(temporary, "wx", 384);
|
|
1263
|
-
try {
|
|
1264
|
-
try {
|
|
1265
|
-
await file.writeFile(bytes);
|
|
1266
|
-
await file.sync();
|
|
1267
|
-
} finally {
|
|
1268
|
-
await file.close();
|
|
1269
|
-
}
|
|
1270
|
-
try {
|
|
1271
|
-
await link(temporary, target);
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
if (!hasCode(error, "EEXIST")) throw error;
|
|
1274
|
-
await this.get(sha256);
|
|
1275
|
-
}
|
|
1276
|
-
if (process.platform !== "win32") {
|
|
1277
|
-
let directory = dirname2(target);
|
|
1278
|
-
for (; ; ) {
|
|
1279
|
-
const handle = await open(directory, "r");
|
|
1280
|
-
try {
|
|
1281
|
-
await handle.sync();
|
|
1282
|
-
} finally {
|
|
1283
|
-
await handle.close();
|
|
1284
|
-
}
|
|
1285
|
-
if (directory === dirname2(this.root)) break;
|
|
1286
|
-
directory = dirname2(directory);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
} finally {
|
|
1290
|
-
await unlink(temporary);
|
|
1291
|
-
}
|
|
1292
|
-
return { sha256, size: bytes.byteLength };
|
|
1293
|
-
}
|
|
1294
|
-
async get(sha256) {
|
|
1295
|
-
const target = this.pathFor(sha256);
|
|
1296
|
-
let content;
|
|
1297
|
-
try {
|
|
1298
|
-
content = await readFile(target);
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
if (hasCode(error, "ENOENT")) return void 0;
|
|
1301
|
-
throw error;
|
|
1302
|
-
}
|
|
1303
|
-
if (hash(content) !== sha256) throw new BlobIntegrityError(sha256);
|
|
1304
|
-
return content;
|
|
1305
|
-
}
|
|
1306
|
-
pathFor(sha256) {
|
|
1307
|
-
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
1308
|
-
return join3(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
1309
|
-
}
|
|
1310
749
|
};
|
|
1311
750
|
|
|
1312
751
|
// apps/server/files.ts
|
|
1313
752
|
var FileRepository = class {
|
|
1314
753
|
constructor(hub) {
|
|
1315
754
|
this.hub = hub;
|
|
1316
|
-
this.blobs = new LocalBlobStore(
|
|
755
|
+
this.blobs = new LocalBlobStore(join2(hub.directory, "blobs"));
|
|
1317
756
|
}
|
|
1318
757
|
hub;
|
|
1319
758
|
blobs;
|
|
@@ -1325,7 +764,7 @@ var FileRepository = class {
|
|
|
1325
764
|
}
|
|
1326
765
|
async upload(authorization, device, hash2, bytes) {
|
|
1327
766
|
this.hub.authorize(authorization, device);
|
|
1328
|
-
if (
|
|
767
|
+
if (createHash3("sha256").update(bytes).digest("hex") !== HashSchema.parse(hash2))
|
|
1329
768
|
throw new HubError(400, "HASH_MISMATCH", "Content does not match its SHA-256.");
|
|
1330
769
|
const ref = await this.blobs.put(bytes);
|
|
1331
770
|
transaction(this.hub.db, () => {
|
|
@@ -1342,19 +781,41 @@ var FileRepository = class {
|
|
|
1342
781
|
return bytes;
|
|
1343
782
|
}
|
|
1344
783
|
head(identity, owner) {
|
|
784
|
+
if (owner === PROJECT_FILE_OWNER) {
|
|
785
|
+
new SharedFilesRepository(this.hub).config(identity);
|
|
786
|
+
return Number(
|
|
787
|
+
this.hub.db.prepare("SELECT file_revision FROM project_files WHERE workspace_id=?").get(identity.workspaceId).file_revision
|
|
788
|
+
);
|
|
789
|
+
}
|
|
1345
790
|
const row = this.hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(identity.workspaceId, owner);
|
|
1346
791
|
if (!row) throw new HubError(404, "MEMBER_NOT_FOUND", "Member is not part of this workspace.");
|
|
1347
792
|
return Number(row.file_revision);
|
|
1348
793
|
}
|
|
1349
|
-
commit(authorization, device, input) {
|
|
794
|
+
commit(authorization, device, input, sharedConfigRevision) {
|
|
1350
795
|
const batch = FileCommitSchema.parse(input);
|
|
796
|
+
if (batch.changes.some((change) => privateWorkspacePath(change.path)))
|
|
797
|
+
throw new HubError(400, "PRIVATE_PATH", "Teammy configuration is not a shared document.");
|
|
1351
798
|
const changes = [...batch.changes].sort(
|
|
1352
799
|
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
1353
800
|
);
|
|
1354
|
-
const fingerprint =
|
|
801
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify({ parentRevision: batch.parentRevision, changes })).digest("hex");
|
|
1355
802
|
return transaction(this.hub.db, () => {
|
|
1356
803
|
const identity = this.hub.authorize(authorization, device);
|
|
1357
|
-
const
|
|
804
|
+
const workspace = identity.workspaceId;
|
|
805
|
+
const owner = sharedConfigRevision === void 0 ? identity.memberId : PROJECT_FILE_OWNER;
|
|
806
|
+
if (owner === PROJECT_FILE_OWNER) {
|
|
807
|
+
const config = new SharedFilesRepository(this.hub).config(identity);
|
|
808
|
+
if (config.maintainer !== identity.memberId)
|
|
809
|
+
throw new HubError(
|
|
810
|
+
403,
|
|
811
|
+
"SHARED_FILES_READ_ONLY",
|
|
812
|
+
"Only the designated maintainer can publish project files."
|
|
813
|
+
);
|
|
814
|
+
if (config.revision !== sharedConfigRevision)
|
|
815
|
+
throw new HubError(409, "STALE_CONFIG", "Shared file settings changed.");
|
|
816
|
+
if (batch.changes.some((change) => !sharedPathAllowed(change.path, config.paths)))
|
|
817
|
+
throw new HubError(403, "SHARED_PATH_DENIED", "Path is outside the shared file list.");
|
|
818
|
+
}
|
|
1358
819
|
const prior = this.hub.db.prepare(
|
|
1359
820
|
"SELECT fingerprint, revision FROM file_operations WHERE workspace_id=? AND owner=? AND operation_id=?"
|
|
1360
821
|
).get(workspace, owner, batch.operationId);
|
|
@@ -1451,7 +912,10 @@ var FileRepository = class {
|
|
|
1451
912
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1452
913
|
JSON.stringify(effective)
|
|
1453
914
|
);
|
|
1454
|
-
|
|
915
|
+
if (owner === PROJECT_FILE_OWNER)
|
|
916
|
+
this.hub.db.prepare("UPDATE project_files SET file_revision=? WHERE workspace_id=?").run(revision, workspace);
|
|
917
|
+
else
|
|
918
|
+
this.hub.db.prepare("UPDATE members SET file_revision=? WHERE workspace_id=? AND member_id=?").run(revision, workspace, owner);
|
|
1455
919
|
}
|
|
1456
920
|
this.hub.db.prepare("INSERT INTO file_operations VALUES (?, ?, ?, ?, ?)").run(workspace, owner, batch.operationId, fingerprint, revision);
|
|
1457
921
|
return { operationId: batch.operationId, revision };
|
|
@@ -1567,10 +1031,15 @@ var FileRepository = class {
|
|
|
1567
1031
|
};
|
|
1568
1032
|
|
|
1569
1033
|
// apps/server/ai/snapshots.ts
|
|
1034
|
+
function taskSnapshotCurrent(hub, workspace, snapshot) {
|
|
1035
|
+
const member = hub.db.prepare("SELECT file_revision FROM members WHERE workspace_id=? AND member_id=?").get(workspace, snapshot.owner);
|
|
1036
|
+
const project = hub.db.prepare("SELECT file_revision FROM project_files WHERE workspace_id=?").get(workspace);
|
|
1037
|
+
return Number(member?.file_revision) === snapshot.revision && Number(project?.file_revision ?? 0) === (snapshot.project?.revision ?? 0);
|
|
1038
|
+
}
|
|
1570
1039
|
function snapshotDigest(files) {
|
|
1571
|
-
return
|
|
1040
|
+
return createHash4("sha256").update(JSON.stringify(files.map((f) => [f.path, f.sha256]))).digest("hex");
|
|
1572
1041
|
}
|
|
1573
|
-
function
|
|
1042
|
+
function captureScope(hub, identity, selection, allowEmpty = false) {
|
|
1574
1043
|
const repository = new FileRepository(hub);
|
|
1575
1044
|
const files = [];
|
|
1576
1045
|
let cursor = "";
|
|
@@ -1600,158 +1069,832 @@ function captureSnapshot(hub, identity, selection, allowEmpty = false) {
|
|
|
1600
1069
|
fingerprint: snapshotDigest(files)
|
|
1601
1070
|
};
|
|
1602
1071
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1072
|
+
function captureSnapshot(hub, identity, selection, options = {}) {
|
|
1073
|
+
const snapshot = captureScope(hub, identity, selection, options.allowEmpty);
|
|
1074
|
+
if (options.projectReferences && hub.db.prepare("SELECT 1 FROM project_files WHERE workspace_id=?").get(identity.workspaceId)) {
|
|
1075
|
+
const shared = captureScope(
|
|
1076
|
+
hub,
|
|
1077
|
+
identity,
|
|
1078
|
+
{ owner: PROJECT_FILE_OWNER, kind: "directory", path: "" },
|
|
1079
|
+
true
|
|
1080
|
+
);
|
|
1081
|
+
snapshot.project = {
|
|
1082
|
+
revision: shared.revision,
|
|
1083
|
+
files: shared.files,
|
|
1084
|
+
fingerprint: shared.fingerprint
|
|
1085
|
+
};
|
|
1086
|
+
if (shared.revision)
|
|
1087
|
+
snapshot.fingerprint = createHash4("sha256").update(snapshot.fingerprint + ":" + shared.fingerprint).digest("hex");
|
|
1088
|
+
}
|
|
1089
|
+
return snapshot;
|
|
1090
|
+
}
|
|
1091
|
+
var SnapshotReader = class _SnapshotReader {
|
|
1092
|
+
constructor(hub, identity, snapshot, budget = 6e4, sharedBudget) {
|
|
1093
|
+
this.hub = hub;
|
|
1094
|
+
this.identity = identity;
|
|
1095
|
+
this.snapshot = snapshot;
|
|
1096
|
+
this.budget = sharedBudget ?? { remaining: budget };
|
|
1097
|
+
}
|
|
1098
|
+
hub;
|
|
1099
|
+
identity;
|
|
1100
|
+
snapshot;
|
|
1101
|
+
readFiles = /* @__PURE__ */ new Set();
|
|
1102
|
+
lineCounts = /* @__PURE__ */ new Map();
|
|
1103
|
+
readRanges = /* @__PURE__ */ new Map();
|
|
1104
|
+
limitations = /* @__PURE__ */ new Set();
|
|
1105
|
+
budget;
|
|
1106
|
+
commonReader;
|
|
1107
|
+
projectReader() {
|
|
1108
|
+
const project = this.snapshot.project;
|
|
1109
|
+
if (!project)
|
|
1110
|
+
throw new HubError(403, "AI_SCOPE_VIOLATION", "No project reference snapshot is available.");
|
|
1111
|
+
return this.commonReader ??= new _SnapshotReader(
|
|
1112
|
+
this.hub,
|
|
1113
|
+
this.identity,
|
|
1114
|
+
{ ...project, owner: PROJECT_FILE_OWNER, kind: "directory", path: "" },
|
|
1115
|
+
0,
|
|
1116
|
+
this.budget
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
afterIndex(after) {
|
|
1120
|
+
if (!after) return 0;
|
|
1121
|
+
const index = this.snapshot.files.findIndex((f) => f.path === after);
|
|
1122
|
+
if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
|
|
1123
|
+
return index + 1;
|
|
1124
|
+
}
|
|
1125
|
+
fullyRead(path) {
|
|
1126
|
+
let end = 0;
|
|
1127
|
+
for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
|
|
1128
|
+
if (range[0] > end + 1) break;
|
|
1129
|
+
end = Math.max(end, range[1]);
|
|
1130
|
+
}
|
|
1131
|
+
return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
|
|
1132
|
+
}
|
|
1133
|
+
list(after = "", limit = 50) {
|
|
1134
|
+
const entries = this.snapshot.files.slice(
|
|
1135
|
+
this.afterIndex(after),
|
|
1136
|
+
this.afterIndex(after) + limit + 1
|
|
1137
|
+
);
|
|
1138
|
+
return {
|
|
1139
|
+
files: entries.slice(0, limit).map((f) => ({ path: f.path, size: f.size })),
|
|
1140
|
+
total: this.snapshot.files.length,
|
|
1141
|
+
nextCursor: entries.length > limit ? entries[limit - 1].path : null
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
source(path) {
|
|
1145
|
+
const file = this.snapshot.files.find((entry) => entry.path === path);
|
|
1146
|
+
if (!file || !this.readFiles.has(path))
|
|
1147
|
+
throw new HubError(
|
|
1148
|
+
422,
|
|
1149
|
+
"AI_TASK_SOURCE_INVALID",
|
|
1150
|
+
"Task source must be an actually read snapshot file."
|
|
1151
|
+
);
|
|
1152
|
+
const row = this.hub.db.prepare(
|
|
1153
|
+
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
1154
|
+
).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
|
|
1155
|
+
if (!row)
|
|
1156
|
+
throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
|
|
1157
|
+
return { ...file, syncedAt: String(row.created_at) };
|
|
1158
|
+
}
|
|
1159
|
+
async text(path) {
|
|
1160
|
+
const entry = this.snapshot.files.find((f) => f.path === path);
|
|
1161
|
+
if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
|
|
1162
|
+
if (entry.size > 256 * 1024)
|
|
1163
|
+
throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
|
|
1164
|
+
const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
|
|
1165
|
+
if (bytes.byteLength !== entry.size || createHash4("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
1166
|
+
throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
|
|
1167
|
+
let text;
|
|
1168
|
+
try {
|
|
1169
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1170
|
+
} catch {
|
|
1171
|
+
throw new HubError(422, "AI_TEXT_REQUIRED", "Only UTF-8 text files can be analysed.");
|
|
1172
|
+
}
|
|
1173
|
+
if (text.includes("\0"))
|
|
1174
|
+
throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
|
|
1175
|
+
this.lineCounts.set(path, text.split("\n").length);
|
|
1176
|
+
return text;
|
|
1177
|
+
}
|
|
1178
|
+
async read(path, startLine = 1, limit = 150) {
|
|
1179
|
+
const text = await this.text(path), lines = text.split("\n");
|
|
1180
|
+
if (startLine > lines.length)
|
|
1181
|
+
throw new HubError(400, "AI_LINE_RANGE", "Line is outside the file.");
|
|
1182
|
+
const selected = [];
|
|
1183
|
+
let size = 0;
|
|
1184
|
+
for (const line of lines.slice(startLine - 1, startLine - 1 + limit)) {
|
|
1185
|
+
const cost = Buffer.byteLength(line) + 1;
|
|
1186
|
+
if (size + cost > Math.min(12e3, this.budget.remaining)) break;
|
|
1187
|
+
selected.push(line);
|
|
1188
|
+
size += cost;
|
|
1189
|
+
}
|
|
1190
|
+
if (!selected.length) {
|
|
1191
|
+
this.limitations.add("\u8BFB\u53D6\u9884\u7B97\u6216\u5355\u884C\u957F\u5EA6\u9650\u5236\u963B\u6B62\u4E86\u7EE7\u7EED\u8BFB\u53D6\u3002");
|
|
1192
|
+
throw new HubError(413, "AI_READ_BUDGET", "Read budget exceeded or line is too long.");
|
|
1193
|
+
}
|
|
1194
|
+
this.budget.remaining -= size;
|
|
1195
|
+
this.readFiles.add(path);
|
|
1196
|
+
const endLine = startLine + selected.length - 1;
|
|
1197
|
+
this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
|
|
1198
|
+
return {
|
|
1199
|
+
path,
|
|
1200
|
+
startLine,
|
|
1201
|
+
endLine,
|
|
1202
|
+
totalLines: lines.length,
|
|
1203
|
+
content: selected.map((line, i) => `${startLine + i}: ${line}`).join("\n"),
|
|
1204
|
+
hasMore: endLine < lines.length
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async whole(path, budget) {
|
|
1208
|
+
const text = await this.text(path);
|
|
1209
|
+
if (Buffer.byteLength(text) > budget)
|
|
1210
|
+
throw new HubError(
|
|
1211
|
+
413,
|
|
1212
|
+
"AI_CONTEXT_LIMIT",
|
|
1213
|
+
"File is too large for this model budget; use directory analysis to read portions."
|
|
1214
|
+
);
|
|
1215
|
+
this.readFiles.add(path);
|
|
1216
|
+
this.readRanges.set(path, [[1, text.split("\n").length]]);
|
|
1217
|
+
return text;
|
|
1218
|
+
}
|
|
1219
|
+
async search(query, after = "", limit = 50) {
|
|
1220
|
+
const files = this.snapshot.files.slice(this.afterIndex(after), this.afterIndex(after) + limit);
|
|
1221
|
+
const matches = [];
|
|
1222
|
+
let scannedBytes = 0;
|
|
1223
|
+
for (const file of files) {
|
|
1224
|
+
if (file.size > 256 * 1024) {
|
|
1225
|
+
this.limitations.add("\u90E8\u5206\u5927\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (scannedBytes + file.size > 1024 * 1024) {
|
|
1229
|
+
return {
|
|
1230
|
+
matches,
|
|
1231
|
+
nextCursor: files[files.indexOf(file) - 1]?.path ?? after,
|
|
1232
|
+
limited: true
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
scannedBytes += file.size;
|
|
1236
|
+
try {
|
|
1237
|
+
const lines = (await this.text(file.path)).split("\n");
|
|
1238
|
+
for (let i = 0; i < lines.length; i++)
|
|
1239
|
+
if (lines[i].toLowerCase().includes(query.toLowerCase())) {
|
|
1240
|
+
matches.push({ path: file.path, line: i + 1, excerpt: lines[i].slice(0, 400) });
|
|
1241
|
+
if (matches.length >= 30) return { matches, nextCursor: file.path, limited: true };
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
if (e instanceof HubError && e.code === "AI_TEXT_REQUIRED") {
|
|
1245
|
+
this.limitations.add("\u90E8\u5206\u975E\u6587\u672C\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
throw e;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
matches,
|
|
1253
|
+
nextCursor: files.length === limit ? files.at(-1).path : null,
|
|
1254
|
+
limited: false
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
// apps/server/ai/worker.ts
|
|
1260
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1261
|
+
|
|
1262
|
+
// apps/server/store.ts
|
|
1263
|
+
import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
|
|
1264
|
+
import { join as join3 } from "node:path";
|
|
1265
|
+
import { existsSync } from "node:fs";
|
|
1266
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1267
|
+
|
|
1268
|
+
// apps/server/session-schema.ts
|
|
1269
|
+
var sessionMigration = String.raw`
|
|
1270
|
+
CREATE TABLE sessions (
|
|
1271
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1272
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1273
|
+
agent TEXT NOT NULL, native_id TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL,
|
|
1274
|
+
revision INTEGER NOT NULL, updated_at TEXT NOT NULL, message_count INTEGER NOT NULL,
|
|
1275
|
+
hash TEXT NOT NULL, size INTEGER NOT NULL,
|
|
1276
|
+
UNIQUE(workspace_id, owner, session_key)
|
|
1277
|
+
);
|
|
1278
|
+
CREATE INDEX session_listing ON sessions(workspace_id, id);
|
|
1279
|
+
CREATE TABLE session_revisions (
|
|
1280
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1281
|
+
revision INTEGER NOT NULL, info_json TEXT NOT NULL,
|
|
1282
|
+
PRIMARY KEY(workspace_id, owner, session_key, revision)
|
|
1283
|
+
);
|
|
1284
|
+
CREATE TABLE session_operations (
|
|
1285
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
1286
|
+
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1287
|
+
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
1288
|
+
);
|
|
1289
|
+
CREATE TABLE session_messages (
|
|
1290
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1291
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, session_key TEXT NOT NULL,
|
|
1292
|
+
revision INTEGER NOT NULL, message_id TEXT NOT NULL, role TEXT NOT NULL, body TEXT NOT NULL, search_text TEXT NOT NULL
|
|
1293
|
+
);
|
|
1294
|
+
CREATE INDEX session_message_lookup ON session_messages(workspace_id, owner, session_key);
|
|
1295
|
+
CREATE VIRTUAL TABLE session_fts USING fts5(body, tokenize='trigram');
|
|
1296
|
+
`;
|
|
1297
|
+
var sessionOwnerIndexMigration = `CREATE INDEX session_owner_listing ON sessions(workspace_id,owner,id);`;
|
|
1298
|
+
|
|
1299
|
+
// apps/server/control-schema.ts
|
|
1300
|
+
var controlMigration = String.raw`
|
|
1301
|
+
CREATE TABLE device_status(workspace_id TEXT NOT NULL,member_id TEXT NOT NULL,device_id TEXT NOT NULL,seen_at TEXT NOT NULL,status_json TEXT NOT NULL,PRIMARY KEY(workspace_id,member_id));
|
|
1302
|
+
`;
|
|
1303
|
+
|
|
1304
|
+
// apps/server/device-schema.ts
|
|
1305
|
+
var deviceMigration = String.raw`
|
|
1306
|
+
CREATE TABLE device_enrollments(workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, operation_id TEXT NOT NULL, fingerprint TEXT NOT NULL, PRIMARY KEY(workspace_id,member_id,operation_id));
|
|
1307
|
+
`;
|
|
1308
|
+
|
|
1309
|
+
// apps/server/project-schema.ts
|
|
1310
|
+
var projectMigration = String.raw`
|
|
1311
|
+
ALTER TABLE members ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
|
1312
|
+
ALTER TABLE members ADD COLUMN role TEXT NOT NULL DEFAULT 'member';
|
|
1313
|
+
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
1314
|
+
ALTER TABLE members ADD COLUMN member_revision INTEGER NOT NULL DEFAULT 1;
|
|
1315
|
+
UPDATE members SET enabled=0 WHERE token_hash IS NULL;
|
|
1316
|
+
CREATE TABLE project_settings(id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL, setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL);
|
|
1317
|
+
CREATE TABLE web_sessions(token_hash TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, member_id TEXT NOT NULL, expires_at INTEGER NOT NULL);
|
|
1318
|
+
`;
|
|
1319
|
+
var multiProjectMigration = String.raw`
|
|
1320
|
+
DROP TABLE project_settings;
|
|
1321
|
+
DROP TABLE web_sessions;
|
|
1322
|
+
ALTER TABLE members DROP COLUMN display_name;
|
|
1323
|
+
CREATE TABLE users (
|
|
1324
|
+
id TEXT PRIMARY KEY, display_name TEXT NOT NULL DEFAULT '',
|
|
1325
|
+
site_role TEXT NOT NULL DEFAULT 'member', enabled INTEGER NOT NULL DEFAULT 1,
|
|
1326
|
+
revision INTEGER NOT NULL DEFAULT 1
|
|
1327
|
+
);
|
|
1328
|
+
CREATE TABLE projects (
|
|
1329
|
+
id TEXT PRIMARY KEY, config_json TEXT NOT NULL,
|
|
1330
|
+
operation_id TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL
|
|
1331
|
+
);
|
|
1332
|
+
CREATE TABLE site_settings (
|
|
1333
|
+
id INTEGER PRIMARY KEY CHECK(id=1), config_json TEXT NOT NULL,
|
|
1334
|
+
setup_operation TEXT NOT NULL, setup_fingerprint TEXT NOT NULL
|
|
1335
|
+
);
|
|
1336
|
+
CREATE TABLE web_sessions (
|
|
1337
|
+
token_hash TEXT PRIMARY KEY, member_id TEXT NOT NULL REFERENCES users(id), expires_at INTEGER NOT NULL
|
|
1338
|
+
);
|
|
1339
|
+
DROP TABLE ai_profiles;
|
|
1340
|
+
CREATE TABLE ai_profiles (
|
|
1341
|
+
id TEXT PRIMARY KEY, revision INTEGER NOT NULL, config_json TEXT NOT NULL, secret TEXT
|
|
1342
|
+
);
|
|
1343
|
+
`;
|
|
1344
|
+
var sitePolicyMigration = `
|
|
1345
|
+
UPDATE site_settings SET config_json=json_set(config_json,'$.sync',json(COALESCE(
|
|
1346
|
+
(SELECT json_extract(config_json,'$.sync') FROM projects ORDER BY rowid LIMIT 1),
|
|
1347
|
+
'{"checkIntervalSeconds":60,"fileSettleSeconds":60}'
|
|
1348
|
+
)));
|
|
1349
|
+
UPDATE projects SET config_json=json_set(config_json,
|
|
1350
|
+
'$.sync',json((SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1)),
|
|
1351
|
+
'$.configRevision',json_extract(config_json,'$.configRevision')+1)
|
|
1352
|
+
WHERE json_extract(config_json,'$.sync') != (SELECT json_extract(config_json,'$.sync') FROM site_settings WHERE id=1);
|
|
1353
|
+
`;
|
|
1354
|
+
var equalMembershipMigration = `
|
|
1355
|
+
ALTER TABLE members DROP COLUMN role;
|
|
1356
|
+
ALTER TABLE members ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
|
|
1357
|
+
`;
|
|
1358
|
+
|
|
1359
|
+
// apps/server/file-schema.ts
|
|
1360
|
+
var fileMigration = String.raw`
|
|
1361
|
+
ALTER TABLE members ADD COLUMN file_revision INTEGER NOT NULL DEFAULT 0;
|
|
1362
|
+
CREATE TABLE workspace_blobs (workspace_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(workspace_id, hash));
|
|
1363
|
+
CREATE TABLE file_operations (
|
|
1364
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, operation_id TEXT NOT NULL,
|
|
1365
|
+
fingerprint TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1366
|
+
PRIMARY KEY(workspace_id, owner, operation_id)
|
|
1367
|
+
);
|
|
1368
|
+
CREATE TABLE file_revisions (
|
|
1369
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1370
|
+
operation_id TEXT NOT NULL, created_at TEXT NOT NULL, changes_json TEXT NOT NULL,
|
|
1371
|
+
PRIMARY KEY(workspace_id, owner, revision)
|
|
1372
|
+
);
|
|
1373
|
+
CREATE TABLE current_files (
|
|
1374
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, path_key TEXT NOT NULL,
|
|
1375
|
+
hash TEXT NOT NULL, size INTEGER NOT NULL, revision INTEGER NOT NULL,
|
|
1376
|
+
PRIMARY KEY(workspace_id, owner, path), UNIQUE(workspace_id, owner, path_key)
|
|
1377
|
+
);
|
|
1378
|
+
CREATE TABLE file_events (
|
|
1379
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, path TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1380
|
+
hash TEXT, size INTEGER,
|
|
1381
|
+
PRIMARY KEY(workspace_id, owner, path, revision)
|
|
1382
|
+
);
|
|
1383
|
+
`;
|
|
1384
|
+
|
|
1385
|
+
// apps/server/ai/task-schema.ts
|
|
1386
|
+
var taskBoardMigration = `
|
|
1387
|
+
ALTER TABLE ai_jobs ADD COLUMN runtime_json TEXT;
|
|
1388
|
+
ALTER TABLE ai_jobs ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
|
1389
|
+
CREATE TABLE task_board_members (
|
|
1390
|
+
workspace_id TEXT NOT NULL, owner TEXT NOT NULL, job_id TEXT NOT NULL REFERENCES ai_jobs(id),
|
|
1391
|
+
PRIMARY KEY(workspace_id,owner)
|
|
1392
|
+
);
|
|
1393
|
+
CREATE TABLE task_cards (
|
|
1394
|
+
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, owner TEXT NOT NULL,
|
|
1395
|
+
job_id TEXT NOT NULL REFERENCES ai_jobs(id), fields_json TEXT NOT NULL,
|
|
1396
|
+
sources_json TEXT NOT NULL, synced_at TEXT NOT NULL, correction_json TEXT,
|
|
1397
|
+
active INTEGER NOT NULL DEFAULT 1, needs_review INTEGER NOT NULL DEFAULT 0
|
|
1398
|
+
);
|
|
1399
|
+
CREATE INDEX task_cards_scope ON task_cards(workspace_id,owner,active);
|
|
1400
|
+
`;
|
|
1401
|
+
var taskMaintenanceMigration = `
|
|
1402
|
+
ALTER TABLE ai_jobs ADD COLUMN task_context_json TEXT;
|
|
1403
|
+
ALTER TABLE task_board_members ADD COLUMN applied_job_id TEXT REFERENCES ai_jobs(id);
|
|
1404
|
+
UPDATE task_board_members SET applied_job_id=(
|
|
1405
|
+
SELECT j.id FROM ai_jobs j
|
|
1406
|
+
WHERE j.workspace_id=task_board_members.workspace_id AND j.status='succeeded'
|
|
1407
|
+
AND json_extract(j.request_json,'$.workflow')='task-bootstrap'
|
|
1408
|
+
AND json_extract(j.snapshot_json,'$.owner')=task_board_members.owner
|
|
1409
|
+
ORDER BY j.rowid DESC LIMIT 1
|
|
1410
|
+
);
|
|
1411
|
+
`;
|
|
1412
|
+
|
|
1413
|
+
// apps/server/ai/schema.ts
|
|
1414
|
+
var aiConfigMigration = `
|
|
1415
|
+
CREATE TABLE ai_profiles (
|
|
1416
|
+
workspace_id TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
1417
|
+
config_json TEXT NOT NULL, secret TEXT,
|
|
1418
|
+
PRIMARY KEY(workspace_id,id)
|
|
1419
|
+
);
|
|
1420
|
+
`;
|
|
1421
|
+
var aiJobsMigration = `
|
|
1422
|
+
CREATE TABLE ai_jobs (
|
|
1423
|
+
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, requester TEXT NOT NULL,
|
|
1424
|
+
request_json TEXT NOT NULL, profile_json TEXT NOT NULL, snapshot_json TEXT NOT NULL,
|
|
1425
|
+
prompt_version TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, recovery_count INTEGER NOT NULL DEFAULT 0,
|
|
1426
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, run_token TEXT, lease_until INTEGER,
|
|
1427
|
+
result_json TEXT, error TEXT
|
|
1428
|
+
);
|
|
1429
|
+
CREATE INDEX ai_jobs_queue ON ai_jobs(status,created_at);
|
|
1430
|
+
CREATE INDEX ai_jobs_workspace ON ai_jobs(workspace_id,id);
|
|
1431
|
+
CREATE TABLE ai_operations(workspace_id TEXT NOT NULL,requester TEXT NOT NULL,operation_id TEXT NOT NULL,fingerprint TEXT NOT NULL,job_id TEXT NOT NULL REFERENCES ai_jobs(id),PRIMARY KEY(workspace_id,requester,operation_id));
|
|
1432
|
+
CREATE TABLE ai_attempts(job_id TEXT NOT NULL REFERENCES ai_jobs(id),number INTEGER NOT NULL,status TEXT NOT NULL,started_at TEXT NOT NULL,ended_at TEXT,error TEXT,usage_json TEXT,PRIMARY KEY(job_id,number));
|
|
1433
|
+
CREATE TABLE ai_events(seq INTEGER PRIMARY KEY AUTOINCREMENT,job_id TEXT NOT NULL REFERENCES ai_jobs(id),attempt INTEGER NOT NULL,kind TEXT NOT NULL,message TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
1434
|
+
CREATE INDEX ai_events_job ON ai_events(job_id,seq);
|
|
1435
|
+
`;
|
|
1436
|
+
var aiDefaultsMigration = `
|
|
1437
|
+
CREATE TABLE ai_defaults (
|
|
1438
|
+
scope TEXT PRIMARY KEY, profile_id TEXT REFERENCES ai_profiles(id), revision INTEGER NOT NULL
|
|
1439
|
+
);
|
|
1440
|
+
`;
|
|
1441
|
+
|
|
1442
|
+
// apps/server/store.ts
|
|
1443
|
+
function digest2(token) {
|
|
1444
|
+
return createHash5("sha256").update(token).digest("hex");
|
|
1445
|
+
}
|
|
1446
|
+
var migrations = [
|
|
1447
|
+
String.raw`
|
|
1448
|
+
CREATE TABLE members (
|
|
1449
|
+
workspace_id TEXT NOT NULL,
|
|
1450
|
+
member_id TEXT NOT NULL,
|
|
1451
|
+
token_hash TEXT UNIQUE,
|
|
1452
|
+
device_id TEXT,
|
|
1453
|
+
PRIMARY KEY (workspace_id, member_id)
|
|
1454
|
+
);
|
|
1455
|
+
`,
|
|
1456
|
+
fileMigration,
|
|
1457
|
+
sessionMigration,
|
|
1458
|
+
projectMigration,
|
|
1459
|
+
deviceMigration,
|
|
1460
|
+
controlMigration,
|
|
1461
|
+
aiConfigMigration,
|
|
1462
|
+
aiJobsMigration,
|
|
1463
|
+
multiProjectMigration,
|
|
1464
|
+
aiDefaultsMigration,
|
|
1465
|
+
sitePolicyMigration,
|
|
1466
|
+
sessionOwnerIndexMigration,
|
|
1467
|
+
equalMembershipMigration,
|
|
1468
|
+
taskBoardMigration,
|
|
1469
|
+
taskMaintenanceMigration,
|
|
1470
|
+
sharedFilesMigration
|
|
1471
|
+
];
|
|
1472
|
+
var HubStore = class {
|
|
1473
|
+
constructor(directory) {
|
|
1474
|
+
this.directory = directory;
|
|
1475
|
+
const path = join3(directory, "hub.sqlite3");
|
|
1476
|
+
if (existsSync(path)) {
|
|
1477
|
+
const existing = new DatabaseSync2(path, { readOnly: true });
|
|
1478
|
+
try {
|
|
1479
|
+
const version = Number(existing.prepare("PRAGMA user_version").get().user_version);
|
|
1480
|
+
if (version > 0 && version < 9)
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
"Single-project data is not supported by this release. Start with a new --data-dir; existing data has been preserved."
|
|
1483
|
+
);
|
|
1484
|
+
} finally {
|
|
1485
|
+
existing.close();
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
this.db = openDatabase(path, migrations);
|
|
1489
|
+
}
|
|
1490
|
+
directory;
|
|
1491
|
+
db;
|
|
1492
|
+
close() {
|
|
1493
|
+
this.db.close();
|
|
1494
|
+
}
|
|
1495
|
+
issueMember(workspaceId, memberId, reset = false) {
|
|
1496
|
+
NameSchema.parse(workspaceId);
|
|
1497
|
+
NameSchema.parse(memberId);
|
|
1498
|
+
const token = `tmy_${randomBytes3(32).toString("hex")}`;
|
|
1499
|
+
transaction(this.db, () => {
|
|
1500
|
+
this.db.prepare("INSERT OR IGNORE INTO users(id) VALUES (?)").run(memberId);
|
|
1501
|
+
const exists = this.db.prepare("SELECT 1 FROM members WHERE workspace_id=? AND member_id=?").get(workspaceId, memberId);
|
|
1502
|
+
if (exists && !reset)
|
|
1503
|
+
throw new HubError(
|
|
1504
|
+
409,
|
|
1505
|
+
"MEMBER_EXISTS",
|
|
1506
|
+
"Member exists; use member reset to replace its credential and device binding."
|
|
1507
|
+
);
|
|
1508
|
+
if (!exists && reset) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
1509
|
+
this.db.prepare(
|
|
1510
|
+
`INSERT INTO members (workspace_id, member_id, token_hash) VALUES (?, ?, ?)
|
|
1511
|
+
ON CONFLICT(workspace_id, member_id) DO UPDATE SET token_hash=excluded.token_hash, device_id=NULL, enabled=1, member_revision=member_revision+1`
|
|
1512
|
+
).run(workspaceId, memberId, digest2(token));
|
|
1513
|
+
});
|
|
1514
|
+
return token;
|
|
1515
|
+
}
|
|
1516
|
+
revokeMember(workspaceId, memberId) {
|
|
1517
|
+
const result = this.db.prepare(
|
|
1518
|
+
"UPDATE members SET token_hash=NULL, device_id=NULL, enabled=0, member_revision=member_revision+1 WHERE workspace_id=? AND member_id=?"
|
|
1519
|
+
).run(NameSchema.parse(workspaceId), NameSchema.parse(memberId));
|
|
1520
|
+
if (!result.changes) throw new HubError(404, "MEMBER_NOT_FOUND", "Member does not exist.");
|
|
1521
|
+
this.db.prepare("DELETE FROM device_status WHERE workspace_id=? AND member_id=?").run(workspaceId, memberId);
|
|
1522
|
+
}
|
|
1523
|
+
identity(authorization) {
|
|
1524
|
+
const match = authorization?.match(/^Bearer (tmy_[a-f0-9]{64})$/);
|
|
1525
|
+
const row = match && this.db.prepare(
|
|
1526
|
+
"SELECT m.workspace_id,m.member_id,m.device_id FROM members m JOIN users u ON u.id=m.member_id WHERE m.token_hash=? AND m.enabled=1 AND u.enabled=1"
|
|
1527
|
+
).get(digest2(match[1]));
|
|
1528
|
+
if (!row) throw new HubError(401, "UNAUTHORIZED", "A valid member credential is required.");
|
|
1529
|
+
return {
|
|
1530
|
+
workspaceId: String(row.workspace_id),
|
|
1531
|
+
memberId: String(row.member_id),
|
|
1532
|
+
deviceId: row.device_id
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
bind(authorization, deviceId, projectId2) {
|
|
1536
|
+
return transaction(this.db, () => {
|
|
1537
|
+
const identity = this.identity(authorization);
|
|
1538
|
+
if (projectId2 && projectId2 !== identity.workspaceId)
|
|
1539
|
+
throw new HubError(403, "PROJECT_MISMATCH", "Token belongs to a different project.");
|
|
1540
|
+
if (identity.deviceId && identity.deviceId !== deviceId)
|
|
1541
|
+
throw new HubError(
|
|
1542
|
+
409,
|
|
1543
|
+
"DEVICE_BOUND",
|
|
1544
|
+
"This member is bound to another device. An administrator must reset the binding."
|
|
1545
|
+
);
|
|
1546
|
+
this.db.prepare("UPDATE members SET device_id=? WHERE workspace_id=? AND member_id=?").run(deviceId, identity.workspaceId, identity.memberId);
|
|
1547
|
+
return BindingSchema.parse({ ...identity, deviceId });
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
authorize(authorization, deviceId) {
|
|
1551
|
+
const identity = this.identity(authorization);
|
|
1552
|
+
if (!identity.deviceId || identity.deviceId !== deviceId)
|
|
1553
|
+
throw new HubError(
|
|
1554
|
+
403,
|
|
1555
|
+
"DEVICE_MISMATCH",
|
|
1556
|
+
"Connect this device before using workspace APIs."
|
|
1557
|
+
);
|
|
1558
|
+
return BindingSchema.parse(identity);
|
|
1559
|
+
}
|
|
1560
|
+
members(identity) {
|
|
1561
|
+
return this.db.prepare("SELECT member_id FROM members WHERE workspace_id=? ORDER BY member_id").all(identity.workspaceId).map((row) => String(row.member_id));
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
// apps/server/ai/jobs.ts
|
|
1566
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
1567
|
+
|
|
1568
|
+
// packages/protocol/ai.ts
|
|
1569
|
+
import { z as z6 } from "zod";
|
|
1570
|
+
|
|
1571
|
+
// packages/protocol/tasks.ts
|
|
1572
|
+
import { z as z5 } from "zod";
|
|
1573
|
+
var TaskStatusSchema = z5.enum(["planned", "in_progress", "in_review", "completed"]);
|
|
1574
|
+
var TaskFieldsSchema = z5.object({
|
|
1575
|
+
title: z5.string().trim().min(1).max(200),
|
|
1576
|
+
goal: z5.string().trim().min(1).max(2e3).nullable(),
|
|
1577
|
+
status: TaskStatusSchema.nullable(),
|
|
1578
|
+
progress: z5.string().trim().min(1).max(2e3)
|
|
1579
|
+
}).strict();
|
|
1580
|
+
var TaskSourceSchema = z5.object({
|
|
1581
|
+
path: FilePathSchema,
|
|
1582
|
+
sha256: HashSchema,
|
|
1583
|
+
revision: RevisionSchema,
|
|
1584
|
+
syncedAt: z5.string()
|
|
1585
|
+
});
|
|
1586
|
+
var TaskCardSchema = TaskFieldsSchema.extend({
|
|
1587
|
+
id: z5.uuid(),
|
|
1588
|
+
owner: NameSchema,
|
|
1589
|
+
jobId: z5.uuid(),
|
|
1590
|
+
sources: z5.array(TaskSourceSchema).min(1),
|
|
1591
|
+
syncedAt: z5.string(),
|
|
1592
|
+
corrected: z5.boolean(),
|
|
1593
|
+
needsReview: z5.boolean()
|
|
1594
|
+
});
|
|
1595
|
+
var TaskCorrectionSchema = z5.object({
|
|
1596
|
+
title: TaskFieldsSchema.shape.title.optional(),
|
|
1597
|
+
status: TaskStatusSchema.nullable().optional()
|
|
1598
|
+
}).strict().refine((v) => Object.keys(v).length > 0, "Provide a correction.");
|
|
1599
|
+
var TaskBoardSchema = z5.object({
|
|
1600
|
+
tasks: z5.array(TaskCardSchema),
|
|
1601
|
+
members: z5.array(
|
|
1602
|
+
z5.object({
|
|
1603
|
+
owner: NameSchema,
|
|
1604
|
+
jobId: z5.uuid().nullable(),
|
|
1605
|
+
status: z5.enum(["empty", "waiting", "queued", "running", "succeeded", "failed", "cancelled"]),
|
|
1606
|
+
error: z5.string().nullable(),
|
|
1607
|
+
sourceChanged: z5.boolean(),
|
|
1608
|
+
partial: z5.boolean(),
|
|
1609
|
+
uncertainties: z5.array(z5.string())
|
|
1610
|
+
})
|
|
1611
|
+
),
|
|
1612
|
+
modelAvailable: z5.boolean()
|
|
1613
|
+
});
|
|
1614
|
+
var TaskBootstrapRequestSchema = z5.object({ operationId: z5.uuid(), owner: NameSchema }).strict();
|
|
1615
|
+
var TaskBootstrapResponseSchema = z5.object({
|
|
1616
|
+
jobId: z5.uuid().nullable(),
|
|
1617
|
+
empty: z5.boolean()
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// packages/protocol/ai.ts
|
|
1621
|
+
var AiProfileSchema = z6.object({
|
|
1622
|
+
id: NameSchema,
|
|
1623
|
+
name: z6.string().trim().min(1).max(120),
|
|
1624
|
+
protocol: z6.enum(["openai-completions", "openai-responses", "anthropic-messages"]),
|
|
1625
|
+
baseUrl: ServerUrlSchema,
|
|
1626
|
+
model: z6.string().trim().min(1).max(200),
|
|
1627
|
+
enabled: z6.boolean().default(true),
|
|
1628
|
+
contextWindow: z6.number().int().min(4096).max(2e6).default(32e3),
|
|
1629
|
+
maxOutputTokens: z6.number().int().min(128).max(16e3).default(2048)
|
|
1630
|
+
}).strict().refine(
|
|
1631
|
+
(p) => p.maxOutputTokens < p.contextWindow,
|
|
1632
|
+
"Output budget must be smaller than context window."
|
|
1633
|
+
);
|
|
1634
|
+
var AiPublicProfileSchema = AiProfileSchema.safeExtend({
|
|
1635
|
+
revision: z6.number().int(),
|
|
1636
|
+
keyConfigured: z6.boolean()
|
|
1637
|
+
});
|
|
1638
|
+
var AiProfileUpdateSchema = z6.object({
|
|
1639
|
+
expectedRevision: z6.number().int().nonnegative(),
|
|
1640
|
+
profile: AiProfileSchema,
|
|
1641
|
+
apiKey: z6.string().min(1).max(4096).nullable().optional()
|
|
1642
|
+
}).strict();
|
|
1643
|
+
var AiProfilesSchema = z6.object({ profiles: z6.array(AiPublicProfileSchema) });
|
|
1644
|
+
var AiDefaultUpdateSchema = z6.object({
|
|
1645
|
+
expectedRevision: z6.number().int().nonnegative(),
|
|
1646
|
+
profileId: NameSchema.nullable()
|
|
1647
|
+
}).strict();
|
|
1648
|
+
var AiDefaultSchema = z6.object({
|
|
1649
|
+
revision: z6.number().int().nonnegative(),
|
|
1650
|
+
profileId: NameSchema.nullable(),
|
|
1651
|
+
effectiveProfileId: NameSchema.nullable(),
|
|
1652
|
+
source: z6.enum(["site", "project", "none"]),
|
|
1653
|
+
available: z6.boolean()
|
|
1654
|
+
});
|
|
1655
|
+
var AiProbeSchema = z6.object({ mode: z6.enum(["text", "tools", "json"]) }).strict();
|
|
1656
|
+
var AiProbeResultSchema = z6.object({
|
|
1657
|
+
ok: z6.literal(true),
|
|
1658
|
+
mode: AiProbeSchema.shape.mode,
|
|
1659
|
+
streamed: z6.boolean(),
|
|
1660
|
+
inputTokens: z6.number(),
|
|
1661
|
+
outputTokens: z6.number()
|
|
1662
|
+
});
|
|
1663
|
+
var AiSelectionSchema = z6.object({
|
|
1664
|
+
owner: NameSchema,
|
|
1665
|
+
kind: z6.enum(["file", "directory"]),
|
|
1666
|
+
path: z6.union([z6.literal(""), FilePathSchema]),
|
|
1667
|
+
revision: z6.number().int().nonnegative().optional()
|
|
1668
|
+
}).strict().refine((s) => s.kind === "directory" || s.path !== "", "A file path is required.");
|
|
1669
|
+
var AiJobCreateSchema = z6.object({
|
|
1670
|
+
operationId: z6.uuid(),
|
|
1671
|
+
profileId: NameSchema.optional(),
|
|
1672
|
+
workflow: z6.enum(["file-summary", "directory-analysis", "task-bootstrap"]),
|
|
1673
|
+
selection: AiSelectionSchema,
|
|
1674
|
+
question: z6.string().trim().max(2e3).default("")
|
|
1675
|
+
}).strict();
|
|
1676
|
+
var AiCitationSchema = z6.object({
|
|
1677
|
+
path: FilePathSchema,
|
|
1678
|
+
startLine: z6.number().int().positive(),
|
|
1679
|
+
endLine: z6.number().int().positive(),
|
|
1680
|
+
sha256: HashSchema
|
|
1681
|
+
});
|
|
1682
|
+
var AiCoverageSchema = z6.object({
|
|
1683
|
+
totalFiles: z6.number().int(),
|
|
1684
|
+
readFiles: z6.array(FilePathSchema),
|
|
1685
|
+
partial: z6.boolean(),
|
|
1686
|
+
limitations: z6.array(z6.string())
|
|
1687
|
+
});
|
|
1688
|
+
var AiResultSchema = z6.object({
|
|
1689
|
+
summary: z6.string().min(1).max(2e4),
|
|
1690
|
+
findings: z6.array(z6.string().max(2e3)).max(30),
|
|
1691
|
+
uncertainties: z6.array(z6.string().max(2e3)).max(30),
|
|
1692
|
+
citations: z6.array(AiCitationSchema).max(50),
|
|
1693
|
+
coverage: AiCoverageSchema
|
|
1694
|
+
});
|
|
1695
|
+
var TaskAnalysisResultSchema = z6.object({
|
|
1696
|
+
kind: z6.literal("task-bootstrap"),
|
|
1697
|
+
tasks: z6.array(
|
|
1698
|
+
TaskFieldsSchema.extend({
|
|
1699
|
+
taskId: z6.uuid().nullable().optional(),
|
|
1700
|
+
sources: z6.array(TaskSourceSchema).min(1).max(50)
|
|
1701
|
+
})
|
|
1702
|
+
).max(100),
|
|
1703
|
+
uncertainties: z6.array(z6.string().max(2e3)).max(50),
|
|
1704
|
+
coverage: AiCoverageSchema
|
|
1705
|
+
});
|
|
1706
|
+
var AiWorkflowResultSchema = z6.union([AiResultSchema, TaskAnalysisResultSchema]);
|
|
1707
|
+
var AiUsageSchema = z6.object({
|
|
1708
|
+
inputTokens: z6.number().nonnegative(),
|
|
1709
|
+
outputTokens: z6.number().nonnegative()
|
|
1710
|
+
});
|
|
1711
|
+
var AiJobSchema = z6.object({
|
|
1712
|
+
id: z6.uuid(),
|
|
1713
|
+
requester: NameSchema,
|
|
1714
|
+
workflow: AiJobCreateSchema.shape.workflow,
|
|
1715
|
+
question: z6.string(),
|
|
1716
|
+
profileId: NameSchema,
|
|
1717
|
+
model: z6.string(),
|
|
1718
|
+
profileRevision: z6.number().int(),
|
|
1719
|
+
promptVersion: z6.string(),
|
|
1720
|
+
origin: z6.enum(["manual", "automatic"]).default("manual"),
|
|
1721
|
+
status: z6.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
|
|
1722
|
+
createdAt: z6.string(),
|
|
1723
|
+
updatedAt: z6.string(),
|
|
1724
|
+
attemptCount: z6.number().int(),
|
|
1725
|
+
error: z6.string().nullable(),
|
|
1726
|
+
input: z6.object({
|
|
1727
|
+
owner: NameSchema,
|
|
1728
|
+
kind: z6.enum(["file", "directory"]),
|
|
1729
|
+
path: z6.string(),
|
|
1730
|
+
revision: z6.number().int(),
|
|
1731
|
+
fileCount: z6.number().int(),
|
|
1732
|
+
fingerprint: HashSchema
|
|
1733
|
+
}),
|
|
1734
|
+
result: AiWorkflowResultSchema.nullable(),
|
|
1735
|
+
sourceChanged: z6.boolean()
|
|
1736
|
+
});
|
|
1737
|
+
var AiJobsSchema = z6.object({
|
|
1738
|
+
jobs: z6.array(AiJobSchema),
|
|
1739
|
+
nextCursor: z6.string().nullable()
|
|
1740
|
+
});
|
|
1741
|
+
var AiEventSchema = z6.object({
|
|
1742
|
+
seq: z6.number().int(),
|
|
1743
|
+
attempt: z6.number().int(),
|
|
1744
|
+
kind: z6.string(),
|
|
1745
|
+
message: z6.string(),
|
|
1746
|
+
createdAt: z6.string()
|
|
1747
|
+
});
|
|
1748
|
+
var AiEventsSchema = z6.object({
|
|
1749
|
+
events: z6.array(AiEventSchema),
|
|
1750
|
+
nextCursor: z6.number().int()
|
|
1751
|
+
});
|
|
1752
|
+
var AiAttemptSchema = z6.object({
|
|
1753
|
+
number: z6.number().int(),
|
|
1754
|
+
status: z6.string(),
|
|
1755
|
+
startedAt: z6.string(),
|
|
1756
|
+
endedAt: z6.string().nullable(),
|
|
1757
|
+
error: z6.string().nullable(),
|
|
1758
|
+
usage: AiUsageSchema.nullable()
|
|
1759
|
+
});
|
|
1760
|
+
var AiJobDetailSchema = z6.object({ job: AiJobSchema, attempts: z6.array(AiAttemptSchema) });
|
|
1761
|
+
|
|
1762
|
+
// apps/server/ai/config.ts
|
|
1763
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4 } from "node:crypto";
|
|
1764
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1765
|
+
import { join as join4 } from "node:path";
|
|
1766
|
+
var AiConfig = class {
|
|
1767
|
+
constructor(hub) {
|
|
1768
|
+
this.hub = hub;
|
|
1769
|
+
}
|
|
1770
|
+
hub;
|
|
1771
|
+
key(create = false) {
|
|
1772
|
+
const path = join4(this.hub.directory, "ai-secret.key");
|
|
1773
|
+
if (create) {
|
|
1774
|
+
try {
|
|
1775
|
+
writeFileSync(path, randomBytes4(32), { flag: "wx", mode: 384 });
|
|
1776
|
+
} catch (e) {
|
|
1777
|
+
if (e.code !== "EEXIST") throw e;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const key = readFileSync(path);
|
|
1781
|
+
if (key.length !== 32) throw new Error("Invalid AI secret encryption key.");
|
|
1782
|
+
return key;
|
|
1783
|
+
}
|
|
1784
|
+
encrypt(value, id) {
|
|
1785
|
+
const iv = randomBytes4(12), cipher = createCipheriv("aes-256-gcm", this.key(true), iv);
|
|
1786
|
+
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1787
|
+
const body = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
1788
|
+
return [iv, cipher.getAuthTag(), body].map((b) => b.toString("base64")).join(".");
|
|
1789
|
+
}
|
|
1790
|
+
list() {
|
|
1791
|
+
return this.hub.db.prepare("SELECT * FROM ai_profiles ORDER BY id").all().map((row) => ({
|
|
1792
|
+
...AiProfileSchema.parse(JSON.parse(String(row.config_json))),
|
|
1793
|
+
revision: Number(row.revision),
|
|
1794
|
+
keyConfigured: !!row.secret
|
|
1795
|
+
}));
|
|
1796
|
+
}
|
|
1797
|
+
get(id) {
|
|
1798
|
+
const profile = this.list().find((p) => p.id === id);
|
|
1799
|
+
if (!profile) throw new HubError(404, "AI_PROFILE_NOT_FOUND", "Model profile not found.");
|
|
1800
|
+
const { keyConfigured: _, ...result } = profile;
|
|
1801
|
+
return result;
|
|
1802
|
+
}
|
|
1803
|
+
secret(id) {
|
|
1804
|
+
const row = this.hub.db.prepare("SELECT secret FROM ai_profiles WHERE id=?").get(id);
|
|
1805
|
+
if (!row?.secret) return "";
|
|
1806
|
+
const [iv, tag, body] = String(row.secret).split(".").map((s) => Buffer.from(s, "base64"));
|
|
1807
|
+
const cipher = createDecipheriv("aes-256-gcm", this.key(), iv);
|
|
1808
|
+
cipher.setAAD(Buffer.from(JSON.stringify(["site", id])));
|
|
1809
|
+
cipher.setAuthTag(tag);
|
|
1810
|
+
return Buffer.concat([cipher.update(body), cipher.final()]).toString("utf8");
|
|
1811
|
+
}
|
|
1812
|
+
save(session, value) {
|
|
1813
|
+
requireSiteAdmin(session);
|
|
1814
|
+
const input = AiProfileUpdateSchema.parse(value), { profile } = input;
|
|
1815
|
+
return transaction(this.hub.db, () => {
|
|
1816
|
+
const old = this.hub.db.prepare("SELECT * FROM ai_profiles WHERE id=?").get(profile.id);
|
|
1817
|
+
if ((old ? Number(old.revision) : 0) !== input.expectedRevision)
|
|
1818
|
+
throw new HubError(
|
|
1819
|
+
409,
|
|
1820
|
+
"AI_CONFIG_CONFLICT",
|
|
1821
|
+
"Model configuration changed; reload before saving."
|
|
1822
|
+
);
|
|
1823
|
+
if (!old && this.list().length >= 20)
|
|
1824
|
+
throw new HubError(400, "AI_PROFILE_LIMIT", "At most 20 model profiles are supported.");
|
|
1825
|
+
const secret = input.apiKey === void 0 ? old?.secret ?? null : input.apiKey === null ? null : this.encrypt(input.apiKey, profile.id);
|
|
1826
|
+
this.hub.db.prepare(
|
|
1827
|
+
"INSERT INTO ai_profiles VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,config_json=excluded.config_json,secret=excluded.secret"
|
|
1828
|
+
).run(profile.id, input.expectedRevision + 1, JSON.stringify(profile), secret);
|
|
1829
|
+
return this.list().find((p) => p.id === profile.id);
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// apps/server/ai/defaults.ts
|
|
1835
|
+
var AiDefaults = class {
|
|
1836
|
+
constructor(hub) {
|
|
1605
1837
|
this.hub = hub;
|
|
1606
|
-
this.identity = identity;
|
|
1607
|
-
this.snapshot = snapshot;
|
|
1608
|
-
this.remaining = budget;
|
|
1609
1838
|
}
|
|
1610
1839
|
hub;
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
remaining;
|
|
1618
|
-
afterIndex(after) {
|
|
1619
|
-
if (!after) return 0;
|
|
1620
|
-
const index = this.snapshot.files.findIndex((f) => f.path === after);
|
|
1621
|
-
if (index < 0) throw new HubError(400, "AI_CURSOR_INVALID", "Unknown snapshot cursor.");
|
|
1622
|
-
return index + 1;
|
|
1623
|
-
}
|
|
1624
|
-
fullyRead(path) {
|
|
1625
|
-
let end = 0;
|
|
1626
|
-
for (const range of [...this.readRanges.get(path) ?? []].sort((a, b) => a[0] - b[0])) {
|
|
1627
|
-
if (range[0] > end + 1) break;
|
|
1628
|
-
end = Math.max(end, range[1]);
|
|
1629
|
-
}
|
|
1630
|
-
return this.lineCounts.has(path) && end >= this.lineCounts.get(path);
|
|
1840
|
+
read(scope) {
|
|
1841
|
+
const row = this.hub.db.prepare("SELECT * FROM ai_defaults WHERE scope=?").get(scope);
|
|
1842
|
+
return {
|
|
1843
|
+
profileId: row?.profile_id ? String(row.profile_id) : null,
|
|
1844
|
+
revision: Number(row?.revision ?? 0)
|
|
1845
|
+
};
|
|
1631
1846
|
}
|
|
1632
|
-
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
);
|
|
1847
|
+
view(projectId2) {
|
|
1848
|
+
const site = this.read("site");
|
|
1849
|
+
const own = projectId2 ? this.read(`project:${projectId2}`) : site;
|
|
1850
|
+
const effectiveProfileId = own.profileId ?? site.profileId;
|
|
1637
1851
|
return {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1852
|
+
...own,
|
|
1853
|
+
effectiveProfileId,
|
|
1854
|
+
source: projectId2 && own.profileId ? "project" : site.profileId ? "site" : "none",
|
|
1855
|
+
available: !!effectiveProfileId && new AiConfig(this.hub).get(effectiveProfileId).enabled
|
|
1641
1856
|
};
|
|
1642
1857
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (!file || !this.readFiles.has(path))
|
|
1646
|
-
throw new HubError(
|
|
1647
|
-
422,
|
|
1648
|
-
"AI_TASK_SOURCE_INVALID",
|
|
1649
|
-
"Task source must be an actually read snapshot file."
|
|
1650
|
-
);
|
|
1651
|
-
const row = this.hub.db.prepare(
|
|
1652
|
-
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
1653
|
-
).get(this.identity.workspaceId, this.snapshot.owner, file.revision);
|
|
1654
|
-
if (!row)
|
|
1655
|
-
throw new HubError(500, "AI_SOURCE_VERSION_MISSING", "Source revision metadata is missing.");
|
|
1656
|
-
return { ...file, syncedAt: String(row.created_at) };
|
|
1858
|
+
site() {
|
|
1859
|
+
return this.view();
|
|
1657
1860
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
if (!entry) throw new HubError(403, "AI_SCOPE_VIOLATION", "File is outside the task snapshot.");
|
|
1661
|
-
if (entry.size > 256 * 1024)
|
|
1662
|
-
throw new HubError(413, "AI_FILE_LIMIT", "File exceeds the 256 KiB analysis limit.");
|
|
1663
|
-
const bytes = await new FileRepository(this.hub).download(this.identity, entry.sha256);
|
|
1664
|
-
if (bytes.byteLength !== entry.size || createHash5("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
1665
|
-
throw new HubError(500, "AI_CONTENT_CORRUPT", "Snapshot content verification failed.");
|
|
1666
|
-
let text;
|
|
1667
|
-
try {
|
|
1668
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1669
|
-
} catch {
|
|
1670
|
-
throw new HubError(422, "AI_TEXT_REQUIRED", "Only UTF-8 text files can be analysed.");
|
|
1671
|
-
}
|
|
1672
|
-
if (text.includes("\0"))
|
|
1673
|
-
throw new HubError(422, "AI_TEXT_REQUIRED", "Binary files cannot be analysed.");
|
|
1674
|
-
this.lineCounts.set(path, text.split("\n").length);
|
|
1675
|
-
return text;
|
|
1861
|
+
project(projectId2) {
|
|
1862
|
+
return this.view(projectId2);
|
|
1676
1863
|
}
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
this.remaining -= size;
|
|
1694
|
-
this.readFiles.add(path);
|
|
1695
|
-
const endLine = startLine + selected.length - 1;
|
|
1696
|
-
this.readRanges.set(path, [...this.readRanges.get(path) ?? [], [startLine, endLine]]);
|
|
1697
|
-
return {
|
|
1698
|
-
path,
|
|
1699
|
-
startLine,
|
|
1700
|
-
endLine,
|
|
1701
|
-
totalLines: lines.length,
|
|
1702
|
-
content: selected.map((line, i) => `${startLine + i}: ${line}`).join("\n"),
|
|
1703
|
-
hasMore: endLine < lines.length
|
|
1704
|
-
};
|
|
1864
|
+
save(scope, value) {
|
|
1865
|
+
const input = AiDefaultUpdateSchema.parse(value);
|
|
1866
|
+
transaction(this.hub.db, () => {
|
|
1867
|
+
if (this.read(scope).revision !== input.expectedRevision)
|
|
1868
|
+
throw new HubError(
|
|
1869
|
+
409,
|
|
1870
|
+
"AI_CONFIG_CONFLICT",
|
|
1871
|
+
"Model default changed; reload before saving."
|
|
1872
|
+
);
|
|
1873
|
+
if (input.profileId && !new AiConfig(this.hub).get(input.profileId).enabled)
|
|
1874
|
+
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1875
|
+
this.hub.db.prepare(
|
|
1876
|
+
`INSERT INTO ai_defaults VALUES(?,?,?) ON CONFLICT(scope) DO UPDATE SET profile_id=excluded.profile_id,revision=excluded.revision`
|
|
1877
|
+
).run(scope, input.profileId, input.expectedRevision + 1);
|
|
1878
|
+
});
|
|
1705
1879
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
413,
|
|
1711
|
-
"AI_CONTEXT_LIMIT",
|
|
1712
|
-
"File is too large for this model budget; use directory analysis to read portions."
|
|
1713
|
-
);
|
|
1714
|
-
this.readFiles.add(path);
|
|
1715
|
-
this.readRanges.set(path, [[1, text.split("\n").length]]);
|
|
1716
|
-
return text;
|
|
1880
|
+
saveSite(identity, value) {
|
|
1881
|
+
requireSiteAdmin(identity);
|
|
1882
|
+
this.save("site", value);
|
|
1883
|
+
return this.site();
|
|
1717
1884
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
limited: true
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
scannedBytes += file.size;
|
|
1735
|
-
try {
|
|
1736
|
-
const lines = (await this.text(file.path)).split("\n");
|
|
1737
|
-
for (let i = 0; i < lines.length; i++)
|
|
1738
|
-
if (lines[i].toLowerCase().includes(query.toLowerCase())) {
|
|
1739
|
-
matches.push({ path: file.path, line: i + 1, excerpt: lines[i].slice(0, 400) });
|
|
1740
|
-
if (matches.length >= 30) return { matches, nextCursor: file.path, limited: true };
|
|
1741
|
-
}
|
|
1742
|
-
} catch (e) {
|
|
1743
|
-
if (e instanceof HubError && e.code === "AI_TEXT_REQUIRED") {
|
|
1744
|
-
this.limitations.add("\u90E8\u5206\u975E\u6587\u672C\u6587\u4EF6\u672A\u53C2\u4E0E\u6B63\u6587\u641C\u7D22\u3002");
|
|
1745
|
-
continue;
|
|
1746
|
-
}
|
|
1747
|
-
throw e;
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
return {
|
|
1751
|
-
matches,
|
|
1752
|
-
nextCursor: files.length === limit ? files.at(-1).path : null,
|
|
1753
|
-
limited: false
|
|
1754
|
-
};
|
|
1885
|
+
saveProject(identity, value) {
|
|
1886
|
+
new ProjectRepository(this.hub).requireMember(identity);
|
|
1887
|
+
this.save(`project:${identity.workspaceId}`, value);
|
|
1888
|
+
return this.project(identity.workspaceId);
|
|
1889
|
+
}
|
|
1890
|
+
resolve(projectId2, explicit) {
|
|
1891
|
+
const id = explicit ?? this.project(projectId2).effectiveProfileId;
|
|
1892
|
+
if (!id)
|
|
1893
|
+
throw new HubError(400, "AI_DEFAULT_REQUIRED", "Select a model or configure a default.");
|
|
1894
|
+
const profile = new AiConfig(this.hub).get(id);
|
|
1895
|
+
if (!profile.enabled)
|
|
1896
|
+
throw new HubError(400, "AI_PROFILE_DISABLED", "Model profile is disabled.");
|
|
1897
|
+
return profile;
|
|
1755
1898
|
}
|
|
1756
1899
|
};
|
|
1757
1900
|
|
|
@@ -1760,32 +1903,32 @@ import { createHash as createHash6 } from "node:crypto";
|
|
|
1760
1903
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1761
1904
|
import { resolve as resolve2 } from "node:path";
|
|
1762
1905
|
import { fileURLToPath } from "node:url";
|
|
1763
|
-
import { z as
|
|
1764
|
-
var WorkflowNameSchema =
|
|
1765
|
-
var ToolNameSchema =
|
|
1766
|
-
var ToolTextSchema =
|
|
1767
|
-
var RuntimeConfigSchema =
|
|
1768
|
-
version:
|
|
1769
|
-
thinkingLevel:
|
|
1770
|
-
tools:
|
|
1771
|
-
maxRounds:
|
|
1772
|
-
maxToolCalls:
|
|
1773
|
-
timeoutMs:
|
|
1774
|
-
requestTimeoutMs:
|
|
1775
|
-
maxReadBytes:
|
|
1776
|
-
settleSeconds:
|
|
1906
|
+
import { z as z7 } from "zod";
|
|
1907
|
+
var WorkflowNameSchema = z7.enum(["file-summary", "directory-analysis", "task-bootstrap"]);
|
|
1908
|
+
var ToolNameSchema = z7.enum(["list_files", "read_file", "search_files"]);
|
|
1909
|
+
var ToolTextSchema = z7.object({ label: z7.string().min(1).max(100), description: z7.string().min(1).max(2e3) }).strict();
|
|
1910
|
+
var RuntimeConfigSchema = z7.object({
|
|
1911
|
+
version: z7.number().int().positive(),
|
|
1912
|
+
thinkingLevel: z7.enum(["off", "minimal", "low", "medium", "high"]),
|
|
1913
|
+
tools: z7.array(ToolNameSchema).max(3).refine((v) => new Set(v).size === v.length),
|
|
1914
|
+
maxRounds: z7.number().int().min(1).max(100),
|
|
1915
|
+
maxToolCalls: z7.number().int().min(1).max(500),
|
|
1916
|
+
timeoutMs: z7.number().int().min(1e3).max(9e5),
|
|
1917
|
+
requestTimeoutMs: z7.number().int().min(1e3).max(12e4),
|
|
1918
|
+
maxReadBytes: z7.number().int().min(1024).max(2e6),
|
|
1919
|
+
settleSeconds: z7.number().int().min(0).max(3600).optional()
|
|
1777
1920
|
}).strict();
|
|
1778
|
-
var WorkflowRuntimeSchema =
|
|
1921
|
+
var WorkflowRuntimeSchema = z7.object({
|
|
1779
1922
|
workflow: WorkflowNameSchema,
|
|
1780
|
-
system:
|
|
1781
|
-
prompt:
|
|
1923
|
+
system: z7.string().min(1).max(32e3),
|
|
1924
|
+
prompt: z7.string().min(1).max(32e3),
|
|
1782
1925
|
config: RuntimeConfigSchema,
|
|
1783
|
-
toolText:
|
|
1926
|
+
toolText: z7.object({
|
|
1784
1927
|
list_files: ToolTextSchema,
|
|
1785
1928
|
read_file: ToolTextSchema,
|
|
1786
1929
|
search_files: ToolTextSchema
|
|
1787
1930
|
}).strict(),
|
|
1788
|
-
fingerprint:
|
|
1931
|
+
fingerprint: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
1789
1932
|
}).strict();
|
|
1790
1933
|
function loadWorkflow(workflow, directory = process.env.TEAMMY_AI_RUNTIME_DIR) {
|
|
1791
1934
|
try {
|
|
@@ -1823,7 +1966,7 @@ function workflowPrompt(runtime, schema) {
|
|
|
1823
1966
|
${runtime.prompt}
|
|
1824
1967
|
|
|
1825
1968
|
JSON Schema:
|
|
1826
|
-
${JSON.stringify(
|
|
1969
|
+
${JSON.stringify(z7.toJSONSchema(schema))}`;
|
|
1827
1970
|
}
|
|
1828
1971
|
|
|
1829
1972
|
// apps/server/ai/access.ts
|
|
@@ -1853,8 +1996,7 @@ function applyTaskResult(hub, job, result) {
|
|
|
1853
1996
|
const current = hub.db.prepare("SELECT job_id FROM task_board_members WHERE workspace_id=? AND owner=?").get(job.workspace, job.snapshot.owner);
|
|
1854
1997
|
if (current?.job_id !== job.id)
|
|
1855
1998
|
throw new HubError(409, "AI_TASK_SUPERSEDED", "A newer workspace analysis exists.");
|
|
1856
|
-
|
|
1857
|
-
if (Number(head?.file_revision) !== job.snapshot.revision)
|
|
1999
|
+
if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
|
|
1858
2000
|
throw new HubError(
|
|
1859
2001
|
409,
|
|
1860
2002
|
"AI_SOURCE_CHANGED",
|
|
@@ -1954,7 +2096,7 @@ var TaskStore = class {
|
|
|
1954
2096
|
jobId: row.job_id ?? null,
|
|
1955
2097
|
status: row.status ?? (row.has_files ? "waiting" : "empty"),
|
|
1956
2098
|
error: row.error ?? null,
|
|
1957
|
-
sourceChanged: snapshot ?
|
|
2099
|
+
sourceChanged: snapshot ? !taskSnapshotCurrent(this.hub, session.workspaceId, snapshot) : false,
|
|
1958
2100
|
partial: result?.coverage.partial ?? false,
|
|
1959
2101
|
uncertainties: result ? [...result.uncertainties, ...result.coverage.limitations] : []
|
|
1960
2102
|
};
|
|
@@ -2023,7 +2165,10 @@ var AiJobs = class {
|
|
|
2023
2165
|
this.hub,
|
|
2024
2166
|
{ workspaceId: String(row.workspace_id), memberId: String(row.requester) },
|
|
2025
2167
|
{ owner: snapshot.owner, kind: snapshot.kind, path: snapshot.path },
|
|
2026
|
-
|
|
2168
|
+
{
|
|
2169
|
+
allowEmpty: request.workflow === "task-bootstrap",
|
|
2170
|
+
projectReferences: request.workflow === "task-bootstrap"
|
|
2171
|
+
}
|
|
2027
2172
|
).fingerprint !== snapshot.fingerprint;
|
|
2028
2173
|
} catch {
|
|
2029
2174
|
sourceChanged = true;
|
|
@@ -2144,18 +2289,21 @@ var AiJobs = class {
|
|
|
2144
2289
|
"Task bootstrap uses the current complete member workspace without an additional question."
|
|
2145
2290
|
);
|
|
2146
2291
|
const id = this.operation(session, input.operationId, { action: "create", ...input }, () => {
|
|
2147
|
-
const snapshot = captureSnapshot(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
input.workflow === "task-bootstrap"
|
|
2152
|
-
);
|
|
2292
|
+
const snapshot = captureSnapshot(this.hub, session, input.selection, {
|
|
2293
|
+
allowEmpty: input.workflow === "task-bootstrap",
|
|
2294
|
+
projectReferences: input.workflow === "task-bootstrap"
|
|
2295
|
+
});
|
|
2153
2296
|
if (input.workflow === "task-bootstrap") {
|
|
2154
2297
|
if (origin === "automatic") {
|
|
2155
2298
|
const published = this.hub.db.prepare(
|
|
2156
|
-
"SELECT created_at FROM file_revisions WHERE workspace_id=? AND owner=? AND revision=?"
|
|
2157
|
-
).get(
|
|
2158
|
-
|
|
2299
|
+
"SELECT MAX(created_at) created_at FROM file_revisions WHERE workspace_id=? AND ((owner=? AND revision=?) OR (owner='_project' AND revision=?))"
|
|
2300
|
+
).get(
|
|
2301
|
+
session.workspaceId,
|
|
2302
|
+
snapshot.owner,
|
|
2303
|
+
snapshot.revision,
|
|
2304
|
+
snapshot.project?.revision ?? 0
|
|
2305
|
+
);
|
|
2306
|
+
if (!published?.created_at || (scheduledAt ?? Date.now()) - Date.parse(String(published.created_at)) < resource.config.settleSeconds * 1e3)
|
|
2159
2307
|
throw new HubError(
|
|
2160
2308
|
409,
|
|
2161
2309
|
"AI_TASK_UNSETTLED",
|
|
@@ -2169,7 +2317,7 @@ var AiJobs = class {
|
|
|
2169
2317
|
if (latest) {
|
|
2170
2318
|
const previous = JSON.parse(String(latest.snapshot_json));
|
|
2171
2319
|
if (["queued", "running"].includes(String(latest.status))) {
|
|
2172
|
-
if (origin === "manual" && previous.revision === snapshot.revision)
|
|
2320
|
+
if (origin === "manual" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
|
|
2173
2321
|
return String(latest.id);
|
|
2174
2322
|
throw new HubError(
|
|
2175
2323
|
409,
|
|
@@ -2177,7 +2325,7 @@ var AiJobs = class {
|
|
|
2177
2325
|
"This member already has an analysis in progress."
|
|
2178
2326
|
);
|
|
2179
2327
|
}
|
|
2180
|
-
if (origin === "automatic" && previous.revision === snapshot.revision)
|
|
2328
|
+
if (origin === "automatic" && previous.revision === snapshot.revision && (previous.project?.revision ?? 0) === (snapshot.project?.revision ?? 0))
|
|
2181
2329
|
throw new HubError(
|
|
2182
2330
|
409,
|
|
2183
2331
|
"AI_TASK_CURRENT",
|
|
@@ -2368,7 +2516,7 @@ import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi
|
|
|
2368
2516
|
|
|
2369
2517
|
// apps/server/ai/tools.ts
|
|
2370
2518
|
import { Type } from "@earendil-works/pi-ai";
|
|
2371
|
-
import { z as
|
|
2519
|
+
import { z as z8 } from "zod";
|
|
2372
2520
|
function analysisTools(reader, signal, progress, runtime) {
|
|
2373
2521
|
let calls = 0;
|
|
2374
2522
|
const guard = () => {
|
|
@@ -2387,15 +2535,21 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2387
2535
|
name: "list_files",
|
|
2388
2536
|
...runtime.toolText.list_files,
|
|
2389
2537
|
parameters: Type.Object(
|
|
2390
|
-
{
|
|
2538
|
+
{
|
|
2539
|
+
after: Type.Optional(Type.String({ maxLength: 1024 })),
|
|
2540
|
+
scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")]))
|
|
2541
|
+
},
|
|
2391
2542
|
{ additionalProperties: false }
|
|
2392
2543
|
),
|
|
2393
2544
|
executionMode: "sequential",
|
|
2394
2545
|
async execute(_id, params) {
|
|
2395
2546
|
guard();
|
|
2396
|
-
const p =
|
|
2547
|
+
const p = z8.object({
|
|
2548
|
+
after: z8.string().max(1024).default(""),
|
|
2549
|
+
scope: z8.enum(["member", "project"]).default("member")
|
|
2550
|
+
}).strict().parse(params);
|
|
2397
2551
|
progress("list", "\u6D4F\u89C8\u6587\u4EF6\u6E05\u5355\u3002");
|
|
2398
|
-
return reply(reader.list(p.after, 30));
|
|
2552
|
+
return reply((p.scope === "project" ? reader.projectReader() : reader).list(p.after, 30));
|
|
2399
2553
|
}
|
|
2400
2554
|
},
|
|
2401
2555
|
{
|
|
@@ -2404,6 +2558,7 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2404
2558
|
parameters: Type.Object(
|
|
2405
2559
|
{
|
|
2406
2560
|
path: Type.String(),
|
|
2561
|
+
scope: Type.Optional(Type.Union([Type.Literal("member"), Type.Literal("project")])),
|
|
2407
2562
|
startLine: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
2408
2563
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 150 }))
|
|
2409
2564
|
},
|
|
@@ -2412,13 +2567,20 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2412
2567
|
executionMode: "sequential",
|
|
2413
2568
|
async execute(_id, params) {
|
|
2414
2569
|
guard();
|
|
2415
|
-
const p =
|
|
2570
|
+
const p = z8.object({
|
|
2416
2571
|
path: FilePathSchema,
|
|
2417
|
-
|
|
2418
|
-
|
|
2572
|
+
scope: z8.enum(["member", "project"]).default("member"),
|
|
2573
|
+
startLine: z8.number().int().min(1).max(1e6).default(1),
|
|
2574
|
+
limit: z8.number().int().min(1).max(150).default(150)
|
|
2419
2575
|
}).strict().parse(params);
|
|
2420
2576
|
progress("read", `\u8BFB\u53D6 ${p.path}\uFF0C\u7B2C ${p.startLine} \u884C\u8D77\u3002`);
|
|
2421
|
-
return reply(
|
|
2577
|
+
return reply(
|
|
2578
|
+
await (p.scope === "project" ? reader.projectReader() : reader).read(
|
|
2579
|
+
p.path,
|
|
2580
|
+
p.startLine,
|
|
2581
|
+
p.limit
|
|
2582
|
+
)
|
|
2583
|
+
);
|
|
2422
2584
|
}
|
|
2423
2585
|
},
|
|
2424
2586
|
{
|
|
@@ -2434,7 +2596,7 @@ function analysisTools(reader, signal, progress, runtime) {
|
|
|
2434
2596
|
executionMode: "sequential",
|
|
2435
2597
|
async execute(_id, params) {
|
|
2436
2598
|
guard();
|
|
2437
|
-
const p =
|
|
2599
|
+
const p = z8.object({ query: z8.string().min(1).max(200), after: z8.string().max(1024).default("") }).strict().parse(params);
|
|
2438
2600
|
progress("search", "\u641C\u7D22\u5FEB\u7167\u6B63\u6587\u3002");
|
|
2439
2601
|
return reply(await reader.search(p.query, p.after));
|
|
2440
2602
|
}
|
|
@@ -2704,16 +2866,16 @@ async function runAgent(profile, reader, key, signal, progress, runtime, systemP
|
|
|
2704
2866
|
}
|
|
2705
2867
|
|
|
2706
2868
|
// apps/server/ai/results.ts
|
|
2707
|
-
import { z as
|
|
2708
|
-
var SummaryOutputSchema =
|
|
2709
|
-
summary:
|
|
2710
|
-
findings:
|
|
2711
|
-
uncertainties:
|
|
2712
|
-
citations:
|
|
2713
|
-
|
|
2869
|
+
import { z as z9 } from "zod";
|
|
2870
|
+
var SummaryOutputSchema = z9.object({
|
|
2871
|
+
summary: z9.string().min(1).max(2e4),
|
|
2872
|
+
findings: z9.array(z9.string().max(2e3)).max(30),
|
|
2873
|
+
uncertainties: z9.array(z9.string().max(2e3)).max(30),
|
|
2874
|
+
citations: z9.array(
|
|
2875
|
+
z9.object({
|
|
2714
2876
|
path: FilePathSchema,
|
|
2715
|
-
startLine:
|
|
2716
|
-
endLine:
|
|
2877
|
+
startLine: z9.number().int().positive(),
|
|
2878
|
+
endLine: z9.number().int().positive()
|
|
2717
2879
|
}).strict()
|
|
2718
2880
|
).min(1).max(50)
|
|
2719
2881
|
}).strict();
|
|
@@ -2826,15 +2988,15 @@ async function summarizeFile(job, reader, key, signal, runtime) {
|
|
|
2826
2988
|
}
|
|
2827
2989
|
|
|
2828
2990
|
// apps/server/ai/workflows/task-bootstrap.ts
|
|
2829
|
-
import { z as
|
|
2830
|
-
var TaskOutputSchema =
|
|
2831
|
-
tasks:
|
|
2991
|
+
import { z as z10 } from "zod";
|
|
2992
|
+
var TaskOutputSchema = z10.object({
|
|
2993
|
+
tasks: z10.array(
|
|
2832
2994
|
TaskFieldsSchema.extend({
|
|
2833
|
-
taskId:
|
|
2834
|
-
sources:
|
|
2995
|
+
taskId: z10.uuid().nullable().optional(),
|
|
2996
|
+
sources: z10.array(FilePathSchema).min(1).max(50).refine((v) => new Set(v).size === v.length)
|
|
2835
2997
|
})
|
|
2836
2998
|
).max(100),
|
|
2837
|
-
uncertainties:
|
|
2999
|
+
uncertainties: z10.array(z10.string().max(2e3)).max(50)
|
|
2838
3000
|
}).strict();
|
|
2839
3001
|
function validateTasks(text, reader, context) {
|
|
2840
3002
|
let output;
|
|
@@ -2893,7 +3055,8 @@ async function bootstrapTasks(job, reader, key, signal, progress, runtime) {
|
|
|
2893
3055
|
owner: job.snapshot.owner,
|
|
2894
3056
|
revision: job.snapshot.revision,
|
|
2895
3057
|
inventory: reader.list("", 100),
|
|
2896
|
-
previous: job.taskContext
|
|
3058
|
+
previous: job.taskContext,
|
|
3059
|
+
projectReferences: job.snapshot.project ? reader.projectReader().list("", 100) : null
|
|
2897
3060
|
}
|
|
2898
3061
|
);
|
|
2899
3062
|
return { result: validateTasks(output.text, reader, job.taskContext), usage: output.usage };
|
|
@@ -2914,13 +3077,15 @@ function requestTaskBootstrap(hub, session, owner, operationId) {
|
|
|
2914
3077
|
}
|
|
2915
3078
|
function scheduleTaskBootstraps(hub, at = Date.now()) {
|
|
2916
3079
|
const candidates = hub.db.prepare(
|
|
2917
|
-
`SELECT m.workspace_id,m.member_id,r.created_at
|
|
3080
|
+
`SELECT m.workspace_id,m.member_id,MAX(r.created_at,COALESCE(pr.created_at,r.created_at)) created_at
|
|
2918
3081
|
FROM members m JOIN projects p ON p.id=m.workspace_id JOIN users u ON u.id=m.member_id
|
|
2919
3082
|
JOIN file_revisions r ON r.workspace_id=m.workspace_id AND r.owner=m.member_id AND r.revision=m.file_revision
|
|
3083
|
+
LEFT JOIN project_files pf ON pf.workspace_id=m.workspace_id
|
|
3084
|
+
LEFT JOIN file_revisions pr ON pr.workspace_id=m.workspace_id AND pr.owner='_project' AND pr.revision=pf.file_revision
|
|
2920
3085
|
LEFT JOIN task_board_members b ON b.workspace_id=m.workspace_id AND b.owner=m.member_id LEFT JOIN ai_jobs j ON j.id=b.job_id
|
|
2921
3086
|
WHERE m.enabled=1 AND u.enabled=1 AND (
|
|
2922
3087
|
(b.job_id IS NULL AND EXISTS(SELECT 1 FROM current_files f WHERE f.workspace_id=m.workspace_id AND f.owner=m.member_id))
|
|
2923
|
-
OR (j.status IN ('succeeded','failed','cancelled') AND json_extract(j.snapshot_json,'$.revision') < m.file_revision)
|
|
3088
|
+
OR (j.status IN ('succeeded','failed','cancelled') AND (json_extract(j.snapshot_json,'$.revision') < m.file_revision OR COALESCE(json_extract(j.snapshot_json,'$.project.revision'),0) < COALESCE(pf.file_revision,0)))
|
|
2924
3089
|
)
|
|
2925
3090
|
ORDER BY r.created_at,m.workspace_id,m.member_id`
|
|
2926
3091
|
).all();
|
|
@@ -2985,8 +3150,7 @@ async function runAiCycle(hub, stop) {
|
|
|
2985
3150
|
if (!requesterActive())
|
|
2986
3151
|
throw new HubError(403, "AI_REQUESTER_DISABLED", "Requester is disabled.");
|
|
2987
3152
|
if (job.request.workflow === "task-bootstrap") {
|
|
2988
|
-
|
|
2989
|
-
if (Number(current?.file_revision) !== job.snapshot.revision)
|
|
3153
|
+
if (!taskSnapshotCurrent(hub, job.workspace, job.snapshot))
|
|
2990
3154
|
throw new HubError(
|
|
2991
3155
|
409,
|
|
2992
3156
|
"AI_SOURCE_CHANGED",
|
|
@@ -3083,7 +3247,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
3083
3247
|
import { parseArgs } from "node:util";
|
|
3084
3248
|
|
|
3085
3249
|
// apps/server/ai/job-routes.ts
|
|
3086
|
-
import { z as
|
|
3250
|
+
import { z as z11 } from "zod";
|
|
3087
3251
|
|
|
3088
3252
|
// apps/server/web-auth.ts
|
|
3089
3253
|
function cookieToken(request) {
|
|
@@ -3129,10 +3293,10 @@ function authorizeRead(store, request) {
|
|
|
3129
3293
|
// apps/server/ai/job-routes.ts
|
|
3130
3294
|
function registerAiJobRoutes(server, hub) {
|
|
3131
3295
|
const jobs = new AiJobs(hub);
|
|
3132
|
-
const id = (params) =>
|
|
3296
|
+
const id = (params) => z11.object({ id: z11.uuid() }).parse(params).id;
|
|
3133
3297
|
server.get("/ai/jobs", async (req) => {
|
|
3134
|
-
const session = authorizeWeb(hub, req), query =
|
|
3135
|
-
after:
|
|
3298
|
+
const session = authorizeWeb(hub, req), query = z11.object({
|
|
3299
|
+
after: z11.string().regex(/^\d{0,15}$/).default("")
|
|
3136
3300
|
}).parse(req.query);
|
|
3137
3301
|
return jobs.list(session.workspaceId, query.after);
|
|
3138
3302
|
});
|
|
@@ -3145,7 +3309,7 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3145
3309
|
async (req) => jobs.get(authorizeWeb(hub, req).workspaceId, id(req.params))
|
|
3146
3310
|
);
|
|
3147
3311
|
server.get("/ai/jobs/:id/events", async (req) => {
|
|
3148
|
-
const query =
|
|
3312
|
+
const query = z11.object({ after: z11.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0) }).parse(req.query);
|
|
3149
3313
|
return jobs.events(authorizeWeb(hub, req).workspaceId, id(req.params), query.after);
|
|
3150
3314
|
});
|
|
3151
3315
|
server.post("/ai/jobs/:id/cancel", async (req) => {
|
|
@@ -3157,13 +3321,13 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3157
3321
|
return jobs.retry(
|
|
3158
3322
|
authorizeWeb(hub, req),
|
|
3159
3323
|
id(req.params),
|
|
3160
|
-
|
|
3324
|
+
z11.object({ operationId: z11.uuid() }).strict().parse(req.body).operationId
|
|
3161
3325
|
);
|
|
3162
3326
|
});
|
|
3163
3327
|
server.get("/ai/jobs/:id/source", async (req) => {
|
|
3164
3328
|
const session = authorizeWeb(hub, req), jobId = id(req.params);
|
|
3165
3329
|
jobs.get(session.workspaceId, jobId);
|
|
3166
|
-
const query =
|
|
3330
|
+
const query = z11.object({ path: z11.string().max(1024), startLine: z11.coerce.number().int().min(1).default(1) }).parse(req.query);
|
|
3167
3331
|
const row = hub.db.prepare("SELECT snapshot_json FROM ai_jobs WHERE id=? AND workspace_id=?").get(jobId, session.workspaceId);
|
|
3168
3332
|
if (!row) throw new HubError(404, "AI_JOB_NOT_FOUND", "Task not found.");
|
|
3169
3333
|
return new SnapshotReader(hub, session, JSON.parse(String(row.snapshot_json))).read(
|
|
@@ -3175,10 +3339,10 @@ function registerAiJobRoutes(server, hub) {
|
|
|
3175
3339
|
}
|
|
3176
3340
|
|
|
3177
3341
|
// apps/server/ai/task-routes.ts
|
|
3178
|
-
import { z as
|
|
3342
|
+
import { z as z12 } from "zod";
|
|
3179
3343
|
function registerTaskRoutes(server, hub) {
|
|
3180
3344
|
const store = new TaskStore(hub);
|
|
3181
|
-
const id = (params) =>
|
|
3345
|
+
const id = (params) => z12.object({ id: z12.uuid() }).parse(params).id;
|
|
3182
3346
|
server.get("/tasks", async (req) => store.board(authorizeWeb(hub, req)));
|
|
3183
3347
|
server.post("/tasks/bootstrap", async (req) => {
|
|
3184
3348
|
webMutation(req);
|
|
@@ -3312,20 +3476,20 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
3312
3476
|
import Fastify from "fastify";
|
|
3313
3477
|
|
|
3314
3478
|
// packages/protocol/index.ts
|
|
3315
|
-
import { z as
|
|
3479
|
+
import { z as z13 } from "zod";
|
|
3316
3480
|
var PROTOCOL_VERSION = 1;
|
|
3317
|
-
var HealthSchema =
|
|
3318
|
-
var ServerInfoSchema =
|
|
3319
|
-
name:
|
|
3320
|
-
version:
|
|
3321
|
-
protocolVersion:
|
|
3322
|
-
capabilities:
|
|
3481
|
+
var HealthSchema = z13.object({ status: z13.literal("ok") });
|
|
3482
|
+
var ServerInfoSchema = z13.object({
|
|
3483
|
+
name: z13.literal("teammy"),
|
|
3484
|
+
version: z13.string().min(1),
|
|
3485
|
+
protocolVersion: z13.literal(PROTOCOL_VERSION),
|
|
3486
|
+
capabilities: z13.array(z13.string())
|
|
3323
3487
|
});
|
|
3324
|
-
var ErrorResponseSchema =
|
|
3325
|
-
error:
|
|
3326
|
-
code:
|
|
3327
|
-
message:
|
|
3328
|
-
requestId:
|
|
3488
|
+
var ErrorResponseSchema = z13.object({
|
|
3489
|
+
error: z13.object({
|
|
3490
|
+
code: z13.string(),
|
|
3491
|
+
message: z13.string(),
|
|
3492
|
+
requestId: z13.string()
|
|
3329
3493
|
})
|
|
3330
3494
|
});
|
|
3331
3495
|
|
|
@@ -3333,43 +3497,43 @@ var ErrorResponseSchema = z12.object({
|
|
|
3333
3497
|
import { ZodError } from "zod";
|
|
3334
3498
|
|
|
3335
3499
|
// packages/protocol/devices.ts
|
|
3336
|
-
import { z as
|
|
3337
|
-
var CredentialSchema =
|
|
3338
|
-
var EnrollmentSchema =
|
|
3339
|
-
operationId:
|
|
3500
|
+
import { z as z14 } from "zod";
|
|
3501
|
+
var CredentialSchema = z14.string().regex(/^tmy_[a-f0-9]{64}$/);
|
|
3502
|
+
var EnrollmentSchema = z14.object({
|
|
3503
|
+
operationId: z14.uuid(),
|
|
3340
3504
|
projectId: NameSchema,
|
|
3341
3505
|
memberId: NameSchema,
|
|
3342
3506
|
deviceId: DeviceIdSchema,
|
|
3343
3507
|
credential: CredentialSchema
|
|
3344
3508
|
}).strict();
|
|
3345
|
-
var EffectivePolicySchema =
|
|
3346
|
-
checkIntervalSeconds:
|
|
3347
|
-
fileSettleSeconds:
|
|
3509
|
+
var EffectivePolicySchema = z14.object({
|
|
3510
|
+
checkIntervalSeconds: z14.number().min(1).max(86400),
|
|
3511
|
+
fileSettleSeconds: z14.number().min(0).max(86400)
|
|
3348
3512
|
}).strict();
|
|
3349
|
-
var SyncSummarySchema =
|
|
3350
|
-
checkedAt:
|
|
3351
|
-
published:
|
|
3352
|
-
appliedFiles:
|
|
3353
|
-
pendingFiles:
|
|
3354
|
-
sessionSources:
|
|
3355
|
-
errors:
|
|
3513
|
+
var SyncSummarySchema = z14.object({
|
|
3514
|
+
checkedAt: z14.string(),
|
|
3515
|
+
published: z14.boolean(),
|
|
3516
|
+
appliedFiles: z14.number().int().nonnegative(),
|
|
3517
|
+
pendingFiles: z14.number().int().nonnegative(),
|
|
3518
|
+
sessionSources: z14.number().int().nonnegative(),
|
|
3519
|
+
errors: z14.array(z14.string().max(1e3)).max(20)
|
|
3356
3520
|
}).strict();
|
|
3357
|
-
var DeviceStatusSchema =
|
|
3358
|
-
clientVersion:
|
|
3521
|
+
var DeviceStatusSchema = z14.object({
|
|
3522
|
+
clientVersion: z14.string().max(64),
|
|
3359
3523
|
receivedRevision: RevisionSchema.nullable(),
|
|
3360
3524
|
appliedRevision: RevisionSchema.nullable(),
|
|
3361
3525
|
effectivePolicy: EffectivePolicySchema,
|
|
3362
|
-
overrides:
|
|
3363
|
-
configError:
|
|
3364
|
-
reportError:
|
|
3526
|
+
overrides: z14.array(z14.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
|
|
3527
|
+
configError: z14.string().max(2e3).nullable(),
|
|
3528
|
+
reportError: z14.string().max(2e3).nullable(),
|
|
3365
3529
|
lastSync: SyncSummarySchema.nullable()
|
|
3366
3530
|
}).strict();
|
|
3367
|
-
var WebDevicesSchema =
|
|
3368
|
-
devices:
|
|
3369
|
-
|
|
3531
|
+
var WebDevicesSchema = z14.object({
|
|
3532
|
+
devices: z14.array(
|
|
3533
|
+
z14.object({
|
|
3370
3534
|
memberId: NameSchema,
|
|
3371
|
-
bound:
|
|
3372
|
-
lastSeenAt:
|
|
3535
|
+
bound: z14.boolean(),
|
|
3536
|
+
lastSeenAt: z14.string().nullable(),
|
|
3373
3537
|
status: DeviceStatusSchema.nullable()
|
|
3374
3538
|
})
|
|
3375
3539
|
)
|
|
@@ -3407,7 +3571,7 @@ function registerControlRoutes(server, store) {
|
|
|
3407
3571
|
}
|
|
3408
3572
|
|
|
3409
3573
|
// apps/server/project-routes.ts
|
|
3410
|
-
import { z as
|
|
3574
|
+
import { z as z15 } from "zod";
|
|
3411
3575
|
function registerProjectRoutes(server, store) {
|
|
3412
3576
|
const project = new ProjectRepository(store), site = new SiteRepository(store);
|
|
3413
3577
|
server.get("/api/v1/projects", async (request) => ({
|
|
@@ -3454,7 +3618,7 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3454
3618
|
});
|
|
3455
3619
|
server.patch("/web/members/:id", async (request) => {
|
|
3456
3620
|
webMutation(request);
|
|
3457
|
-
const { id } =
|
|
3621
|
+
const { id } = z15.object({ id: NameSchema }).parse(request.params);
|
|
3458
3622
|
return project.updateMember(
|
|
3459
3623
|
authorizeWeb(store, request),
|
|
3460
3624
|
id,
|
|
@@ -3463,13 +3627,13 @@ function registerProjectScopeRoutes(server, store) {
|
|
|
3463
3627
|
});
|
|
3464
3628
|
server.post("/web/members/:id/device-reset", async (request) => {
|
|
3465
3629
|
webMutation(request);
|
|
3466
|
-
const { id } =
|
|
3630
|
+
const { id } = z15.object({ id: NameSchema }).parse(request.params);
|
|
3467
3631
|
return project.resetDevice(authorizeWeb(store, request), id);
|
|
3468
3632
|
});
|
|
3469
3633
|
}
|
|
3470
3634
|
|
|
3471
3635
|
// apps/server/site-routes.ts
|
|
3472
|
-
import { z as
|
|
3636
|
+
import { z as z16 } from "zod";
|
|
3473
3637
|
|
|
3474
3638
|
// apps/server/devices.ts
|
|
3475
3639
|
var DeviceRepository = class {
|
|
@@ -3487,7 +3651,7 @@ var DeviceRepository = class {
|
|
|
3487
3651
|
).get(projectId2, value.memberId);
|
|
3488
3652
|
if (!member)
|
|
3489
3653
|
throw new HubError(401, "UNKNOWN_MEMBER", "Member ID does not exist or has been disabled.");
|
|
3490
|
-
const fingerprint =
|
|
3654
|
+
const fingerprint = digest(JSON.stringify(value));
|
|
3491
3655
|
const prior = this.hub.db.prepare(
|
|
3492
3656
|
"SELECT fingerprint FROM device_enrollments WHERE workspace_id=? AND member_id=? AND operation_id=?"
|
|
3493
3657
|
).get(projectId2, value.memberId, value.operationId);
|
|
@@ -3497,7 +3661,7 @@ var DeviceRepository = class {
|
|
|
3497
3661
|
"OPERATION_MISMATCH",
|
|
3498
3662
|
"Enrollment operation was reused with different content."
|
|
3499
3663
|
);
|
|
3500
|
-
const tokenHash =
|
|
3664
|
+
const tokenHash = digest(value.credential);
|
|
3501
3665
|
if (prior) {
|
|
3502
3666
|
if (member.token_hash !== tokenHash || member.device_id !== value.deviceId)
|
|
3503
3667
|
throw new HubError(
|
|
@@ -3545,7 +3709,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3545
3709
|
"/api/v1/web/session",
|
|
3546
3710
|
{ onRequest: async (request) => webMutation(request) },
|
|
3547
3711
|
async (request, reply) => {
|
|
3548
|
-
const { id } =
|
|
3712
|
+
const { id } = z16.object({ id: NameSchema }).strict().parse(request.body);
|
|
3549
3713
|
const { token, session } = site.login(id);
|
|
3550
3714
|
const secure = request.protocol === "https" || request.headers.origin?.startsWith("https://");
|
|
3551
3715
|
reply.header(
|
|
@@ -3559,7 +3723,7 @@ function registerSiteRoutes(server, store) {
|
|
|
3559
3723
|
server.patch("/api/v1/web/session", async (request) => {
|
|
3560
3724
|
webMutation(request);
|
|
3561
3725
|
const identity = site.identity(cookieToken(request));
|
|
3562
|
-
const { displayName } =
|
|
3726
|
+
const { displayName } = z16.object({ displayName: z16.string().trim().max(120) }).strict().parse(request.body);
|
|
3563
3727
|
return site.renameSelf(identity, displayName);
|
|
3564
3728
|
});
|
|
3565
3729
|
server.delete("/api/v1/web/session", async (request, reply) => {
|
|
@@ -3600,71 +3764,71 @@ function registerSiteRoutes(server, store) {
|
|
|
3600
3764
|
}
|
|
3601
3765
|
|
|
3602
3766
|
// apps/server/session-routes.ts
|
|
3603
|
-
import { z as
|
|
3767
|
+
import { z as z18 } from "zod";
|
|
3604
3768
|
|
|
3605
3769
|
// packages/protocol/sessions.ts
|
|
3606
|
-
import { z as
|
|
3607
|
-
var AgentSchema =
|
|
3608
|
-
var SessionSnapshotSchema =
|
|
3609
|
-
formatVersion:
|
|
3610
|
-
adapterVersion:
|
|
3770
|
+
import { z as z17 } from "zod";
|
|
3771
|
+
var AgentSchema = z17.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
|
|
3772
|
+
var SessionSnapshotSchema = z17.object({
|
|
3773
|
+
formatVersion: z17.literal(1),
|
|
3774
|
+
adapterVersion: z17.literal(1),
|
|
3611
3775
|
agent: AgentSchema,
|
|
3612
|
-
nativeId:
|
|
3613
|
-
cwd:
|
|
3614
|
-
messages:
|
|
3615
|
-
|
|
3616
|
-
id:
|
|
3617
|
-
role:
|
|
3618
|
-
text:
|
|
3619
|
-
source:
|
|
3776
|
+
nativeId: z17.string().min(1).max(512),
|
|
3777
|
+
cwd: z17.string().min(1).max(4096),
|
|
3778
|
+
messages: z17.array(
|
|
3779
|
+
z17.object({
|
|
3780
|
+
id: z17.string().min(1).max(1024),
|
|
3781
|
+
role: z17.enum(["user", "assistant"]),
|
|
3782
|
+
text: z17.string().min(1).max(2 * 1024 * 1024),
|
|
3783
|
+
source: z17.string().min(1).max(1024)
|
|
3620
3784
|
}).strict()
|
|
3621
3785
|
).max(5e4)
|
|
3622
3786
|
}).strict().refine(
|
|
3623
3787
|
(s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
|
|
3624
3788
|
"Duplicate message IDs."
|
|
3625
3789
|
);
|
|
3626
|
-
var SessionCommitSchema =
|
|
3627
|
-
operationId:
|
|
3790
|
+
var SessionCommitSchema = z17.object({
|
|
3791
|
+
operationId: z17.uuid(),
|
|
3628
3792
|
sessionKey: HashSchema,
|
|
3629
3793
|
parentRevision: RevisionSchema,
|
|
3630
3794
|
blob: BlobRefSchema
|
|
3631
3795
|
}).strict();
|
|
3632
|
-
var SessionInfoSchema =
|
|
3796
|
+
var SessionInfoSchema = z17.object({
|
|
3633
3797
|
owner: NameSchema,
|
|
3634
3798
|
sessionKey: HashSchema,
|
|
3635
3799
|
agent: AgentSchema,
|
|
3636
|
-
nativeId:
|
|
3637
|
-
cwd:
|
|
3638
|
-
title:
|
|
3800
|
+
nativeId: z17.string(),
|
|
3801
|
+
cwd: z17.string(),
|
|
3802
|
+
title: z17.string(),
|
|
3639
3803
|
revision: RevisionSchema,
|
|
3640
|
-
updatedAt:
|
|
3641
|
-
messageCount:
|
|
3804
|
+
updatedAt: z17.string(),
|
|
3805
|
+
messageCount: z17.number().int().nonnegative(),
|
|
3642
3806
|
blob: BlobRefSchema
|
|
3643
3807
|
});
|
|
3644
|
-
var SessionListSchema =
|
|
3645
|
-
sessions:
|
|
3808
|
+
var SessionListSchema = z17.object({
|
|
3809
|
+
sessions: z17.array(SessionInfoSchema),
|
|
3646
3810
|
nextCursor: RevisionSchema.nullable()
|
|
3647
3811
|
});
|
|
3648
|
-
var SessionStatsSchema =
|
|
3649
|
-
totalSessions:
|
|
3650
|
-
totalMessages:
|
|
3651
|
-
memberCount:
|
|
3652
|
-
updatedAt:
|
|
3812
|
+
var SessionStatsSchema = z17.object({
|
|
3813
|
+
totalSessions: z17.number().int().nonnegative(),
|
|
3814
|
+
totalMessages: z17.number().int().nonnegative(),
|
|
3815
|
+
memberCount: z17.number().int().nonnegative(),
|
|
3816
|
+
updatedAt: z17.string().nullable()
|
|
3653
3817
|
});
|
|
3654
|
-
var SessionScopeSchema =
|
|
3818
|
+
var SessionScopeSchema = z17.object({
|
|
3655
3819
|
owner: NameSchema.optional(),
|
|
3656
3820
|
sessionKey: HashSchema.optional()
|
|
3657
3821
|
}).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
|
|
3658
|
-
var SessionSearchSchema =
|
|
3659
|
-
matches:
|
|
3660
|
-
|
|
3822
|
+
var SessionSearchSchema = z17.object({
|
|
3823
|
+
matches: z17.array(
|
|
3824
|
+
z17.object({
|
|
3661
3825
|
owner: NameSchema,
|
|
3662
3826
|
sessionKey: HashSchema,
|
|
3663
3827
|
revision: RevisionSchema,
|
|
3664
|
-
messageId:
|
|
3665
|
-
role:
|
|
3666
|
-
snippet:
|
|
3667
|
-
title:
|
|
3828
|
+
messageId: z17.string(),
|
|
3829
|
+
role: z17.enum(["user", "assistant"]),
|
|
3830
|
+
snippet: z17.string(),
|
|
3831
|
+
title: z17.string()
|
|
3668
3832
|
})
|
|
3669
3833
|
),
|
|
3670
3834
|
nextCursor: RevisionSchema.nullable()
|
|
@@ -3881,9 +4045,9 @@ function registerSessionRoutes(server, store) {
|
|
|
3881
4045
|
authorize(request);
|
|
3882
4046
|
}
|
|
3883
4047
|
};
|
|
3884
|
-
const pagination =
|
|
3885
|
-
after:
|
|
3886
|
-
limit:
|
|
4048
|
+
const pagination = z18.object({
|
|
4049
|
+
after: z18.coerce.number().pipe(RevisionSchema).default(0),
|
|
4050
|
+
limit: z18.coerce.number().int().min(1).max(100).default(50)
|
|
3887
4051
|
});
|
|
3888
4052
|
server.post(
|
|
3889
4053
|
"/sessions/commits",
|
|
@@ -3903,7 +4067,7 @@ function registerSessionRoutes(server, store) {
|
|
|
3903
4067
|
const q = pagination.extend({
|
|
3904
4068
|
owner: NameSchema.optional(),
|
|
3905
4069
|
sessionKey: HashSchema.optional(),
|
|
3906
|
-
q:
|
|
4070
|
+
q: z18.string().min(1).max(500).refine((s) => !s.includes("\0"))
|
|
3907
4071
|
}).parse(request.query);
|
|
3908
4072
|
return sessions.search(
|
|
3909
4073
|
authorizeRead(store, request),
|
|
@@ -3914,16 +4078,25 @@ function registerSessionRoutes(server, store) {
|
|
|
3914
4078
|
);
|
|
3915
4079
|
});
|
|
3916
4080
|
server.get("/sessions/:owner/:key", async (request) => {
|
|
3917
|
-
const p =
|
|
3918
|
-
const q =
|
|
4081
|
+
const p = z18.object({ owner: NameSchema, key: HashSchema }).parse(request.params);
|
|
4082
|
+
const q = z18.object({ revision: z18.coerce.number().pipe(RevisionSchema).optional() }).parse(request.query);
|
|
3919
4083
|
return sessions.read(authorizeRead(store, request), p.owner, p.key, q.revision);
|
|
3920
4084
|
});
|
|
3921
4085
|
}
|
|
3922
4086
|
|
|
3923
4087
|
// apps/server/file-routes.ts
|
|
3924
|
-
import { z as
|
|
4088
|
+
import { z as z19 } from "zod";
|
|
3925
4089
|
function registerFileRoutes(server, store) {
|
|
3926
4090
|
const files = new FileRepository(store);
|
|
4091
|
+
const shared = new SharedFilesRepository(store);
|
|
4092
|
+
server.get(
|
|
4093
|
+
"/project/shared-files",
|
|
4094
|
+
async (request) => shared.config(authorizeRead(store, request))
|
|
4095
|
+
);
|
|
4096
|
+
server.patch("/project/shared-files", async (request) => {
|
|
4097
|
+
webMutation(request);
|
|
4098
|
+
return shared.update(authorizeWeb(store, request), SharedFilesUpdateSchema.parse(request.body));
|
|
4099
|
+
});
|
|
3927
4100
|
const device = (request) => {
|
|
3928
4101
|
const value = request.headers["x-teammy-device"];
|
|
3929
4102
|
return typeof value === "string" ? value : void 0;
|
|
@@ -3943,13 +4116,13 @@ function registerFileRoutes(server, store) {
|
|
|
3943
4116
|
missing: files.missing(authorize(request), BlobCheckSchema.parse(request.body).hashes)
|
|
3944
4117
|
}));
|
|
3945
4118
|
server.put("/blobs/:hash", { ...guard, bodyLimit: MAX_BLOB_BYTES }, async (request) => {
|
|
3946
|
-
const { hash: hash2 } =
|
|
4119
|
+
const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
|
|
3947
4120
|
if (!Buffer.isBuffer(request.body))
|
|
3948
4121
|
throw Object.assign(new Error("Expected binary content"), { statusCode: 400 });
|
|
3949
4122
|
return files.upload(request.headers.authorization, device(request), hash2, request.body);
|
|
3950
4123
|
});
|
|
3951
4124
|
server.get("/blobs/:hash", async (request, reply) => {
|
|
3952
|
-
const { hash: hash2 } =
|
|
4125
|
+
const { hash: hash2 } = z19.object({ hash: HashSchema }).parse(request.params);
|
|
3953
4126
|
return reply.type("application/octet-stream").send(Buffer.from(await files.download(authorizeRead(store, request), hash2)));
|
|
3954
4127
|
});
|
|
3955
4128
|
server.post(
|
|
@@ -3961,12 +4134,16 @@ function registerFileRoutes(server, store) {
|
|
|
3961
4134
|
FileCommitSchema.parse(request.body)
|
|
3962
4135
|
)
|
|
3963
4136
|
);
|
|
4137
|
+
server.post("/files/_project/commits", guard, async (request) => {
|
|
4138
|
+
const { configRevision, ...batch } = SharedFileCommitSchema.parse(request.body);
|
|
4139
|
+
return files.commit(request.headers.authorization, device(request), batch, configRevision);
|
|
4140
|
+
});
|
|
3964
4141
|
server.get("/files/:owner/manifest", async (request) => {
|
|
3965
|
-
const { owner } =
|
|
3966
|
-
const query =
|
|
3967
|
-
revision:
|
|
3968
|
-
after:
|
|
3969
|
-
limit:
|
|
4142
|
+
const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4143
|
+
const query = z19.object({
|
|
4144
|
+
revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4145
|
+
after: z19.string().max(1024).default(""),
|
|
4146
|
+
limit: z19.coerce.number().int().min(1).max(500).default(200)
|
|
3970
4147
|
}).parse(request.query);
|
|
3971
4148
|
return files.manifest(
|
|
3972
4149
|
authorizeRead(store, request),
|
|
@@ -3977,28 +4154,28 @@ function registerFileRoutes(server, store) {
|
|
|
3977
4154
|
);
|
|
3978
4155
|
});
|
|
3979
4156
|
server.get("/files/:owner/entry", async (request) => {
|
|
3980
|
-
const { owner } =
|
|
3981
|
-
const q =
|
|
4157
|
+
const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4158
|
+
const q = z19.object({
|
|
3982
4159
|
path: FilePathSchema,
|
|
3983
|
-
revision:
|
|
4160
|
+
revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional()
|
|
3984
4161
|
}).parse(request.query);
|
|
3985
4162
|
return files.entry(authorizeRead(store, request), owner, q.path, q.revision);
|
|
3986
4163
|
});
|
|
3987
4164
|
server.get("/files/:owner/tree", async (request) => {
|
|
3988
|
-
const { owner } =
|
|
3989
|
-
const q =
|
|
3990
|
-
path:
|
|
3991
|
-
revision:
|
|
3992
|
-
after:
|
|
3993
|
-
limit:
|
|
4165
|
+
const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4166
|
+
const q = z19.object({
|
|
4167
|
+
path: z19.union([z19.literal(""), FilePathSchema]).default(""),
|
|
4168
|
+
revision: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
4169
|
+
after: z19.string().max(255).default(""),
|
|
4170
|
+
limit: z19.coerce.number().int().min(1).max(200).default(100)
|
|
3994
4171
|
}).parse(request.query);
|
|
3995
4172
|
return files.tree(authorizeRead(store, request), owner, q.path, q.revision, q.after, q.limit);
|
|
3996
4173
|
});
|
|
3997
4174
|
server.get("/files/:owner/changes", async (request) => {
|
|
3998
|
-
const { owner } =
|
|
3999
|
-
const query =
|
|
4000
|
-
after:
|
|
4001
|
-
limit:
|
|
4175
|
+
const { owner } = z19.object({ owner: FileOwnerSchema }).parse(request.params);
|
|
4176
|
+
const query = z19.object({
|
|
4177
|
+
after: z19.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
4178
|
+
limit: z19.coerce.number().int().min(1).max(100).default(100)
|
|
4002
4179
|
}).parse(request.query);
|
|
4003
4180
|
return files.changes(authorizeRead(store, request), owner, query.after, query.limit);
|
|
4004
4181
|
});
|
|
@@ -4050,6 +4227,7 @@ function buildServer(options) {
|
|
|
4050
4227
|
"identity",
|
|
4051
4228
|
"blobs",
|
|
4052
4229
|
"file-revisions",
|
|
4230
|
+
"project-files",
|
|
4053
4231
|
"session-revisions",
|
|
4054
4232
|
"session-search",
|
|
4055
4233
|
"project-config",
|