@prismer/sdk 1.9.22 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1498,6 +1498,106 @@ var ENVIRONMENTS = {
1498
1498
  production: "https://prismer.cloud"
1499
1499
  };
1500
1500
 
1501
+ // src/im/workspace/tasks.ts
1502
+ var CARD_KANBAN_STATUSES = /* @__PURE__ */ new Set([
1503
+ "backlog",
1504
+ "todo",
1505
+ "in_progress",
1506
+ "review",
1507
+ "completed",
1508
+ "failed",
1509
+ "cancelled",
1510
+ "pending",
1511
+ "assigned",
1512
+ "running",
1513
+ "done"
1514
+ ]);
1515
+ var CARD_PRIORITIES = /* @__PURE__ */ new Set([
1516
+ "low",
1517
+ "medium",
1518
+ "high",
1519
+ "urgent"
1520
+ ]);
1521
+ function readCardKanbanMetadata(source) {
1522
+ const metadata = readSourceMetadata(source);
1523
+ const nestedKanban = readRecord(metadata.kanban);
1524
+ const fields = nestedKanban ? { ...metadata, ...nestedKanban } : metadata;
1525
+ return toCanonicalCardKanbanMetadata(fields);
1526
+ }
1527
+ function writeCardKanbanMetadata(metadata, kanban) {
1528
+ const previous = readRecord(metadata) ?? {};
1529
+ const previousKanban = readRecord(previous.kanban);
1530
+ const currentView = toWritableCardKanbanMetadata(readCardKanbanMetadata(previous));
1531
+ const nextView = toWritableCardKanbanMetadata(toCanonicalCardKanbanMetadata(kanban));
1532
+ const nextKanban = toWritableCardKanbanMetadata({
1533
+ ...previousKanban ? omitLegacyOrder(previousKanban) : {},
1534
+ ...currentView,
1535
+ ...nextView
1536
+ });
1537
+ return {
1538
+ ...omitLegacyOrder(previous),
1539
+ ...nextKanban,
1540
+ kanban: nextKanban
1541
+ };
1542
+ }
1543
+ function readSourceMetadata(source) {
1544
+ const record = readRecord(source);
1545
+ if (!record) return {};
1546
+ return readRecord(record.metadata) ?? record;
1547
+ }
1548
+ function toCanonicalCardKanbanMetadata(source) {
1549
+ const record = readRecord(source) ?? {};
1550
+ const metadata = {};
1551
+ const columnId = readString(record.columnId);
1552
+ const cardOrder = readFiniteNumber(record.cardOrder) ?? readFiniteNumber(record.order);
1553
+ const cardStatus = readCardKanbanStatus(record.cardStatus);
1554
+ const cardPriority = readCardPriority(record.cardPriority);
1555
+ const cardLabels = readCardLabels(record.cardLabels);
1556
+ if (columnId) metadata.columnId = columnId;
1557
+ if (cardOrder !== void 0) metadata.cardOrder = cardOrder;
1558
+ if (cardStatus) metadata.cardStatus = cardStatus;
1559
+ if (cardPriority) metadata.cardPriority = cardPriority;
1560
+ if (cardLabels) metadata.cardLabels = cardLabels;
1561
+ return metadata;
1562
+ }
1563
+ function toWritableCardKanbanMetadata(view) {
1564
+ return view.cardStatus === "done" ? { ...view, cardStatus: "completed" } : view;
1565
+ }
1566
+ function omitLegacyOrder(record) {
1567
+ const { order: _legacyOrder, ...rest } = record;
1568
+ return rest;
1569
+ }
1570
+ function readRecord(value) {
1571
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1572
+ }
1573
+ function readString(value) {
1574
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1575
+ }
1576
+ function readFiniteNumber(value) {
1577
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1578
+ }
1579
+ function readCardKanbanStatus(value) {
1580
+ return typeof value === "string" && CARD_KANBAN_STATUSES.has(value) ? value : void 0;
1581
+ }
1582
+ function readCardPriority(value) {
1583
+ return typeof value === "string" && CARD_PRIORITIES.has(value) ? value : void 0;
1584
+ }
1585
+ function readCardLabels(value) {
1586
+ const record = readRecord(value);
1587
+ if (!record) return void 0;
1588
+ const labels = {};
1589
+ const color = readString(record.color);
1590
+ const emoji = readString(record.emoji);
1591
+ const tagIds = Array.isArray(record.tagIds) && record.tagIds.every((tagId) => typeof tagId === "string") ? [...record.tagIds] : void 0;
1592
+ if (color) labels.color = color;
1593
+ if (emoji) labels.emoji = emoji;
1594
+ if (tagIds) labels.tagIds = tagIds;
1595
+ return Object.keys(labels).length > 0 ? labels : void 0;
1596
+ }
1597
+
1598
+ // src/im/workspace/assets.ts
1599
+ var WORKSPACE_ASSETS_ROUTE = "/api/im/assets";
1600
+
1501
1601
  // src/storage.ts
