@workflow-code/cli 0.2.4-20261011005.codex-cli-web → 0.2.4-20261011007.codex-cli-web

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.
@@ -0,0 +1,613 @@
1
+ // src/web/kanban-data.ts
2
+ import { readFile, readdir, realpath } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import path from "node:path";
5
+
6
+ // .shared/project-info/index.ts
7
+ var WORKFLOW_PROJECT_DATA_STORAGE_MODES = ["local", "server", "both"];
8
+ var WORKFLOW_RELATED_PROJECT_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
9
+ var WORKFLOW_RELATED_PROJECT_LIMIT = 32;
10
+ var WORKFLOW_RELATED_PROJECT_PREFIX_LIMIT = 32;
11
+ var WORKFLOW_RELATED_PROJECT_KEY_LIMIT = 1024;
12
+ var WORKFLOW_PROJECT_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13
+ var PROJECT_INFO_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
14
+ function createUndeclaredWorkflowProjectInfo() {
15
+ return {
16
+ localizedNames: {},
17
+ declarations: [],
18
+ platformSupport: { mode: "agnostic" },
19
+ dataStorage: null,
20
+ relatedProjects: []
21
+ };
22
+ }
23
+ function normalizeWorkflowProjectInfo(value) {
24
+ if (!isRecord(value)) {
25
+ throw new Error("workflowCode.projectInfo must be an object.");
26
+ }
27
+ const localizedNames = normalizeLocalizedNames(value["localizedNames"]);
28
+ const declarations = normalizeIds(value["declarations"], "workflowCode.projectInfo.declarations");
29
+ const platformSupport = normalizePlatformSupport(value["platformSupport"]);
30
+ const dataStorage = normalizeDataStorage(value["dataStorage"]);
31
+ const relatedProjects = normalizeRelatedProjects(value["relatedProjects"]);
32
+ return { localizedNames, declarations, platformSupport, dataStorage, relatedProjects };
33
+ }
34
+ function readWorkflowProjectInfo(packageJson) {
35
+ if (!isRecord(packageJson)) return createUndeclaredWorkflowProjectInfo();
36
+ const workflowCode = packageJson["workflowCode"];
37
+ if (!isRecord(workflowCode) || workflowCode["projectInfo"] === void 0) {
38
+ return createUndeclaredWorkflowProjectInfo();
39
+ }
40
+ const projectInfo = normalizeWorkflowProjectInfo(workflowCode["projectInfo"]);
41
+ const projectId = typeof packageJson["id"] === "string" ? packageJson["id"].trim() : void 0;
42
+ if (projectId !== void 0 && projectId !== "") {
43
+ assertWorkflowRelatedProjects(projectInfo, projectId);
44
+ }
45
+ return projectInfo;
46
+ }
47
+ function assertWorkflowRelatedProjects(projectInfo, currentProjectId) {
48
+ if (projectInfo.relatedProjects.length === 0) return;
49
+ const normalizedProjectId = currentProjectId.trim();
50
+ if (!WORKFLOW_PROJECT_UUID_PATTERN.test(normalizedProjectId)) {
51
+ throw new Error("package.json.id must be a UUID before related projects can be declared.");
52
+ }
53
+ if (projectInfo.relatedProjects.some(
54
+ (relation) => relation.projectId.toLowerCase() === normalizedProjectId.toLowerCase()
55
+ )) {
56
+ throw new Error("workflowCode.projectInfo.relatedProjects cannot reference the current project.");
57
+ }
58
+ }
59
+ function findWorkflowRelatedProject(projectInfo, alias) {
60
+ const normalizedAlias = alias.trim();
61
+ return projectInfo.relatedProjects.find((relation) => relation.alias === normalizedAlias);
62
+ }
63
+ function resolveWorkflowRelatedProjectAccess(input) {
64
+ const consumer = findWorkflowRelatedProject(input.consumerProjectInfo, input.alias);
65
+ if (consumer?.projectId.toLowerCase() !== input.targetProjectId.trim().toLowerCase()) {
66
+ return void 0;
67
+ }
68
+ const target = input.targetProjectInfo.relatedProjects.find(
69
+ (relation) => relation.projectId.toLowerCase() === input.consumerProjectId.trim().toLowerCase()
70
+ );
71
+ if (target === void 0) return void 0;
72
+ return { consumer, target, grant: target.grantToRelatedProject };
73
+ }
74
+ function isWorkflowProjectAccessAllowed(rule, value) {
75
+ if (rule === void 0 || rule.mode === "none") return false;
76
+ if (rule.mode === "all") return true;
77
+ return rule.prefixes.some((prefix) => value.startsWith(prefix));
78
+ }
79
+ function isWorkflowProjectDataStorageLocationAllowed(projectInfo, location) {
80
+ const mode = projectInfo.dataStorage?.mode;
81
+ return mode === "both" || mode === location;
82
+ }
83
+ function normalizeLocalizedNames(value) {
84
+ if (value === void 0) return {};
85
+ if (!isRecord(value)) {
86
+ throw new Error("workflowCode.projectInfo.localizedNames must be an object.");
87
+ }
88
+ const entries = [];
89
+ for (const [rawLocale, rawName] of Object.entries(value)) {
90
+ const locale = rawLocale.trim();
91
+ if (locale === "") {
92
+ throw new Error("workflowCode.projectInfo.localizedNames contains an empty locale.");
93
+ }
94
+ if (typeof rawName !== "string") {
95
+ throw new Error(`workflowCode.projectInfo.localizedNames.${locale} must be a string.`);
96
+ }
97
+ const name = rawName.trim();
98
+ if (name !== "") entries.push([locale, name]);
99
+ }
100
+ return Object.fromEntries(entries);
101
+ }
102
+ function normalizePlatformSupport(value) {
103
+ if (value === void 0) return { mode: "agnostic" };
104
+ if (!isRecord(value)) {
105
+ throw new Error("workflowCode.projectInfo.platformSupport must be an object.");
106
+ }
107
+ if (value["mode"] === "agnostic") {
108
+ if (value["platforms"] !== void 0) {
109
+ throw new Error("workflowCode.projectInfo.platformSupport.platforms is not allowed in agnostic mode.");
110
+ }
111
+ return { mode: "agnostic" };
112
+ }
113
+ if (value["mode"] !== "specific") {
114
+ throw new Error('workflowCode.projectInfo.platformSupport.mode must be "agnostic" or "specific".');
115
+ }
116
+ const platforms = normalizeIds(
117
+ value["platforms"],
118
+ "workflowCode.projectInfo.platformSupport.platforms"
119
+ );
120
+ if (platforms.length === 0) {
121
+ throw new Error("workflowCode.projectInfo.platformSupport.platforms must contain at least one platform.");
122
+ }
123
+ return { mode: "specific", platforms };
124
+ }
125
+ function normalizeDataStorage(value) {
126
+ if (value === void 0 || value === null) return null;
127
+ if (!isRecord(value)) {
128
+ throw new Error("workflowCode.projectInfo.dataStorage must be an object.");
129
+ }
130
+ const mode = value["mode"];
131
+ if (typeof mode !== "string" || !WORKFLOW_PROJECT_DATA_STORAGE_MODES.includes(
132
+ mode
133
+ )) {
134
+ throw new Error(
135
+ 'workflowCode.projectInfo.dataStorage.mode must be "local", "server", or "both".'
136
+ );
137
+ }
138
+ return { mode };
139
+ }
140
+ function normalizeRelatedProjects(value) {
141
+ if (value === void 0) return [];
142
+ if (!Array.isArray(value)) {
143
+ throw new Error("workflowCode.projectInfo.relatedProjects must be an array.");
144
+ }
145
+ if (value.length > WORKFLOW_RELATED_PROJECT_LIMIT) {
146
+ throw new Error(
147
+ `workflowCode.projectInfo.relatedProjects supports at most ${WORKFLOW_RELATED_PROJECT_LIMIT} items.`
148
+ );
149
+ }
150
+ const aliases = /* @__PURE__ */ new Set();
151
+ const projectIds = /* @__PURE__ */ new Set();
152
+ return value.map((item, index) => {
153
+ const field = `workflowCode.projectInfo.relatedProjects[${index}]`;
154
+ if (!isRecord(item)) throw new Error(`${field} must be an object.`);
155
+ const supportedFields = /* @__PURE__ */ new Set(["alias", "projectId", "grantToRelatedProject"]);
156
+ if (Object.keys(item).some((key) => !supportedFields.has(key))) {
157
+ throw new Error(`${field} contains unsupported fields.`);
158
+ }
159
+ const alias = readRelatedProjectString(item["alias"], `${field}.alias`);
160
+ if (!WORKFLOW_RELATED_PROJECT_ALIAS_PATTERN.test(alias)) {
161
+ throw new Error(`${field}.alias is invalid.`);
162
+ }
163
+ if (aliases.has(alias)) {
164
+ throw new Error(`workflowCode.projectInfo.relatedProjects contains duplicate alias "${alias}".`);
165
+ }
166
+ aliases.add(alias);
167
+ const projectId = readRelatedProjectString(item["projectId"], `${field}.projectId`);
168
+ if (!WORKFLOW_PROJECT_UUID_PATTERN.test(projectId)) {
169
+ throw new Error(`${field}.projectId must be a UUID.`);
170
+ }
171
+ const normalizedProjectId = projectId.toLowerCase();
172
+ if (projectIds.has(normalizedProjectId)) {
173
+ throw new Error(`workflowCode.projectInfo.relatedProjects contains duplicate projectId "${projectId}".`);
174
+ }
175
+ projectIds.add(normalizedProjectId);
176
+ return {
177
+ alias,
178
+ projectId,
179
+ grantToRelatedProject: normalizeRelatedProjectGrant(
180
+ item["grantToRelatedProject"],
181
+ `${field}.grantToRelatedProject`
182
+ )
183
+ };
184
+ });
185
+ }
186
+ function normalizeRelatedProjectGrant(value, field) {
187
+ if (!isRecord(value)) throw new Error(`${field} must be an object.`);
188
+ const supported = /* @__PURE__ */ new Set(["read", "write", "knowledge", "entrypoints"]);
189
+ if (Object.keys(value).some((key) => !supported.has(key))) {
190
+ throw new Error(`${field} contains unsupported fields.`);
191
+ }
192
+ const grant = {
193
+ read: normalizeProjectAccessRule(value["read"], `${field}.read`),
194
+ write: normalizeProjectAccessRule(value["write"], `${field}.write`)
195
+ };
196
+ const knowledge = value["knowledge"];
197
+ if (knowledge !== void 0) {
198
+ if (!isRecord(knowledge)) throw new Error(`${field}.knowledge must be an object.`);
199
+ if (Object.keys(knowledge).some((key) => key !== "read" && key !== "write")) {
200
+ throw new Error(`${field}.knowledge contains unsupported fields.`);
201
+ }
202
+ grant.knowledge = {
203
+ read: normalizeProjectAccessRule(knowledge["read"], `${field}.knowledge.read`),
204
+ write: normalizeProjectAccessRule(knowledge["write"], `${field}.knowledge.write`)
205
+ };
206
+ }
207
+ const entrypoints = value["entrypoints"];
208
+ if (entrypoints !== void 0) {
209
+ grant.entrypoints = normalizeProjectAccessRule(entrypoints, `${field}.entrypoints`);
210
+ }
211
+ return grant;
212
+ }
213
+ function normalizeProjectAccessRule(value, field) {
214
+ if (!isRecord(value)) throw new Error(`${field} must be an object.`);
215
+ const mode = value["mode"];
216
+ if (mode === "none" || mode === "all") {
217
+ if (Object.keys(value).some((key) => key !== "mode")) {
218
+ throw new Error(`${field} contains unsupported fields.`);
219
+ }
220
+ return { mode };
221
+ }
222
+ if (mode !== "prefixes") {
223
+ throw new Error(`${field}.mode must be "none", "all", or "prefixes".`);
224
+ }
225
+ if (Object.keys(value).some((key) => key !== "mode" && key !== "prefixes")) {
226
+ throw new Error(`${field} contains unsupported fields.`);
227
+ }
228
+ const rawPrefixes = value["prefixes"];
229
+ if (!Array.isArray(rawPrefixes) || rawPrefixes.length === 0 || rawPrefixes.length > WORKFLOW_RELATED_PROJECT_PREFIX_LIMIT) {
230
+ throw new Error(
231
+ `${field}.prefixes must contain 1 to ${WORKFLOW_RELATED_PROJECT_PREFIX_LIMIT} items.`
232
+ );
233
+ }
234
+ const prefixes = rawPrefixes.map((prefix, index) => {
235
+ const normalized = readRelatedProjectString(prefix, `${field}.prefixes[${index}]`);
236
+ if (normalized.length > WORKFLOW_RELATED_PROJECT_KEY_LIMIT || normalized.includes("\0")) {
237
+ throw new Error(`${field}.prefixes[${index}] is invalid.`);
238
+ }
239
+ return normalized;
240
+ });
241
+ if (new Set(prefixes).size !== prefixes.length) {
242
+ throw new Error(`${field}.prefixes contains duplicates.`);
243
+ }
244
+ return { mode: "prefixes", prefixes };
245
+ }
246
+ function readRelatedProjectString(value, field) {
247
+ if (typeof value !== "string" || value.trim() === "") {
248
+ throw new Error(`${field} must be a non-empty string.`);
249
+ }
250
+ return value.trim();
251
+ }
252
+ function normalizeIds(value, field) {
253
+ if (value === void 0) return [];
254
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array.`);
255
+ const values = value.map((item, index) => {
256
+ if (typeof item !== "string" || !PROJECT_INFO_ID_PATTERN.test(item.trim())) {
257
+ throw new Error(`${field}[${index}] must be a lowercase identifier.`);
258
+ }
259
+ return item.trim();
260
+ });
261
+ return uniqueStrings(values, true);
262
+ }
263
+ function uniqueStrings(values, caseSensitive) {
264
+ const seen = /* @__PURE__ */ new Set();
265
+ const result = [];
266
+ for (const rawValue of values) {
267
+ const value = rawValue.trim();
268
+ if (value === "") continue;
269
+ const key = caseSensitive ? value : value.toLocaleLowerCase();
270
+ if (seen.has(key)) continue;
271
+ seen.add(key);
272
+ result.push(value);
273
+ }
274
+ return result;
275
+ }
276
+ function isRecord(value) {
277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
278
+ }
279
+
280
+ // .shared/kanban-project/index.ts
281
+ var KANBAN_FILE_MAX_BYTES = 50 * 1024 * 1024;
282
+ var KANBAN_BRIDGE_SAFE_ERRORS = {
283
+ KANBAN_BRIDGE_INVALID_REQUEST: "The Kanban bridge request is invalid.",
284
+ KANBAN_BRIDGE_BUSY: "Too many Kanban bridge requests are pending.",
285
+ KANBAN_BRIDGE_TIMEOUT: "Kanban bridge request timed out.",
286
+ KANBAN_BRIDGE_UNAVAILABLE: "Kanban project data is unavailable in this host.",
287
+ KANBAN_BRIDGE_SESSION_INVALID: "The Kanban preview session is invalid or expired.",
288
+ KANBAN_BRIDGE_SERVER_ERROR: "Kanban project data operation failed.",
289
+ KANBAN_DATA_OPERATION_FAILED: "Kanban project data operation failed.",
290
+ KANBAN_RELATED_PROJECT_UNAVAILABLE: "Kanban related project is unavailable.",
291
+ KANBAN_FILE_UNAVAILABLE: "Kanban file is unavailable in this project.",
292
+ KANBAN_FILE_TOO_LARGE: "Kanban files cannot exceed 50 MiB."
293
+ };
294
+ var KANBAN_BRIDGE_CALL_FIELDS = /* @__PURE__ */ new Set(["requestId", "operation", "input"]);
295
+ var MAX_KNOWLEDGE_DOCUMENT_PAGE_SIZE = 100;
296
+ var MAX_KNOWLEDGE_SEARCH_PAGE_SIZE = 1e3;
297
+ var MAX_KNOWLEDGE_CONTEXT_LINES = 50;
298
+ var MAX_KNOWLEDGE_LINE_RANGE = 1e3;
299
+ var KANBAN_BRIDGE_INPUT_FIELDS = {
300
+ "runtime.getLocale": [],
301
+ "files.createFile": ["name", "mimeType", "base64"],
302
+ "files.readFile": ["fileId"],
303
+ "files.deleteFile": ["fileId"],
304
+ "kv.getValue": ["key"],
305
+ "kv.setValue": ["key", "value"],
306
+ "relatedProject.kv.getValue": ["alias", "key"],
307
+ "relatedProject.kv.setValue": ["alias", "key", "value"],
308
+ "knowledge.listDocuments": ["pageSize", "cursor"],
309
+ "knowledge.getDocument": ["documentId"],
310
+ "knowledge.createDocument": ["id", "title", "markdown"],
311
+ "knowledge.editDocument": ["documentId", "title", "markdown"],
312
+ "knowledge.deleteDocument": ["documentId"],
313
+ "knowledge.searchDocuments": [
314
+ "query",
315
+ "documentId",
316
+ "beforeLines",
317
+ "afterLines",
318
+ "maxMatches",
319
+ "pageSize",
320
+ "cursor"
321
+ ],
322
+ "knowledge.readDocumentLines": ["documentId", "startLine", "endLine"]
323
+ };
324
+ function parseKanbanBridgeCall(value) {
325
+ if (!isRecord2(value)) throw new Error("Kanban bridge request must be an object.");
326
+ if (Object.keys(value).some((key) => !KANBAN_BRIDGE_CALL_FIELDS.has(key))) {
327
+ throw new Error("Kanban bridge request contains unsupported fields.");
328
+ }
329
+ const requestId = readNonEmptyString(value["requestId"], "bridge.requestId");
330
+ const operation = value["operation"];
331
+ if (!isKanbanBridgeOperation(operation)) {
332
+ throw new Error(`Unsupported Kanban bridge operation "${String(operation)}".`);
333
+ }
334
+ const input = value["input"];
335
+ if (!isRecord2(input)) throw new Error("Kanban bridge input must be an object.");
336
+ validateKanbanBridgeInput(operation, input);
337
+ return { requestId, operation, input };
338
+ }
339
+ function resolveKanbanRelatedProject(relatedProjects, alias, key) {
340
+ const relatedProject = relatedProjects.find((item) => item.alias === alias);
341
+ if (relatedProject === void 0 || key.trim() === "" || key.length > WORKFLOW_RELATED_PROJECT_KEY_LIMIT) {
342
+ throw Object.assign(new Error("Kanban related project is unavailable."), {
343
+ code: "KANBAN_RELATED_PROJECT_UNAVAILABLE"
344
+ });
345
+ }
346
+ return relatedProject;
347
+ }
348
+ function isKanbanJsonValue(value, seen = /* @__PURE__ */ new Set()) {
349
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
350
+ if (typeof value === "number") return Number.isFinite(value);
351
+ if (typeof value !== "object") return false;
352
+ if (seen.has(value)) return false;
353
+ seen.add(value);
354
+ const valid = Array.isArray(value) ? value.every((item) => isKanbanJsonValue(item, seen)) : Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every((item) => isKanbanJsonValue(item, seen));
355
+ seen.delete(value);
356
+ return valid;
357
+ }
358
+ function validateKanbanBridgeInput(operation, input) {
359
+ const allowedFields = new Set(KANBAN_BRIDGE_INPUT_FIELDS[operation]);
360
+ if (Object.keys(input).some((key) => !allowedFields.has(key))) {
361
+ throw new Error(`Kanban bridge ${operation} input contains unsupported fields.`);
362
+ }
363
+ switch (operation) {
364
+ case "runtime.getLocale":
365
+ return;
366
+ case "files.createFile": {
367
+ const name = readNonEmptyString(input["name"], "bridge.input.name");
368
+ if (name.length > 255 || /[\\/\\\\\0]/.test(name)) {
369
+ throw new Error("Kanban file name must be a plain file name.");
370
+ }
371
+ const mimeType = readOptionalNonEmptyString(input["mimeType"], "bridge.input.mimeType");
372
+ if (mimeType !== void 0 && !/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(mimeType)) {
373
+ throw new Error("Kanban file MIME type is invalid.");
374
+ }
375
+ const base64 = readRequiredString(input["base64"], "bridge.input.base64");
376
+ if (base64.length > Math.ceil(KANBAN_FILE_MAX_BYTES / 3) * 4) {
377
+ throw Object.assign(new Error(KANBAN_BRIDGE_SAFE_ERRORS.KANBAN_FILE_TOO_LARGE), { code: "KANBAN_FILE_TOO_LARGE" });
378
+ }
379
+ if (base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) {
380
+ throw new Error("Kanban file content must be base64 encoded.");
381
+ }
382
+ const size = base64.length / 4 * 3 - (base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0);
383
+ if (size > KANBAN_FILE_MAX_BYTES) {
384
+ throw Object.assign(new Error(KANBAN_BRIDGE_SAFE_ERRORS.KANBAN_FILE_TOO_LARGE), { code: "KANBAN_FILE_TOO_LARGE" });
385
+ }
386
+ return;
387
+ }
388
+ case "files.readFile":
389
+ case "files.deleteFile":
390
+ if (!/^wf_[a-f0-9]+$/.test(readNonEmptyString(input["fileId"], "bridge.input.fileId"))) {
391
+ throw new Error("Kanban file id is invalid.");
392
+ }
393
+ return;
394
+ case "kv.getValue":
395
+ readNonEmptyString(input["key"], "bridge.input.key");
396
+ return;
397
+ case "kv.setValue":
398
+ readNonEmptyString(input["key"], "bridge.input.key");
399
+ if (input["value"] === void 0 || !isKanbanJsonValue(input["value"])) {
400
+ throw new Error("Kanban bridge KV value must be JSON-serializable and cannot be undefined.");
401
+ }
402
+ return;
403
+ case "relatedProject.kv.getValue":
404
+ case "relatedProject.kv.setValue": {
405
+ const alias = readNonEmptyString(input["alias"], "bridge.input.alias");
406
+ if (!WORKFLOW_RELATED_PROJECT_ALIAS_PATTERN.test(alias)) {
407
+ throw new Error("Kanban bridge related-project alias is invalid.");
408
+ }
409
+ const key = readNonEmptyString(input["key"], "bridge.input.key");
410
+ if (key.length > WORKFLOW_RELATED_PROJECT_KEY_LIMIT) {
411
+ throw new Error("Kanban bridge related-project key is too long.");
412
+ }
413
+ if (operation === "relatedProject.kv.setValue" && (input["value"] === void 0 || !isKanbanJsonValue(input["value"]))) {
414
+ throw new Error(
415
+ "Kanban bridge related-project KV value must be JSON-serializable and cannot be undefined."
416
+ );
417
+ }
418
+ return;
419
+ }
420
+ case "knowledge.listDocuments":
421
+ readOptionalIntegerInRange(
422
+ input["pageSize"],
423
+ "bridge.input.pageSize",
424
+ 1,
425
+ MAX_KNOWLEDGE_DOCUMENT_PAGE_SIZE
426
+ );
427
+ readOptionalNonEmptyString(input["cursor"], "bridge.input.cursor");
428
+ return;
429
+ case "knowledge.getDocument":
430
+ case "knowledge.deleteDocument":
431
+ readNonEmptyString(input["documentId"], "bridge.input.documentId");
432
+ return;
433
+ case "knowledge.createDocument":
434
+ readOptionalNonEmptyString(input["id"], "bridge.input.id");
435
+ readNonEmptyString(input["title"], "bridge.input.title");
436
+ readRequiredString(input["markdown"], "bridge.input.markdown");
437
+ return;
438
+ case "knowledge.editDocument":
439
+ readNonEmptyString(input["documentId"], "bridge.input.documentId");
440
+ if (input["title"] === void 0 && input["markdown"] === void 0) {
441
+ throw new Error("Kanban bridge knowledge edit requires title or markdown.");
442
+ }
443
+ readOptionalStringValue(input["title"], "bridge.input.title");
444
+ readOptionalStringValue(input["markdown"], "bridge.input.markdown");
445
+ return;
446
+ case "knowledge.searchDocuments":
447
+ readNonEmptyString(input["query"], "bridge.input.query");
448
+ readOptionalNonEmptyString(input["documentId"], "bridge.input.documentId");
449
+ readOptionalIntegerInRange(
450
+ input["beforeLines"],
451
+ "bridge.input.beforeLines",
452
+ 0,
453
+ MAX_KNOWLEDGE_CONTEXT_LINES
454
+ );
455
+ readOptionalIntegerInRange(
456
+ input["afterLines"],
457
+ "bridge.input.afterLines",
458
+ 0,
459
+ MAX_KNOWLEDGE_CONTEXT_LINES
460
+ );
461
+ readOptionalIntegerInRange(
462
+ input["maxMatches"],
463
+ "bridge.input.maxMatches",
464
+ 1,
465
+ MAX_KNOWLEDGE_SEARCH_PAGE_SIZE
466
+ );
467
+ readOptionalIntegerInRange(
468
+ input["pageSize"],
469
+ "bridge.input.pageSize",
470
+ 1,
471
+ MAX_KNOWLEDGE_SEARCH_PAGE_SIZE
472
+ );
473
+ readOptionalNonEmptyString(input["cursor"], "bridge.input.cursor");
474
+ return;
475
+ case "knowledge.readDocumentLines":
476
+ readNonEmptyString(input["documentId"], "bridge.input.documentId");
477
+ const startLine = readPositiveInteger(input["startLine"], "bridge.input.startLine");
478
+ const endLine = readPositiveInteger(input["endLine"], "bridge.input.endLine");
479
+ if (endLine < startLine || endLine - startLine + 1 > MAX_KNOWLEDGE_LINE_RANGE) {
480
+ throw new Error("Kanban bridge knowledge line range is invalid or too large.");
481
+ }
482
+ }
483
+ }
484
+ function isKanbanBridgeOperation(value) {
485
+ return value === "runtime.getLocale" || value === "files.createFile" || value === "files.readFile" || value === "files.deleteFile" || value === "kv.getValue" || value === "kv.setValue" || value === "relatedProject.kv.getValue" || value === "relatedProject.kv.setValue" || value === "knowledge.listDocuments" || value === "knowledge.getDocument" || value === "knowledge.createDocument" || value === "knowledge.editDocument" || value === "knowledge.deleteDocument" || value === "knowledge.searchDocuments" || value === "knowledge.readDocumentLines";
486
+ }
487
+ function readRequiredString(value, field) {
488
+ if (typeof value !== "string") throw new Error(`Kanban ${field} must be a string.`);
489
+ return value;
490
+ }
491
+ function readOptionalStringValue(value, field) {
492
+ if (value === void 0) return void 0;
493
+ return readRequiredString(value, field);
494
+ }
495
+ function readOptionalNonEmptyString(value, field) {
496
+ if (value === void 0) return void 0;
497
+ return readNonEmptyString(value, field);
498
+ }
499
+ function readOptionalIntegerInRange(value, field, minimum, maximum) {
500
+ if (value === void 0) return void 0;
501
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
502
+ throw new Error(`Kanban ${field} must be an integer from ${minimum} to ${maximum}.`);
503
+ }
504
+ return value;
505
+ }
506
+ function readPositiveInteger(value, field) {
507
+ if (!Number.isInteger(value) || value <= 0) {
508
+ throw new Error(`Kanban ${field} must be a positive integer.`);
509
+ }
510
+ return value;
511
+ }
512
+ function readNonEmptyString(value, field) {
513
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`kanban.json ${field} must be a non-empty string.`);
514
+ return value.trim();
515
+ }
516
+ function isRecord2(value) {
517
+ return typeof value === "object" && value !== null && !Array.isArray(value);
518
+ }
519
+
520
+ // src/web/kanban-data.ts
521
+ var readKanbanProjectInfo = readWorkflowProjectInfo;
522
+ var webProjectDataDirectory = (storageDir, projectDir) => path.join(storageDir, createHash("sha256").update(projectDir).digest("hex").slice(0, 24));
523
+ async function localKanbanRelatedProjects(project) {
524
+ const info = readWorkflowProjectInfo(JSON.parse(await readFile(path.join(project.folderPath, "package.json"), "utf8")));
525
+ const relations = info.relatedProjects;
526
+ const projects = /* @__PURE__ */ new Map();
527
+ if (!relations.length) {
528
+ return { info, projects };
529
+ }
530
+ const parent = path.dirname(project.folderPath);
531
+ const directories = (await readdir(parent, { withFileTypes: true })).filter((item) => item.isDirectory() && !item.name.startsWith(".") && item.name !== "node_modules").slice(0, 500);
532
+ await Promise.all(directories.map(async (item) => {
533
+ try {
534
+ const folderPath = await realpath(path.join(parent, item.name));
535
+ const pkg = JSON.parse(await readFile(path.join(folderPath, "package.json"), "utf8"));
536
+ const relation = relations.find((relation2) => relation2.projectId.toLowerCase() === String(pkg.id).toLowerCase());
537
+ if (!relation) {
538
+ return;
539
+ }
540
+ const projectInfo = readWorkflowProjectInfo(pkg);
541
+ const access = resolveWorkflowRelatedProjectAccess({ consumerProjectId: project.id, consumerProjectInfo: info, targetProjectId: pkg.id, targetProjectInfo: projectInfo, alias: relation.alias });
542
+ if (access && isWorkflowProjectDataStorageLocationAllowed(projectInfo, "local")) {
543
+ if (projects.has(relation.alias)) {
544
+ throw new Error(`Duplicate related project UUID: ${pkg.id}`);
545
+ }
546
+ projects.set(relation.alias, { folderPath, id: pkg.id, projectInfo });
547
+ }
548
+ } catch (error) {
549
+ if (String(error).includes("Duplicate related project UUID")) {
550
+ throw error;
551
+ }
552
+ }
553
+ }));
554
+ return { info, projects };
555
+ }
556
+ async function runKanbanDataCall(core, project, storageDir, value) {
557
+ const call = parseKanbanBridgeCall(value), { operation, input } = call;
558
+ const { info, projects } = await localKanbanRelatedProjects(project);
559
+ if (info.dataStorage?.mode === "server" || !isWorkflowProjectDataStorageLocationAllowed(info, "local")) {
560
+ throw new Error("Kanban data must use its declared storage location.");
561
+ }
562
+ const directory = webProjectDataDirectory(storageDir, project.folderPath);
563
+ const kv = core.createWorkflowKVContext({ store: core.createLocalWorkflowKVStore({ rootDir: path.join(directory, "kv"), storageKind: "kv" }), projectId: project.id });
564
+ const knowledge = { persistentValue: core.createWorkflowPersistentValueContext({ store: core.createLocalWorkflowKVStore({ rootDir: path.join(directory, "values"), storageKind: "persistent_value" }), projectId: project.id }) };
565
+ switch (operation) {
566
+ case "kv.getValue":
567
+ return await kv.getValue(String(input.key)) ?? null;
568
+ case "kv.setValue":
569
+ await kv.setValue(String(input.key), input.value);
570
+ return null;
571
+ case "relatedProject.kv.getValue":
572
+ case "relatedProject.kv.setValue": {
573
+ const alias = String(input.alias), key = String(input.key);
574
+ resolveKanbanRelatedProject(info.relatedProjects, alias, key);
575
+ const target = projects.get(alias);
576
+ if (!target) {
577
+ throw Object.assign(new Error("Related project is unavailable."), { code: "KANBAN_RELATED_PROJECT_UNAVAILABLE" });
578
+ }
579
+ const access = resolveWorkflowRelatedProjectAccess({ consumerProjectId: project.id, consumerProjectInfo: info, targetProjectId: target.id, targetProjectInfo: target.projectInfo, alias });
580
+ if (!access || !isWorkflowProjectAccessAllowed(operation === "relatedProject.kv.getValue" ? access.grant.read : access.grant.write, key)) {
581
+ throw Object.assign(new Error("Related project did not grant this key."), { code: "KANBAN_RELATED_PROJECT_UNAVAILABLE" });
582
+ }
583
+ const related = core.createWorkflowKVContext({ projectId: target.id, store: core.createLocalWorkflowKVStore({ rootDir: path.join(webProjectDataDirectory(storageDir, target.folderPath), "kv"), storageKind: "kv" }) });
584
+ if (operation === "relatedProject.kv.getValue") {
585
+ return await related.getValue(key) ?? null;
586
+ }
587
+ await related.setValue(key, input.value);
588
+ return null;
589
+ }
590
+ case "knowledge.listDocuments":
591
+ return core.listKnowledgeDocumentPage(knowledge, input);
592
+ case "knowledge.getDocument":
593
+ return await core.getKnowledgeDocument(knowledge, String(input.documentId)) ?? null;
594
+ case "knowledge.createDocument":
595
+ return core.createKnowledgeDocument(knowledge, input);
596
+ case "knowledge.editDocument":
597
+ return core.editKnowledgeDocument(knowledge, { id: String(input.documentId), title: input.title, markdown: input.markdown });
598
+ case "knowledge.deleteDocument":
599
+ return core.deleteKnowledgeDocument(knowledge, String(input.documentId));
600
+ case "knowledge.searchDocuments":
601
+ return core.searchKnowledgeDocuments(knowledge, input);
602
+ case "knowledge.readDocumentLines":
603
+ return core.readKnowledgeDocumentLines(knowledge, input);
604
+ default:
605
+ throw new Error("Unsupported local Kanban operation.");
606
+ }
607
+ }
608
+ export {
609
+ localKanbanRelatedProjects,
610
+ readKanbanProjectInfo,
611
+ runKanbanDataCall,
612
+ webProjectDataDirectory
613
+ };