@fieldwangai/agentflow 0.1.133 → 0.1.135

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.
@@ -21,6 +21,10 @@ function cleanString(value, max = 4000) {
21
21
  return String(value ?? "").trim().slice(0, max);
22
22
  }
23
23
 
24
+ function hasOwn(value, key) {
25
+ return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
26
+ }
27
+
24
28
  function stableValue(value) {
25
29
  if (Array.isArray(value)) return value.map((item) => stableValue(item));
26
30
  if (value && typeof value === "object") {
@@ -105,6 +109,67 @@ function normalizeWorkflowArtifact(value, index = 0, defaultScope = "action") {
105
109
  };
106
110
  }
107
111
 
112
+ export function normalizeWorkflowTimelineProjection(value, index = 0) {
113
+ const raw = plainObject(value);
114
+ const kind = cleanString(raw.kind || raw.type, 80).toLowerCase();
115
+ const id = cleanString(raw.id || raw.key, 240);
116
+ if (!kind || !id) return null;
117
+ const source = cleanString(raw.source || raw.namespace, 120).toLowerCase();
118
+ const date = cleanString(raw.date || raw.targetDate || raw.target_date, 80);
119
+ const dimensions = mergeWorkflowGlobalState({}, plainObject(raw.dimensions || raw.facets));
120
+ const key = cleanString(raw.key || [source, kind, id].filter(Boolean).join(":"), 500);
121
+ return {
122
+ ...mergeWorkflowGlobalState({}, raw),
123
+ key: key || `${kind}:${id}`,
124
+ kind,
125
+ id,
126
+ title: cleanString(raw.title || raw.label || id, 500) || id,
127
+ ...(source ? { source } : {}),
128
+ ...(date ? { date } : {}),
129
+ dimensions,
130
+ order: Number.isFinite(Number(raw.order)) ? Number(raw.order) : index,
131
+ };
132
+ }
133
+
134
+ function normalizeWorkflowProjectionState(value = {}) {
135
+ const raw = mergeWorkflowGlobalState({}, plainObject(value));
136
+ if (Array.isArray(value?.timeline)) {
137
+ raw.timeline = value.timeline
138
+ .map((item, index) => normalizeWorkflowTimelineProjection(item, index))
139
+ .filter(Boolean)
140
+ .slice(0, 100);
141
+ } else {
142
+ delete raw.timeline;
143
+ }
144
+ return raw;
145
+ }
146
+
147
+ export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []) {
148
+ const base =
149
+ snapshot.projections ||
150
+ snapshot.workflowProjections ||
151
+ snapshot.workflow_projections ||
152
+ snapshot.raw?.projections ||
153
+ {};
154
+ let projections = normalizeWorkflowProjectionState(base);
155
+ const events = [...(Array.isArray(runtimeEvents) ? runtimeEvents : [])]
156
+ .sort((left, right) => eventTime(left) - eventTime(right));
157
+ for (const event of events) {
158
+ const next = event?.projections || event?.workflowProjections || event?.workflow_projections;
159
+ if (!next || typeof next !== "object" || Array.isArray(next) || !hasOwn(next, "timeline")) continue;
160
+ projections = {
161
+ ...projections,
162
+ timeline: Array.isArray(next.timeline)
163
+ ? next.timeline
164
+ .map((item, index) => normalizeWorkflowTimelineProjection(item, index))
165
+ .filter(Boolean)
166
+ .slice(0, 100)
167
+ : projections.timeline || [],
168
+ };
169
+ }
170
+ return projections;
171
+ }
172
+
108
173
  export function mergeWorkflowGlobalState(base, patch) {
109
174
  if (!patch || typeof patch !== "object" || Array.isArray(patch)) return plainObject(base);
110
175
  const out = { ...plainObject(base) };
@@ -206,6 +271,22 @@ export function normalizeWorkflowReport(payload = {}) {
206
271
  .filter((item) => item && typeof item === "object" && !Array.isArray(item))
207
272
  .map((item, index) => normalizeWorkflowArtifact(item, index, action ? "action" : "global"));
208
273
 
274
+ const rawProjections = plainObject(payload.projections || payload.workflowProjections || payload.workflow_projections);
275
+ const hasProjections = Object.keys(rawProjections).length > 0 || hasOwn(payload, "projections");
276
+ if (hasProjections && !hasOwn(rawProjections, "timeline")) {
277
+ return { error: "projections requires timeline" };
278
+ }
279
+ if (hasProjections && !Array.isArray(rawProjections.timeline)) {
280
+ return { error: "projections.timeline must be an array" };
281
+ }
282
+ const invalidTimelineIndex = hasProjections
283
+ ? rawProjections.timeline.findIndex((item, index) => !normalizeWorkflowTimelineProjection(item, index))
284
+ : -1;
285
+ if (invalidTimelineIndex >= 0) {
286
+ return { error: `projections.timeline[${invalidTimelineIndex}] requires kind and id` };
287
+ }
288
+ const projections = hasProjections ? normalizeWorkflowProjectionState(rawProjections) : null;
289
+
209
290
  const rawGlobalState = plainObject(payload.globalState || payload.global_state);
210
291
  const hasGlobalState = Object.keys(rawGlobalState).length > 0;
211
292
  const globalStatePatch = plainObject(rawGlobalState.patch);
@@ -215,8 +296,8 @@ export function normalizeWorkflowReport(payload = {}) {
215
296
  if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
216
297
  return { error: "globalState requires patch or remove" };
217
298
  }
218
- if (!action && !artifacts.length && !hasGlobalState) {
219
- return { error: "Workflow report requires action, artifacts, or globalState" };
299
+ if (!action && !artifacts.length && !hasGlobalState && !hasProjections) {
300
+ return { error: "Workflow report requires action, artifacts, globalState, or projections" };
220
301
  }
221
302
 
222
303
  const idempotencyKey = cleanString(
@@ -257,6 +338,7 @@ export function normalizeWorkflowReport(payload = {}) {
257
338
  scope: "global",
258
339
  }),
259
340
  artifacts,
341
+ ...(hasProjections ? { projections } : {}),
260
342
  ...(hasGlobalState ? {
261
343
  globalStatePatch,
262
344
  globalStateRemove,
@@ -268,6 +350,7 @@ export function normalizeWorkflowReport(payload = {}) {
268
350
  workflow,
269
351
  action,
270
352
  artifacts,
353
+ projections,
271
354
  globalState: hasGlobalState ? {
272
355
  mode: "merge",
273
356
  patch: globalStatePatch,
@@ -448,7 +531,7 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
448
531
  return out;
449
532
  }
450
533
 
451
- export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = []) {
534
+ export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}) {
452
535
  const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => ({
453
536
  id: event?.id || "",
454
537
  action: event?.action || event?.actionId || "",
@@ -457,10 +540,11 @@ export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtim
457
540
  artifacts: event?.artifacts || [],
458
541
  globalStatePatch: event?.globalStatePatch || {},
459
542
  globalStateRemove: event?.globalStateRemove || [],
543
+ projections: event?.projections || {},
460
544
  }));
461
545
  const hash = crypto
462
546
  .createHash("sha256")
463
- .update(JSON.stringify(stableValue({ globalState, artifacts, events })))
547
+ .update(JSON.stringify(stableValue({ globalState, artifacts, projections, events })))
464
548
  .digest("hex")
465
549
  .slice(0, 24);
466
550
  return `runtime:${hash}`;
@@ -2,6 +2,7 @@ import crypto from "crypto";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { getAgentflowDataRoot } from "./paths.mjs";
5
+ import { getTeamForUser } from "./teams.mjs";
5
6
 
6
7
  const REGISTRY_VERSION = 1;
7
8
  const DEFAULT_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -67,15 +68,19 @@ function publicWorkspace(record, userId = "") {
67
68
  if (!record) return null;
68
69
  const actorId = normalizeUserId(userId);
69
70
  const members = record.members && typeof record.members === "object" ? record.members : {};
71
+ const access = workspaceCollaborationAccess(record, actorId);
70
72
  return {
71
73
  id: record.id,
72
74
  flowId: record.flowId,
73
75
  flowSource: record.projectSource || record.flowSource || "workspace",
74
76
  archived: record.archived === true,
75
77
  ownerId: record.ownerId,
76
- role: actorId === record.ownerId ? "owner" : members[actorId] || "",
78
+ role: access.role,
79
+ accessSource: access.source,
80
+ teamId: access.teamId,
77
81
  memberCount: Object.keys(members).length,
78
82
  members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
83
+ teamShares: Object.entries(record.teamShares || {}).map(([teamId, role]) => ({ teamId, role })),
79
84
  createdAt: record.createdAt || "",
80
85
  updatedAt: record.updatedAt || "",
81
86
  };
@@ -98,12 +103,19 @@ export function getWorkspaceCollaborationById(workspaceId) {
98
103
  export function workspaceCollaborationAccess(record, userId) {
99
104
  if (!record) return { allowed: true, role: "" };
100
105
  const id = normalizeUserId(userId);
101
- const role = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
106
+ const directRole = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
107
+ const team = getTeamForUser(id);
108
+ const teamRole = team ? String(record.teamShares?.[team.id] || "") : "";
109
+ const role = directRole === "owner" || directRole === "editor" || teamRole === "editor"
110
+ ? directRole === "owner" ? "owner" : "editor"
111
+ : directRole === "viewer" || teamRole === "viewer" ? "viewer" : "";
102
112
  return {
103
113
  allowed: role === "owner" || role === "editor" || role === "viewer",
104
114
  writable: role === "owner" || role === "editor",
105
115
  runnable: role === "owner" || role === "editor",
106
116
  role,
117
+ source: directRole ? "member" : teamRole ? "team" : "",
118
+ teamId: teamRole ? team?.id || "" : "",
107
119
  };
108
120
  }
109
121
 
@@ -256,6 +268,44 @@ export function addWorkspaceCollaborationMember({
256
268
  return { workspace: publicWorkspace(record, actorId), memberUserId: targetId };
257
269
  }
258
270
 
271
+ export function setWorkspaceCollaborationTeamShare({
272
+ workspaceId,
273
+ userId,
274
+ teamId,
275
+ role = "viewer",
276
+ }) {
277
+ const registry = readRegistry();
278
+ const record = registry.workspaces[String(workspaceId || "").trim()];
279
+ if (!record) return { error: "Workspace collaboration not found", status: 404 };
280
+ const actorId = normalizeUserId(userId);
281
+ const targetTeamId = String(teamId || "").trim();
282
+ if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
283
+ return { error: "Only the workspace owner can share with a team", status: 403 };
284
+ }
285
+ if (!targetTeamId) return { error: "Missing teamId", status: 400 };
286
+ record.teamShares = record.teamShares && typeof record.teamShares === "object" ? record.teamShares : {};
287
+ record.teamShares[targetTeamId] = role === "editor" ? "editor" : "viewer";
288
+ record.updatedAt = new Date().toISOString();
289
+ writeRegistry(registry);
290
+ return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId };
291
+ }
292
+
293
+ export function removeWorkspaceCollaborationTeamShare({ workspaceId, userId, teamId }) {
294
+ const registry = readRegistry();
295
+ const record = registry.workspaces[String(workspaceId || "").trim()];
296
+ if (!record) return { error: "Workspace collaboration not found", status: 404 };
297
+ const actorId = normalizeUserId(userId);
298
+ const targetTeamId = String(teamId || "").trim();
299
+ if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
300
+ return { error: "Only the workspace owner can revoke a team share", status: 403 };
301
+ }
302
+ const removed = Boolean(record.teamShares?.[targetTeamId]);
303
+ if (removed) delete record.teamShares[targetTeamId];
304
+ record.updatedAt = new Date().toISOString();
305
+ writeRegistry(registry);
306
+ return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId, removed };
307
+ }
308
+
259
309
  export function removeWorkspaceCollaborationMember({
260
310
  workspaceId,
261
311
  userId,