1502
1602
  var MemoryStorage = class {
1503
1603
  constructor() {
@@ -2949,10 +3049,13 @@ function createEnrichedExtractor(config) {
2949
3049
  }
2950
3050
 
2951
3051
  // src/evolution-runtime.ts
3052
+ var DEFAULT_OUTBOX_MAX_ATTEMPTS = 5;
2952
3053
  var EvolutionRuntime = class {
2953
3054
  constructor(client, config) {
2954
3055
  this.client = client;
2955
3056
  this.outbox = [];
3057
+ /** Entries that exceeded `outboxMaxAttempts`. Public-readable for tests/ops. */
3058
+ this.deadLetter = [];
2956
3059
  this.started = false;
2957
3060
  // Session tracking
2958
3061
  this._sessions = [];
@@ -2962,7 +3065,8 @@ var EvolutionRuntime = class {
2962
3065
  enrichment: config?.enrichment ?? { mode: "rules" },
2963
3066
  scope: config?.scope ?? "global",
2964
3067
  outboxMaxSize: config?.outboxMaxSize ?? 50,
2965
- outboxFlushMs: config?.outboxFlushMs ?? 5e3
3068
+ outboxFlushMs: config?.outboxFlushMs ?? 5e3,
3069
+ outboxMaxAttempts: config?.outboxMaxAttempts ?? DEFAULT_OUTBOX_MAX_ATTEMPTS
2966
3070
  };
2967
3071
  this.scope = this.config.scope;
2968
3072
  this.cache = new EvolutionCache();
@@ -3028,8 +3132,9 @@ var EvolutionRuntime = class {
3028
3132
  confidence,
3029
3133
  fromCache
3030
3134
  };
3135
+ const narrowed = action === "apply_gene" || action === "create_suggested" || action === "none" ? action : "none";
3031
3136
  return {
3032
- action,
3137
+ action: narrowed,
3033
3138
  geneId,
3034
3139
  gene,
3035
3140
  strategy,
@@ -3180,25 +3285,53 @@ var EvolutionRuntime = class {
3180
3285
  } catch {
3181
3286
  }
3182
3287
  }
3183
- /** Flush outbox to server */
3288
+ /**
3289
+ * Flush outbox to server. Fire-and-forget contract: never blocks the caller's
3290
+ * critical path and never throws. Failed entries are retried until they hit
3291
+ * `outboxMaxAttempts`, then moved to `deadLetter` and reported via
3292
+ * `console.warn` so ops can detect a persistent failure.
3293
+ */
3184
3294
  async flush() {
3185
3295
  if (this.outbox.length === 0) return;
3186
3296
  const batch = this.outbox.splice(0, this.config.outboxMaxSize);
3187
- const promises = batch.map(
3188
- (entry) => this.client.record({
3189
- gene_id: entry.geneId,
3190
- signals: entry.signals.map((s) => s.type),
3191
- outcome: entry.outcome,
3192
- summary: entry.summary,
3193
- score: entry.score,
3194
- metadata: entry.metadata,
3195
- scope: this.scope
3196
- }).catch(() => {
3197
- this.outbox.push(entry);
3198
- })
3199
- );
3297
+ const promises = batch.map(async (entry) => {
3298
+ try {
3299
+ const res = await this.client.record({
3300
+ gene_id: entry.geneId,
3301
+ signals: entry.signals.map((s) => s.type),
3302
+ outcome: entry.outcome,
3303
+ summary: entry.summary,
3304
+ score: entry.score,
3305
+ metadata: entry.metadata,
3306
+ scope: this.scope
3307
+ });
3308
+ if (!res.ok) {
3309
+ this._handleOutboxFailure(entry, res.error?.message ?? "record returned ok=false");
3310
+ }
3311
+ } catch (err) {
3312
+ this._handleOutboxFailure(entry, err instanceof Error ? err.message : String(err));
3313
+ }
3314
+ });
3200
3315
  await Promise.allSettled(promises);
3201
3316
  }
3317
+ /**
3318
+ * Bump the attempt counter and either re-queue (under ceiling) or move to
3319
+ * dead-letter. Always synchronous — caller's `flush()` already wraps in a
3320
+ * promise.allSettled.
3321
+ */
3322
+ _handleOutboxFailure(entry, lastError) {
3323
+ const attempts = (entry.attempts ?? 0) + 1;
3324
+ const ceiling = this.config.outboxMaxAttempts;
3325
+ if (attempts >= ceiling) {
3326
+ const dropped = { ...entry, attempts, lastError, droppedAt: Date.now() };
3327
+ this.deadLetter.push(dropped);
3328
+ console.warn(
3329
+ `[EvolutionRuntime] outbox entry dropped after ${attempts} attempts: gene=${entry.geneId} outcome=${entry.outcome} error=${lastError}`
3330
+ );
3331
+ return;
3332
+ }
3333
+ this.outbox.push({ ...entry, attempts });
3334
+ }
3202
3335
  };
3203
3336
 
3204
3337
  // src/index.ts
@@ -3297,6 +3430,7 @@ var DirectClient = class {
3297
3430
  content,
3298
3431
  type: options?.type ?? "text",
3299
3432
  metadata: options?.metadata,
3433
+ attachments: options?.attachments,
3300
3434
  parentId: options?.parentId,
3301
3435
  quotedMessageId: options?.quotedMessageId
3302
3436
  });
@@ -3331,6 +3465,7 @@ var GroupsClient = class {
3331
3465
  content,
3332
3466
  type: options?.type ?? "text",
3333
3467
  metadata: options?.metadata,
3468
+ attachments: options?.attachments,
3334
3469
  parentId: options?.parentId,
3335
3470
  quotedMessageId: options?.quotedMessageId
3336
3471
  });
@@ -3409,6 +3544,7 @@ var MessagesClient = class {
3409
3544
  content,
3410
3545
  type: options?.type ?? "text",
3411
3546
  metadata: options?.metadata,
3547
+ attachments: options?.attachments,
3412
3548
  parentId: options?.parentId,
3413
3549
  quotedMessageId: options?.quotedMessageId
3414
3550
  });
@@ -3469,6 +3605,11 @@ var ContactsClient = class {
3469
3605
  const query = {};
3470
3606
  if (options?.type) query.type = options.type;
3471
3607
  if (options?.capability) query.capability = options.capability;
3608
+ if (options?.status) query.status = options.status;
3609
+ if (options?.onlineOnly) query.onlineOnly = options.onlineOnly;
3610
+ if (options?.q) query.q = options.q;
3611
+ if (options?.limit) query.limit = options.limit;
3612
+ if (options?.offset) query.offset = options.offset;
3472
3613
  return this._r("GET", "/api/im/discover", void 0, query);
3473
3614
  }
3474
3615
  // ─── Friend System (v1.8.0 P9) ─────────────────────────
@@ -3682,6 +3823,32 @@ var TasksClient = class {
3682
3823
  async cancel(taskId) {
3683
3824
  return this._r("DELETE", `/api/im/tasks/${taskId}`);
3684
3825
  }
3826
+ /**
3827
+ * v2.0 release 200 §6.1 — unified state-machine transition.
3828
+ *
3829
+ * Drives every kanban / approve / reject / cancel / blocked / retry /
3830
+ * restore action through one endpoint. The 5 legacy endpoints
3831
+ * (start/complete/approve/reject/cancel) remain for backward
3832
+ * compatibility but new integrations should prefer this entrypoint.
3833
+ *
3834
+ * Server responds 409 (`code: 'invalid-transition'`) if the requested
3835
+ * `to` is not in the TRANSITIONS matrix from the current status, or
3836
+ * 403 (`code: 'forbidden'`) if the actor's tier is not in the rule's
3837
+ * `allowedActors`.
3838
+ */
3839
+ async transition(taskId, options) {
3840
+ return this._r("POST", `/api/im/tasks/${taskId}/transition`, options);
3841
+ }
3842
+ /**
3843
+ * v2.0 release 200 §5.3 — admin escape-hatch.
3844
+ *
3845
+ * Bypasses the TRANSITIONS matrix. Restricted to workspace owner /
3846
+ * admin / trustTier>=4. Reason is required; the call is audit-logged
3847
+ * with `force_transition: true`. UI does NOT expose this — ops only.
3848
+ */
3849
+ async forceTransition(taskId, options) {
3850
+ return this._r("POST", `/api/im/tasks/${taskId}/force-transition`, options);
3851
+ }
3685
3852
  };
3686
3853
  var MemoryClient = class {
3687
3854
  constructor(_r) {
@@ -3810,9 +3977,118 @@ var SecurityClient = class {
3810
3977
  return this._r("DELETE", `/api/im/conversations/${conversationId}/keys/${keyUserId}`);
3811
3978
  }
3812
3979
  };
3980
+ var EvolutionSkillsClient = class {
3981
+ constructor(_r) {
3982
+ this._r = _r;
3983
+ }
3984
+ /** List the skill catalog. Alias of search() for the v2.0 public surface. */
3985
+ async list(options) {
3986
+ return this.search(options);
3987
+ }
3988
+ /** Browse and search the skill catalog. */
3989
+ async search(options) {
3990
+ const query = {};
3991
+ if (options?.query) query.query = options.query;
3992
+ if (options?.category) query.category = options.category;
3993
+ if (options?.source) query.source = options.source;
3994
+ if (options?.compatibility) query.compatibility = options.compatibility;
3995
+ if (options?.sort) query.sort = options.sort;
3996
+ if (options?.page != null) query.page = String(options.page);
3997
+ if (options?.limit != null) query.limit = String(options.limit);
3998
+ return this._r("GET", "/api/im/skills/search", void 0, query);
3999
+ }
4000
+ /** Get skill catalog stats. */
4001
+ async stats() {
4002
+ return this._r("GET", "/api/im/skills/stats");
4003
+ }
4004
+ /** List available skill categories. */
4005
+ async categories() {
4006
+ return this._r("GET", "/api/im/skills/categories");
4007
+ }
4008
+ /** List trending skills. */
4009
+ async trending(limit) {
4010
+ const query = {};
4011
+ if (limit != null) query.limit = String(limit);
4012
+ return this._r("GET", "/api/im/skills/trending", void 0, query);
4013
+ }
4014
+ /** List skills created by the authenticated agent. */
4015
+ async created() {
4016
+ return this._r("GET", "/api/im/skills/created");
4017
+ }
4018
+ /** Get skill detail by slug or ID. */
4019
+ async get(slugOrId) {
4020
+ return this._r("GET", `/api/im/skills/${encodeURIComponent(slugOrId)}`);
4021
+ }
4022
+ /** Get full SKILL.md content and package metadata. */
4023
+ async content(slugOrId) {
4024
+ return this._r("GET", `/api/im/skills/${encodeURIComponent(slugOrId)}/content`);
4025
+ }
4026
+ /** Create/submit a workspace or community skill. */
4027
+ async create(input) {
4028
+ return this._r("POST", "/api/im/skills", input);
4029
+ }
4030
+ /** Update a skill. */
4031
+ async update(skillId, input) {
4032
+ return this._r("PATCH", `/api/im/skills/${encodeURIComponent(skillId)}`, input);
4033
+ }
4034
+ /** Soft-delete/deprecate a skill. */
4035
+ async delete(skillId) {
4036
+ return this._r("DELETE", `/api/im/skills/${encodeURIComponent(skillId)}`);
4037
+ }
4038
+ /** Install a skill for the authenticated agent. */
4039
+ async install(slugOrId, scope) {
4040
+ return this._r("POST", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`, scope ? { scope } : void 0);
4041
+ }
4042
+ /** Uninstall a skill for the authenticated agent. */
4043
+ async uninstall(slugOrId, scope) {
4044
+ const query = scope ? { scope } : void 0;
4045
+ return this._r("DELETE", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`, void 0, query);
4046
+ }
4047
+ /**
4048
+ * List installed skills. When agentId is supplied, this uses the v2.0 Layer 5
4049
+ * route and includes daemon sync state; otherwise it keeps the legacy current-agent route.
4050
+ */
4051
+ async installed(options) {
4052
+ const query = {};
4053
+ if (options?.workspaceId) query.workspaceId = options.workspaceId;
4054
+ if (options?.includeInactive) query.includeInactive = "true";
4055
+ if (options?.agentId) {
4056
+ return this._r("GET", `/api/im/agents/${encodeURIComponent(options.agentId)}/skills`, void 0, query);
4057
+ }
4058
+ return this._r("GET", "/api/im/skills/installed", void 0, query);
4059
+ }
4060
+ /** Install a skill to a specific agent. */
4061
+ async installForAgent(agentId, skillIdOrSlug, options) {
4062
+ return this._r("POST", `/api/im/agents/${encodeURIComponent(agentId)}/skills`, {
4063
+ skillId: skillIdOrSlug,
4064
+ ...options
4065
+ });
4066
+ }
4067
+ /** Disable/uninstall a skill from a specific agent. Built-ins are disabled cloud-side. */
4068
+ async uninstallFromAgent(agentId, skillIdOrSlug, options) {
4069
+ return this._r("DELETE", `/api/im/agents/${encodeURIComponent(agentId)}/skills`, {
4070
+ skillId: skillIdOrSlug,
4071
+ workspaceId: options?.workspaceId
4072
+ });
4073
+ }
4074
+ /** List skills whose daemon sync state is not current. */
4075
+ async pending(agentId, workspaceId) {
4076
+ const query = workspaceId ? { workspaceId } : void 0;
4077
+ return this._r("GET", `/api/im/agents/${encodeURIComponent(agentId)}/skills/pending`, void 0, query);
4078
+ }
4079
+ /** Acknowledge daemon sync for an installed skill. */
4080
+ async ack(agentId, input) {
4081
+ return this._r("POST", `/api/im/agents/${encodeURIComponent(agentId)}/skills/ack`, input);
4082
+ }
4083
+ /** Star a skill. */
4084
+ async star(skillId) {
4085
+ return this._r("POST", `/api/im/skills/${encodeURIComponent(skillId)}/star`);
4086
+ }
4087
+ };
3813
4088
  var EvolutionClient = class {
3814
4089
  constructor(_r) {
3815
4090
  this._r = _r;
4091
+ this.skills = new EvolutionSkillsClient(_r);
3816
4092
  }
3817
4093
  // ── Public endpoints (no auth required) ──
3818
4094
  /** Get evolution stats */
@@ -3942,7 +4218,7 @@ var EvolutionClient = class {
3942
4218
  const { outcome, score, summary, strategy_used, scope, ...analyzeOpts } = options;
3943
4219
  const analysis = await this.analyze({ ...analyzeOpts, ...scope ? { scope } : {} });
3944
4220
  if (!analysis.ok || !analysis.data) {
3945
- return { ok: false, error: analysis.error };
4221
+ return { ok: false, ...analysis.error ? { error: analysis.error } : {} };
3946
4222
  }
3947
4223
  const data = analysis.data;
3948
4224
  const geneId = data.gene_id;
@@ -4056,39 +4332,35 @@ var EvolutionClient = class {
4056
4332
  }
4057
4333
  /** Search skills catalog */
4058
4334
  async searchSkills(options) {
4059
- const q = {};
4060
- if (options?.query) q.query = options.query;
4061
- if (options?.category) q.category = options.category;
4062
- if (options?.limit != null) q.limit = String(options.limit);
4063
- return this._r("GET", "/api/im/skills/search", void 0, q);
4335
+ return this.skills.search(options);
4064
4336
  }
4065
4337
  /** Get skill catalog stats */
4066
4338
  async getSkillStats() {
4067
- return this._r("GET", "/api/im/skills/stats");
4339
+ return this.skills.stats();
4068
4340
  }
4069
4341
  /** Install a skill — creates Gene + returns content + install guide */
4070
4342
  async installSkill(slugOrId, scope) {
4071
- return this._r("POST", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`, scope ? { scope } : void 0);
4343
+ return this.skills.install(slugOrId, scope);
4072
4344
  }
4073
4345
  /** Uninstall a skill */
4074
- async uninstallSkill(slugOrId) {
4075
- return this._r("DELETE", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`);
4346
+ async uninstallSkill(slugOrId, scope) {
4347
+ return this.skills.uninstall(slugOrId, scope);
4076
4348
  }
4077
4349
  /** List installed skills for this agent */
4078
- async installedSkills() {
4079
- return this._r("GET", "/api/im/skills/installed");
4350
+ async installedSkills(options) {
4351
+ return this.skills.installed(options);
4080
4352
  }
4081
4353
  /** Get full skill content (SKILL.md + package info) */
4082
4354
  async getSkillContent(slugOrId) {
4083
- return this._r("GET", `/api/im/skills/${encodeURIComponent(slugOrId)}/content`);
4355
+ return this.skills.content(slugOrId);
4084
4356
  }
4085
4357
  /** Create/submit a community skill */
4086
4358
  async createSkill(input) {
4087
- return this._r("POST", "/api/im/skills", input);
4359
+ return this.skills.create(input);
4088
4360
  }
4089
4361
  /** Star a skill (increment community rating) */
4090
4362
  async starSkill(skillId) {
4091
- return this._r("POST", `/api/im/skills/${encodeURIComponent(skillId)}/star`);
4363
+ return this.skills.star(skillId);
4092
4364
  }
4093
4365
  /**
4094
4366
  * Install a skill and write SKILL.md to local filesystem.
@@ -4098,19 +4370,26 @@ var EvolutionClient = class {
4098
4370
  */
4099
4371
  async installSkillLocal(slugOrId, options) {
4100
4372
  const result = await this.installSkill(slugOrId);
4101
- if (!result.ok || !result.data) return result;
4102
- let content = result.data.skill?.content || "";
4373
+ if (!result.ok || !result.data) {
4374
+ return result;
4375
+ }
4376
+ const installData = result.data;
4377
+ const withLocalPaths = (localPaths2) => ({
4378
+ ok: true,
4379
+ data: { ...installData, localPaths: localPaths2 }
4380
+ });
4381
+ let content = installData.skill?.content || "";
4103
4382
  if (!content) {
4104
4383
  const contentResult = await this.getSkillContent(slugOrId);
4105
4384
  content = contentResult.data?.content || "";
4106
4385
  }
4107
4386
  if (!content) {
4108
- return { ...result, data: { ...result.data, localPaths: [] } };
4387
+ return withLocalPaths([]);
4109
4388
  }
4110
- const rawSlug = result.data.skill?.slug || slugOrId;
4389
+ const rawSlug = installData.skill?.slug || slugOrId;
4111
4390
  const slug = rawSlug.replace(/[\/\\]/g, "").replace(/\.\./g, "");
4112
4391
  if (!slug) {
4113
- return { ...result, data: { ...result.data, localPaths: [] } };
4392
+ return withLocalPaths([]);
4114
4393
  }
4115
4394
  const localPaths = [];
4116
4395
  try {
@@ -4130,7 +4409,7 @@ var EvolutionClient = class {
4130
4409
  "opencode": path.join(home, ".config", "opencode", "skills", slug),
4131
4410
  "plugin": path.join(pluginBase, "skills", slug)
4132
4411
  };
4133
- const targets = options?.platforms || Object.keys(platformPaths);
4412
+ const targets = options?.platforms ?? Object.keys(platformPaths);
4134
4413
  for (const platform of targets) {
4135
4414
  const dir = platformPaths[platform];
4136
4415
  if (!dir) continue;
@@ -4144,7 +4423,7 @@ var EvolutionClient = class {
4144
4423
  }
4145
4424
  } catch {
4146
4425
  }
4147
- return { ...result, data: { ...result.data, localPaths } };
4426
+ return withLocalPaths(localPaths);
4148
4427
  }
4149
4428
  /**
4150
4429
  * Uninstall a skill and remove local SKILL.md files.
@@ -4152,8 +4431,13 @@ var EvolutionClient = class {
4152
4431
  async uninstallSkillLocal(slugOrId) {
4153
4432
  const result = await this.uninstallSkill(slugOrId);
4154
4433
  const removedPaths = [];
4434
+ const withRemoved = (ok, paths) => ({
4435
+ ok: result.ok,
4436
+ ...result.error ? { error: result.error } : {},
4437
+ data: { uninstalled: ok, removedPaths: paths }
4438
+ });
4155
4439
  const slug = safeSlug(slugOrId);
4156
- if (!slug) return { ...result, data: { uninstalled: result.data?.uninstalled ?? false, removedPaths } };
4440
+ if (!slug) return withRemoved(result.data?.uninstalled ?? false, removedPaths);
4157
4441
  try {
4158
4442
  const fs = await import("fs");
4159
4443
  const path = await import("path");
@@ -4177,7 +4461,7 @@ var EvolutionClient = class {
4177
4461
  }
4178
4462
  } catch {
4179
4463
  }
4180
- return { ...result, data: { uninstalled: result.data?.uninstalled ?? false, removedPaths } };
4464
+ return withRemoved(result.data?.uninstalled ?? false, removedPaths);
4181
4465
  }
4182
4466
  /**
4183
4467
  * Sync all installed skills to local filesystem.
@@ -4217,7 +4501,7 @@ var EvolutionClient = class {
4217
4501
  "opencode": path.join(home, ".config", "opencode", "skills", slug),
4218
4502
  "plugin": path.join(pluginBase, "skills", slug)
4219
4503
  };
4220
- const targets = options?.platforms || Object.keys(platformPaths);
4504
+ const targets = options?.platforms ?? Object.keys(platformPaths);
4221
4505
  for (const platform of targets) {
4222
4506
  const dir = platformPaths[platform];
4223
4507
  if (!dir) continue;
@@ -4266,12 +4550,34 @@ var EvolutionClient = class {
4266
4550
  if (since != null) query.since = String(since);
4267
4551
  return this._r("GET", "/api/im/evolution/sync/snapshot", void 0, query);
4268
4552
  }
4269
- /** Bidirectional sync: push local outcomes and pull remote updates */
4553
+ /**
4554
+ * Bidirectional sync: push local outcomes and pull remote updates.
4555
+ *
4556
+ * Accepts either the flat shape (`pushOutcomes` / `pullSince`) used by older
4557
+ * callers or the nested shape (`push` / `pull`) that mirrors the wire format
4558
+ * expected by `POST /api/im/evolution/sync`. The nested shape is preferred for
4559
+ * new code because it lets you pin a scope per-side.
4560
+ */
4270
4561
  async sync(options) {
4271
4562
  const body = {};
4272
- if (options?.pushOutcomes) body.push = { outcomes: options.pushOutcomes };
4273
- if (options?.pullSince != null) body.pull = { since: options.pullSince };
4274
- return this._r("POST", "/api/im/evolution/sync", body);
4563
+ const outcomes = options?.push?.outcomes ?? options?.pushOutcomes;
4564
+ if (outcomes) {
4565
+ body.push = {
4566
+ outcomes,
4567
+ ...options?.push?.scope ? { scope: options.push.scope } : {},
4568
+ ...options?.push?.workspaceId ? { workspaceId: options.push.workspaceId } : {}
4569
+ };
4570
+ }
4571
+ const since = options?.pull?.since ?? options?.pullSince;
4572
+ if (since != null) {
4573
+ body.pull = {
4574
+ since,
4575
+ ...options?.pull?.scope ? { scope: options.pull.scope } : {}
4576
+ };
4577
+ }
4578
+ const query = {};
4579
+ if (options?.scope) query.scope = options.scope;
4580
+ return this._r("POST", "/api/im/evolution/sync", body, query);
4275
4581
  }
4276
4582
  };
4277
4583
  function safeSlug(input) {
@@ -4315,6 +4621,26 @@ var WorkspacesClient = class {
4315
4621
  async archive(workspaceId) {
4316
4622
  return this._r("DELETE", `/api/im/workspaces/${workspaceId}`);
4317
4623
  }
4624
+ /**
4625
+ * Get the workspace's orchestrator agent (Chief of Staff) — readable by any
4626
+ * member. Returns `{ workspace, orchestrator: null }` when no active
4627
+ * appointment exists. See release 200 §4.
4628
+ */
4629
+ async getOrchestrator(workspaceId) {
4630
+ return this._r("GET", `/api/im/workspaces/${workspaceId}/orchestrator`);
4631
+ }
4632
+ /**
4633
+ * Appoint an agent as the workspace's orchestrator. Owner-only. If an
4634
+ * orchestrator is already active, this auto-revokes the previous one in the
4635
+ * same UPDATE.
4636
+ */
4637
+ async appointOrchestrator(workspaceId, agentImUserId) {
4638
+ return this._r("POST", `/api/im/workspaces/${workspaceId}/orchestrator`, { agentImUserId });
4639
+ }
4640
+ /** Revoke the workspace's current orchestrator. Owner-only. Idempotent. */
4641
+ async revokeOrchestrator(workspaceId) {
4642
+ return this._r("DELETE", `/api/im/workspaces/${workspaceId}/orchestrator`);
4643
+ }
4318
4644
  };
4319
4645
  var WorkspaceFilesClient = class {
4320
4646
  constructor(_r) {
@@ -4348,6 +4674,83 @@ var WorkspaceFilesClient = class {
4348
4674
  return this._r("GET", `/api/im/workspaces/${workspaceId}/files/${fileId}/history`);
4349
4675
  }
4350
4676
  };
4677
+ var MAX_IM_ASSET_BYTES = 1024 * 1024 * 1024;
4678
+ var DIRECT_ASSET_UPLOAD_FALLBACK_STATUSES = /* @__PURE__ */ new Set([404, 501, 503]);
4679
+ function toHex(bytes) {
4680
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
4681
+ }
4682
+ async function sha256BytesHex(bytes) {
4683
+ if (globalThis.crypto?.subtle) {
4684
+ const ab = new ArrayBuffer(bytes.byteLength);
4685
+ new Uint8Array(ab).set(bytes);
4686
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", ab);
4687
+ return toHex(new Uint8Array(digest));
4688
+ }
4689
+ const { createHash } = await import("crypto");
4690
+ return createHash("sha256").update(bytes).digest("hex");
4691
+ }
4692
+ function bytesToBlob(bytes, mimeType) {
4693
+ const ab = new ArrayBuffer(bytes.byteLength);
4694
+ new Uint8Array(ab).set(bytes);
4695
+ return new Blob([ab], { type: mimeType });
4696
+ }
4697
+ function isNamedFile(input) {
4698
+ return typeof File !== "undefined" && input instanceof File;
4699
+ }
4700
+ function normalizeStringHeaders(value) {
4701
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4702
+ const out = {};
4703
+ for (const [key, raw] of Object.entries(value)) {
4704
+ if (typeof raw === "string") out[key] = raw;
4705
+ }
4706
+ return Object.keys(out).length > 0 ? out : void 0;
4707
+ }
4708
+ function isDirectAssetUploadPlan(value) {
4709
+ if (!value || typeof value !== "object") return false;
4710
+ const plan = value;
4711
+ if (plan.mode === "single") {
4712
+ return typeof plan.uploadUrl === "string" && typeof plan.bucket === "string" && typeof plan.key === "string";
4713
+ }
4714
+ if (plan.mode === "multipart") {
4715
+ return typeof plan.bucket === "string" && typeof plan.key === "string" && typeof plan.uploadId === "string" && typeof plan.partSizeBytes === "number" && Array.isArray(plan.parts) && plan.parts.every((part) => {
4716
+ const item = part;
4717
+ return typeof item.url === "string" && Number.isInteger(item.partNumber);
4718
+ });
4719
+ }
4720
+ return false;
4721
+ }
4722
+ function assetUploadError(code, message) {
4723
+ return { ok: false, error: { code, message } };
4724
+ }
4725
+ async function normalizeAssetUploadInput(input, options) {
4726
+ let bytes;
4727
+ let fileName;
4728
+ if (typeof input === "string") {
4729
+ const fs = await import("fs");
4730
+ const path = await import("path");
4731
+ const buf = await fs.promises.readFile(input);
4732
+ bytes = new Uint8Array(buf);
4733
+ fileName = options.fileName || path.basename(input);
4734
+ } else if (typeof Blob !== "undefined" && input instanceof Blob) {
4735
+ const ab = await input.arrayBuffer();
4736
+ bytes = new Uint8Array(ab);
4737
+ fileName = options.fileName || (isNamedFile(input) ? input.name : "");
4738
+ if (!fileName) throw new Error("fileName is required when uploading Blob without name");
4739
+ } else if (input instanceof Uint8Array) {
4740
+ bytes = input;
4741
+ fileName = options.fileName || "";
4742
+ if (!fileName) throw new Error("fileName is required when uploading Buffer or Uint8Array");
4743
+ } else {
4744
+ throw new Error("Unsupported input type");
4745
+ }
4746
+ const sizeBytes = bytes.byteLength;
4747
+ if (sizeBytes > MAX_IM_ASSET_BYTES) {
4748
+ throw new Error("Asset exceeds 1 GB cap");
4749
+ }
4750
+ const mimeType = options.mimeType || guessMimeType(fileName);
4751
+ const contentHash = await sha256BytesHex(bytes);
4752
+ return { bytes, fileName, mimeType, sizeBytes, contentHash };
4753
+ }
4351
4754
  var AssetsClient = class {
4352
4755
  constructor(_r, _baseUrl, _fetchFn, _getAuthHeaders) {
4353
4756
  this._r = _r;
@@ -4414,57 +4817,130 @@ var AssetsClient = class {
4414
4817
  };
4415
4818
  }
4416
4819
  /**
4417
- * Upload bytes as an asset (multipart). 100 MB hard cap; >50 MB returns
4418
- * 413 with `USE_PRESIGNED` use S3 presign flow for those (not yet wrapped).
4820
+ * Upload bytes as an asset. Uses direct-to-S3 upload when the server exposes
4821
+ * `/assets/direct-upload/*`; falls back to legacy multipart POST for local
4822
+ * filesystem mode and older deployments. The client always sends SHA-256 for
4823
+ * server-side byte integrity checks. Hard cap: 1 GB.
4419
4824
  */
4420
4825
  async upload(input, options) {
4421
- let bytes;
4422
- let fileName;
4423
- if (typeof input === "string") {
4424
- const fs = await import("fs");
4425
- const path = await import("path");
4426
- const buf = await fs.promises.readFile(input);
4427
- bytes = new Uint8Array(buf);
4428
- fileName = options.fileName || path.basename(input);
4429
- } else if (typeof Blob !== "undefined" && input instanceof Blob) {
4430
- const ab2 = await input.arrayBuffer();
4431
- bytes = new Uint8Array(ab2);
4432
- fileName = options.fileName || (input instanceof File ? input.name : "");
4433
- if (!fileName) throw new Error("fileName is required when uploading Blob without name");
4434
- } else if (input instanceof Uint8Array) {
4435
- bytes = input;
4436
- fileName = options.fileName || "";
4437
- if (!fileName) throw new Error("fileName is required when uploading Buffer or Uint8Array");
4438
- } else {
4439
- throw new Error("Unsupported input type");
4826
+ const normalized = await normalizeAssetUploadInput(input, options);
4827
+ const direct = await this._tryDirectUpload(normalized, options);
4828
+ if (direct) return direct;
4829
+ return this._uploadMultipart(normalized, options);
4830
+ }
4831
+ async _tryDirectUpload(file, options) {
4832
+ const init = await this._postAssetJson("/direct-upload/init", {
4833
+ workspaceId: options.workspaceId,
4834
+ filename: file.fileName,
4835
+ mime: file.mimeType,
4836
+ sizeBytes: file.sizeBytes,
4837
+ contentHash: file.contentHash
4838
+ });
4839
+ if (!init.response.ok) {
4840
+ if (DIRECT_ASSET_UPLOAD_FALLBACK_STATUSES.has(init.response.status)) return null;
4841
+ return init.data && init.data.ok === false ? {
4842
+ ok: false,
4843
+ error: init.data.error ?? {
4844
+ code: "http_error",
4845
+ message: `Direct upload init failed (${init.response.status})`
4846
+ }
4847
+ } : assetUploadError("http_error", `Direct upload init failed (${init.response.status})`);
4440
4848
  }
4441
- const mimeType = options.mimeType || guessMimeType(fileName);
4442
- const sizeBytes = bytes.byteLength;
4443
- if (sizeBytes > 100 * 1024 * 1024) {
4444
- throw new Error("Asset exceeds 100 MB cap");
4849
+ if (!isDirectAssetUploadPlan(init.data?.data)) return null;
4850
+ const plan = init.data.data;
4851
+ try {
4852
+ const parts = await this._putDirectUploadBytes(plan, file, options.onProgress);
4853
+ const complete = await this._postAssetJson("/direct-upload/complete", {
4854
+ workspaceId: options.workspaceId,
4855
+ filename: file.fileName,
4856
+ mime: file.mimeType,
4857
+ sizeBytes: file.sizeBytes,
4858
+ contentHash: file.contentHash,
4859
+ kind: options.kind,
4860
+ metadata: options.metadata,
4861
+ sourceTaskId: options.sourceTaskId,
4862
+ sourceAgentImUserId: options.sourceAgentImUserId,
4863
+ bucket: plan.bucket,
4864
+ key: plan.key,
4865
+ ...plan.mode === "multipart" ? { uploadId: plan.uploadId, parts: parts ?? [] } : {}
4866
+ });
4867
+ if (!complete.response.ok) {
4868
+ return complete.data && complete.data.ok === false ? complete.data : assetUploadError("http_error", `Direct upload complete failed (${complete.response.status})`);
4869
+ }
4870
+ if (!complete.data) {
4871
+ return assetUploadError("invalid_response", "Direct upload complete returned an empty response");
4872
+ }
4873
+ return complete.data;
4874
+ } catch {
4875
+ return null;
4876
+ }
4877
+ }
4878
+ async _postAssetJson(path, body) {
4879
+ const response = await this._fetchFn(`${this._baseUrl}/api/im/assets${path}`, {
4880
+ method: "POST",
4881
+ headers: {
4882
+ ...this._getAuthHeaders(),
4883
+ "Content-Type": "application/json",
4884
+ Accept: "application/json"
4885
+ },
4886
+ body: JSON.stringify(body)
4887
+ });
4888
+ const data = await response.json().catch(() => null);
4889
+ return { response, data };
4890
+ }
4891
+ async _putDirectUploadBytes(plan, file, onProgress) {
4892
+ if (plan.mode === "single") {
4893
+ const response = await this._fetchFn(plan.uploadUrl, {
4894
+ method: plan.method ?? "PUT",
4895
+ headers: normalizeStringHeaders(plan.headers),
4896
+ body: bytesToBlob(file.bytes, file.mimeType)
4897
+ });
4898
+ if (!response.ok) throw new Error(`signed PUT failed (${response.status})`);
4899
+ onProgress?.(file.sizeBytes, file.sizeBytes);
4900
+ return null;
4901
+ }
4902
+ const completedParts = [];
4903
+ let uploaded = 0;
4904
+ for (const part of plan.parts) {
4905
+ const start = (part.partNumber - 1) * plan.partSizeBytes;
4906
+ const end = Math.min(start + plan.partSizeBytes, file.sizeBytes);
4907
+ const chunk = file.bytes.slice(start, end);
4908
+ const response = await this._fetchFn(part.url, { method: "PUT", body: bytesToBlob(chunk, file.mimeType) });
4909
+ if (!response.ok) throw new Error(`signed multipart PUT failed for part ${part.partNumber} (${response.status})`);
4910
+ const etag = response.headers.get("etag")?.replace(/^"|"$/g, "");
4911
+ if (!etag) throw new Error(`signed multipart PUT missing ETag for part ${part.partNumber}`);
4912
+ completedParts.push({ partNumber: part.partNumber, etag });
4913
+ uploaded += chunk.byteLength;
4914
+ onProgress?.(uploaded, file.sizeBytes);
4445
4915
  }
4916
+ return completedParts;
4917
+ }
4918
+ async _uploadMultipart(file, options) {
4919
+ const { bytes, fileName, mimeType, sizeBytes, contentHash } = file;
4446
4920
  const formData = new FormData();
4447
- const ab = new ArrayBuffer(bytes.byteLength);
4448
- new Uint8Array(ab).set(bytes);
4449
- formData.append("file", new Blob([ab], { type: mimeType }), fileName);
4921
+ formData.append("file", bytesToBlob(bytes, mimeType), fileName);
4450
4922
  formData.append("workspaceId", options.workspaceId);
4451
4923
  if (options.kind) formData.append("kind", options.kind);
4452
4924
  if (options.sourceAgentImUserId) formData.append("sourceAgentImUserId", options.sourceAgentImUserId);
4453
4925
  if (options.sourceTaskId) formData.append("sourceTaskId", options.sourceTaskId);
4454
4926
  if (options.metadata) formData.append("metadata", JSON.stringify(options.metadata));
4927
+ formData.append("contentSha256", contentHash);
4455
4928
  const resp = await this._fetchFn(`${this._baseUrl}/api/im/assets`, {
4456
4929
  method: "POST",
4457
4930
  body: formData,
4458
- headers: this._getAuthHeaders()
4931
+ headers: {
4932
+ ...this._getAuthHeaders(),
4933
+ "X-Content-Sha256": contentHash
4934
+ }
4459
4935
  });
4460
- options.onProgress?.(sizeBytes, sizeBytes);
4461
4936
  const data = await resp.json().catch(() => ({}));
4462
4937
  if (!resp.ok) {
4463
4938
  return {
4464
4939
  ok: false,
4465
- error: data?.error || { code: "HTTP_ERROR", message: `Upload failed (${resp.status})` }
4940
+ error: data?.error || { code: "http_error", message: `Upload failed (${resp.status})` }
4466
4941
  };
4467
4942
  }
4943
+ options.onProgress?.(sizeBytes, sizeBytes);
4468
4944
  return data;
4469
4945
  }
4470
4946
  };
@@ -4830,6 +5306,7 @@ var IMRealtimeClient = class {
4830
5306
  };
4831
5307
  var IMClient = class {
4832
5308
  constructor(request, wsBase, fetchFn, getAuthHeaders, offlineManager, communityHubConfig) {
5309
+ this._request = request;
4833
5310
  this.account = new AccountClient(request);
4834
5311
  this.direct = new DirectClient(request);
4835
5312
  this.groups = new GroupsClient(request);
@@ -4856,7 +5333,7 @@ var IMClient = class {
4856
5333
  }
4857
5334
  /** IM health check */
4858
5335
  async health() {
4859
- return this.account["_r"]("GET", "/api/im/health");
5336
+ return this._request("GET", "/api/im/health");
4860
5337
  }
4861
5338
  /** Get workspace superset view with slot filtering */
4862
5339
  async getWorkspace(scope, slots, includeContent) {
@@ -4864,7 +5341,22 @@ var IMClient = class {
4864
5341
  if (scope) params.set("scope", scope);
4865
5342
  if (slots?.length) params.set("slots", slots.join(","));
4866
5343
  if (includeContent) params.set("includeContent", "true");
4867
- return this.workspace["_r"]("GET", `/api/im/workspace/view?${params}`);
5344
+ return this._request("GET", `/api/im/workspace/view?${params}`);
5345
+ }
5346
+ /**
5347
+ * Issue a typed IM API request via the shared `RequestFn` pipeline.
5348
+ *
5349
+ * Use this when you need to hit an IM endpoint that isn't (yet) exposed by a
5350
+ * sub-client (e.g. `/api/im/approvals`). Same auth + offline routing + retry
5351
+ * behaviour as the typed sub-clients.
5352
+ *
5353
+ * @example
5354
+ * const res = await client.im.request<ApprovalCreateResponse>(
5355
+ * 'POST', '/api/im/approvals', { category, title, context, options },
5356
+ * );
5357
+ */
5358
+ async request(method, path, body, query) {
5359
+ return this._request(method, path, body, query);
4868
5360
  }
4869
5361
  };
4870
5362
  var PrismerClient = class {
@@ -4932,6 +5424,10 @@ var PrismerClient = class {
4932
5424
  this._offlineManager,
4933
5425
  config.community ?? null
4934
5426
  );
5427
+ this.workspaces = this.im.workspaces;
5428
+ this.workspaceFiles = this.im.workspaceFiles;
5429
+ this.assets = this.im.assets;
5430
+ this.evolution = this.im.evolution;
4935
5431
  }
4936
5432
  /** Wait for identity to be ready (useful for tests or explicit await) */
4937
5433
  async ensureIdentity() {
@@ -4980,6 +5476,39 @@ var PrismerClient = class {
4980
5476
  await this._offlineManager.destroy();
4981
5477
  }
4982
5478
  }
5479
+ /**
5480
+ * Issue an authenticated raw HTTP request against the configured base URL,
5481
+ * returning the underlying `Response` so callers can inspect headers
5482
+ * (`Content-Range`, `Content-Length`, etc.) and stream the body.
5483
+ *
5484
+ * The path may be absolute (`/api/...`) or a full URL — full URLs are used
5485
+ * verbatim (useful for following 302 redirects), otherwise the path is
5486
+ * appended to the client's configured `baseUrl`. Authorization + `X-IM-Agent`
5487
+ * headers are added automatically; caller-supplied headers in `init.headers`
5488
+ * override them on collision.
5489
+ *
5490
+ * Use this for binary downloads / partial fetches; for normal JSON-envelope
5491
+ * IM requests use `client.im.request()` (typed) or the typed sub-clients.
5492
+ */
5493
+ async fetchAuthed(url, init) {
5494
+ const fullUrl = /^https?:\/\//i.test(url) ? url : `${this.baseUrl}${url}`;
5495
+ const authHeaders = this._getAuthHeaders();
5496
+ const callerHeaders = {};
5497
+ if (init?.headers) {
5498
+ const h = init.headers;
5499
+ if (h instanceof Headers) {
5500
+ h.forEach((v, k) => {
5501
+ callerHeaders[k] = v;
5502
+ });
5503
+ } else if (Array.isArray(h)) {
5504
+ for (const [k, v] of h) callerHeaders[k] = v;
5505
+ } else {
5506
+ Object.assign(callerHeaders, h);
5507
+ }
5508
+ }
5509
+ const headers = { ...authHeaders, ...callerHeaders };
5510
+ return this.fetchFn(fullUrl, { ...init ?? {}, headers });
5511
+ }
4983
5512
  // --------------------------------------------------------------------------
4984
5513
  // Internal request helper
4985
5514
  // --------------------------------------------------------------------------
@@ -5016,18 +5545,18 @@ var PrismerClient = class {
5016
5545
  }
5017
5546
  }
5018
5547
  if (!response.ok) {
5019
- const err = data.error || { code: "HTTP_ERROR", message: `Request failed with status ${response.status}` };
5548
+ const err = data.error || { code: "http_error", message: `Request failed with status ${response.status}` };
5020
5549
  return { ...data, success: false, ok: false, error: err };
5021
5550
  }
5022
5551
  return data;
5023
5552
  } catch (error) {
5024
5553
  if (error instanceof Error && error.name === "AbortError") {
5025
- return { success: false, ok: false, error: { code: "TIMEOUT", message: "Request timed out" } };
5554
+ return { success: false, ok: false, error: { code: "timeout", message: "Request timed out" } };
5026
5555
  }
5027
5556
  return {
5028
5557
  success: false,
5029
5558
  ok: false,
5030
- error: { code: "NETWORK_ERROR", message: error instanceof Error ? error.message : "Unknown error" }
5559
+ error: { code: "cloud_unreachable", message: error instanceof Error ? error.message : "Unknown error" }
5031
5560
  };
5032
5561
  } finally {
5033
5562
  clearTimeout(timeoutId);
@@ -5116,6 +5645,7 @@ export {
5116
5645
  EvolutionCache,
5117
5646
  EvolutionClient,
5118
5647
  EvolutionRuntime,
5648
+ EvolutionSkillsClient,
5119
5649
  FilesClient,
5120
5650
  GroupsClient,
5121
5651
  IMClient,
@@ -5135,6 +5665,7 @@ export {
5135
5665
  SecurityClient,
5136
5666
  TabCoordinator,
5137
5667
  TasksClient,
5668
+ WORKSPACE_ASSETS_ROUTE,
5138
5669
  WorkspaceClient,
5139
5670
  WorkspaceFilesClient,
5140
5671
  WorkspacesClient,
@@ -5150,5 +5681,7 @@ export {
5150
5681
  encryptForSend,
5151
5682
  extractSignals,
5152
5683
  guessMimeType,
5153
- safeSlug
5684
+ readCardKanbanMetadata,
5685
+ safeSlug,
5686
+ writeCardKanbanMetadata
5154
5687
  };