@penclipai/plugin-sdk 2026.508.2 → 2026.511.0-canary.1

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/testing.js CHANGED
@@ -210,6 +210,8 @@ export function createTestHarness(options) {
210
210
  const agents = new Map();
211
211
  const goals = new Map();
212
212
  const projectWorkspaces = new Map();
213
+ const localFolderStatuses = new Map();
214
+ const localFolderFiles = new Map();
213
215
  const sessions = new Map();
214
216
  const sessionEventCallbacks = new Map();
215
217
  const events = [];
@@ -218,6 +220,41 @@ export function createTestHarness(options) {
218
220
  const dataHandlers = new Map();
219
221
  const actionHandlers = new Map();
220
222
  const toolHandlers = new Map();
223
+ function localFolderKey(companyId, folderKey) {
224
+ return `${companyId}:${folderKey}`;
225
+ }
226
+ function localFolderFileKey(companyId, folderKey, relativePath) {
227
+ return `${localFolderKey(companyId, folderKey)}:${relativePath}`;
228
+ }
229
+ function normalizeLocalFolderRelativePath(relativePath) {
230
+ const parts = [];
231
+ for (const segment of relativePath.split(/[\\/]+/)) {
232
+ if (!segment || segment === ".")
233
+ continue;
234
+ if (segment === "..")
235
+ throw new Error("Local folder path traversal is not allowed");
236
+ parts.push(segment);
237
+ }
238
+ return parts.join("/");
239
+ }
240
+ function notConfiguredLocalFolderStatus(folderKey) {
241
+ return {
242
+ folderKey,
243
+ configured: false,
244
+ path: null,
245
+ realPath: null,
246
+ access: "readWrite",
247
+ readable: false,
248
+ writable: false,
249
+ requiredDirectories: [],
250
+ requiredFiles: [],
251
+ missingDirectories: [],
252
+ missingFiles: [],
253
+ healthy: false,
254
+ problems: [{ code: "not_configured", message: "No local folder path is configured." }],
255
+ checkedAt: new Date().toISOString(),
256
+ };
257
+ }
221
258
  function issueRelationSummary(issueId) {
222
259
  const issue = issues.get(issueId);
223
260
  if (!issue)
@@ -306,7 +343,7 @@ export function createTestHarness(options) {
306
343
  },
307
344
  async configure(input) {
308
345
  requireCapability(manifest, capabilitySet, "local.folders");
309
- return {
346
+ const status = {
310
347
  folderKey: input.folderKey,
311
348
  configured: true,
312
349
  path: input.path,
@@ -322,57 +359,102 @@ export function createTestHarness(options) {
322
359
  problems: [],
323
360
  checkedAt: new Date().toISOString(),
324
361
  };
362
+ localFolderStatuses.set(localFolderKey(input.companyId, input.folderKey), status);
363
+ return status;
325
364
  },
326
- async status(_companyId, folderKey) {
365
+ async status(companyId, folderKey) {
327
366
  requireCapability(manifest, capabilitySet, "local.folders");
328
- return {
329
- folderKey,
330
- configured: false,
331
- path: null,
332
- realPath: null,
333
- access: "readWrite",
334
- readable: false,
335
- writable: false,
336
- requiredDirectories: [],
337
- requiredFiles: [],
338
- missingDirectories: [],
339
- missingFiles: [],
340
- healthy: false,
341
- problems: [{ code: "not_configured", message: "No local folder path is configured." }],
342
- checkedAt: new Date().toISOString(),
343
- };
367
+ return localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? notConfiguredLocalFolderStatus(folderKey);
344
368
  },
345
- async list(_companyId, folderKey, options) {
369
+ async list(companyId, folderKey, options) {
346
370
  requireCapability(manifest, capabilitySet, "local.folders");
371
+ const status = localFolderStatuses.get(localFolderKey(companyId, folderKey));
372
+ if (!status?.configured)
373
+ throw new Error("Local folder is not configured");
374
+ const prefix = normalizeLocalFolderRelativePath(options?.relativePath ?? "");
375
+ const prefixWithSlash = prefix ? `${prefix}/` : "";
376
+ const entries = new Map();
377
+ for (const [key, contents] of localFolderFiles) {
378
+ const filePrefix = `${localFolderKey(companyId, folderKey)}:`;
379
+ if (!key.startsWith(filePrefix))
380
+ continue;
381
+ const filePath = key.slice(filePrefix.length);
382
+ if (prefix && filePath !== prefix && !filePath.startsWith(prefixWithSlash))
383
+ continue;
384
+ const remainder = prefix ? filePath.slice(prefixWithSlash.length) : filePath;
385
+ const [name] = remainder.split("/");
386
+ if (!name)
387
+ continue;
388
+ const entryPath = prefix ? `${prefix}/${name}` : name;
389
+ const isNested = remainder.includes("/");
390
+ if (!options?.recursive && isNested) {
391
+ entries.set(entryPath, {
392
+ path: entryPath,
393
+ name,
394
+ kind: "directory",
395
+ size: null,
396
+ modifiedAt: null,
397
+ });
398
+ continue;
399
+ }
400
+ entries.set(filePath, {
401
+ path: filePath,
402
+ name: filePath.split("/").pop() ?? filePath,
403
+ kind: "file",
404
+ size: Buffer.byteLength(contents, "utf8"),
405
+ modifiedAt: null,
406
+ });
407
+ }
408
+ const maxEntries = options?.maxEntries && options.maxEntries > 0 ? options.maxEntries : entries.size;
409
+ const allEntries = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
347
410
  return {
348
411
  folderKey,
349
412
  relativePath: options?.relativePath ?? null,
350
- entries: [],
351
- truncated: false,
413
+ entries: allEntries.slice(0, maxEntries),
414
+ truncated: allEntries.length > maxEntries,
352
415
  };
353
416
  },
354
- async readText() {
417
+ async readText(companyId, folderKey, relativePath) {
355
418
  requireCapability(manifest, capabilitySet, "local.folders");
356
- throw new Error("Test harness local folder readText is not implemented");
419
+ const normalizedPath = normalizeLocalFolderRelativePath(relativePath);
420
+ const contents = localFolderFiles.get(localFolderFileKey(companyId, folderKey, normalizedPath));
421
+ if (contents === undefined)
422
+ throw new Error(`Local folder file not found: ${relativePath}`);
423
+ return contents;
357
424
  },
358
- async writeTextAtomic(_companyId, folderKey) {
425
+ async writeTextAtomic(companyId, folderKey, relativePath, contents) {
359
426
  requireCapability(manifest, capabilitySet, "local.folders");
360
- return {
427
+ const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? {
361
428
  folderKey,
362
- configured: false,
363
- path: null,
364
- realPath: null,
429
+ configured: true,
430
+ path: `memory://${manifest.id}/${companyId}/${folderKey}`,
431
+ realPath: `memory://${manifest.id}/${companyId}/${folderKey}`,
365
432
  access: "readWrite",
366
- readable: false,
367
- writable: false,
433
+ readable: true,
434
+ writable: true,
368
435
  requiredDirectories: [],
369
436
  requiredFiles: [],
370
437
  missingDirectories: [],
371
438
  missingFiles: [],
372
- healthy: false,
373
- problems: [{ code: "not_configured", message: "No local folder path is configured." }],
439
+ healthy: true,
440
+ problems: [],
374
441
  checkedAt: new Date().toISOString(),
375
442
  };
443
+ if (status.access !== "readWrite" || !status.writable) {
444
+ throw new Error("Local folder is not configured for writes");
445
+ }
446
+ localFolderStatuses.set(localFolderKey(companyId, folderKey), status);
447
+ localFolderFiles.set(localFolderFileKey(companyId, folderKey, normalizeLocalFolderRelativePath(relativePath)), contents);
448
+ return status;
449
+ },
450
+ async deleteFile(companyId, folderKey, relativePath) {
451
+ requireCapability(manifest, capabilitySet, "local.folders");
452
+ const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? notConfiguredLocalFolderStatus(folderKey);
453
+ if (status.configured && (status.access !== "readWrite" || !status.writable)) {
454
+ throw new Error("Local folder is not configured for writes");
455
+ }
456
+ localFolderFiles.delete(localFolderFileKey(companyId, folderKey, normalizeLocalFolderRelativePath(relativePath)));
457
+ return status;
376
458
  },
377
459
  },
378
460
  events: {
@@ -771,14 +853,14 @@ export function createTestHarness(options) {
771
853
  concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active",
772
854
  catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed",
773
855
  variables: declaration.variables ?? [],
856
+ latestRevisionId: null,
857
+ latestRevisionNumber: 1,
774
858
  createdByAgentId: null,
775
859
  createdByUserId: null,
776
860
  updatedByAgentId: null,
777
861
  updatedByUserId: null,
778
862
  lastTriggeredAt: null,
779
863
  lastEnqueuedAt: null,
780
- latestRevisionId: null,
781
- latestRevisionNumber: 1,
782
864
  createdAt: now,
783
865
  updatedAt: now,
784
866
  managedByPlugin: {
@@ -869,6 +951,173 @@ export function createTestHarness(options) {
869
951
  },
870
952
  },
871
953
  },
954
+ skills: {
955
+ managed: {
956
+ async get(skillKey, companyId) {
957
+ requireCapability(manifest, capabilitySet, "skills.managed");
958
+ const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey);
959
+ if (!declaration) {
960
+ return {
961
+ pluginKey: manifest.id,
962
+ resourceKind: "skill",
963
+ resourceKey: skillKey,
964
+ companyId,
965
+ skillId: null,
966
+ skill: null,
967
+ status: "missing",
968
+ defaultDrift: null,
969
+ };
970
+ }
971
+ const externalId = `${manifest.id}:skill:${skillKey}`;
972
+ const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource"
973
+ && entity.scopeKind === "company"
974
+ && entity.scopeId === companyId
975
+ && entity.externalId === externalId);
976
+ const existingSkill = existingEntity?.data?.skill;
977
+ if (existingSkill && existingSkill.companyId === companyId) {
978
+ return {
979
+ pluginKey: manifest.id,
980
+ resourceKind: "skill",
981
+ resourceKey: skillKey,
982
+ companyId,
983
+ skillId: existingSkill.id,
984
+ skill: existingSkill,
985
+ status: "resolved",
986
+ defaultDrift: null,
987
+ };
988
+ }
989
+ return {
990
+ pluginKey: manifest.id,
991
+ resourceKind: "skill",
992
+ resourceKey: skillKey,
993
+ companyId,
994
+ skillId: null,
995
+ skill: null,
996
+ status: "missing",
997
+ defaultDrift: null,
998
+ };
999
+ },
1000
+ async reconcile(skillKey, companyId) {
1001
+ const existing = await this.get(skillKey, companyId);
1002
+ if (existing.skill)
1003
+ return existing;
1004
+ const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey);
1005
+ if (!declaration)
1006
+ return existing;
1007
+ const now = new Date();
1008
+ const skill = {
1009
+ id: randomUUID(),
1010
+ companyId,
1011
+ key: `plugin/${manifest.id.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}/${skillKey}`,
1012
+ slug: declaration.slug ?? skillKey,
1013
+ name: declaration.displayName,
1014
+ description: declaration.description ?? null,
1015
+ markdown: declaration.markdown ?? `# ${declaration.displayName}\n`,
1016
+ sourceType: "catalog",
1017
+ sourceLocator: null,
1018
+ sourceRef: null,
1019
+ trustLevel: "markdown_only",
1020
+ compatibility: "compatible",
1021
+ fileInventory: [{ path: "SKILL.md", kind: "skill" }],
1022
+ metadata: {
1023
+ sourceKind: "catalog",
1024
+ pluginManagedResource: {
1025
+ pluginKey: manifest.id,
1026
+ resourceKind: "skill",
1027
+ resourceKey: skillKey,
1028
+ },
1029
+ },
1030
+ createdAt: now,
1031
+ updatedAt: now,
1032
+ };
1033
+ const nowIso = now.toISOString();
1034
+ const record = {
1035
+ id: randomUUID(),
1036
+ entityType: "managed_resource",
1037
+ scopeKind: "company",
1038
+ scopeId: companyId,
1039
+ externalId: `${manifest.id}:skill:${skillKey}`,
1040
+ title: declaration.displayName,
1041
+ status: null,
1042
+ data: { resourceKind: "skill", resourceKey: skillKey, skillId: skill.id, skill },
1043
+ createdAt: nowIso,
1044
+ updatedAt: nowIso,
1045
+ };
1046
+ entities.set(record.id, record);
1047
+ return {
1048
+ pluginKey: manifest.id,
1049
+ resourceKind: "skill",
1050
+ resourceKey: skillKey,
1051
+ companyId,
1052
+ skillId: skill.id,
1053
+ skill,
1054
+ status: "created",
1055
+ defaultDrift: null,
1056
+ };
1057
+ },
1058
+ async reset(skillKey, companyId) {
1059
+ requireCapability(manifest, capabilitySet, "skills.managed");
1060
+ const existing = await this.get(skillKey, companyId);
1061
+ const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey);
1062
+ if (!declaration)
1063
+ return existing;
1064
+ const now = new Date();
1065
+ const skill = {
1066
+ id: existing.skill?.id ?? randomUUID(),
1067
+ companyId,
1068
+ key: `plugin/${manifest.id.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}/${skillKey}`,
1069
+ slug: declaration.slug ?? skillKey,
1070
+ name: declaration.displayName,
1071
+ description: declaration.description ?? null,
1072
+ markdown: declaration.markdown ?? `# ${declaration.displayName}\n`,
1073
+ sourceType: "catalog",
1074
+ sourceLocator: null,
1075
+ sourceRef: null,
1076
+ trustLevel: "markdown_only",
1077
+ compatibility: "compatible",
1078
+ fileInventory: [{ path: "SKILL.md", kind: "skill" }],
1079
+ metadata: {
1080
+ sourceKind: "catalog",
1081
+ pluginManagedResource: {
1082
+ pluginKey: manifest.id,
1083
+ resourceKind: "skill",
1084
+ resourceKey: skillKey,
1085
+ },
1086
+ },
1087
+ createdAt: existing.skill?.createdAt ?? now,
1088
+ updatedAt: now,
1089
+ };
1090
+ const nowIso = now.toISOString();
1091
+ const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource" &&
1092
+ entity.scopeKind === "company" &&
1093
+ entity.scopeId === companyId &&
1094
+ entity.externalId === `${manifest.id}:skill:${skillKey}`);
1095
+ const record = {
1096
+ id: existingEntity?.id ?? randomUUID(),
1097
+ entityType: "managed_resource",
1098
+ scopeKind: "company",
1099
+ scopeId: companyId,
1100
+ externalId: `${manifest.id}:skill:${skillKey}`,
1101
+ title: declaration.displayName,
1102
+ status: null,
1103
+ data: { resourceKind: "skill", resourceKey: skillKey, skillId: skill.id, skill },
1104
+ createdAt: existingEntity?.createdAt ?? nowIso,
1105
+ updatedAt: nowIso,
1106
+ };
1107
+ entities.set(record.id, record);
1108
+ return {
1109
+ pluginKey: manifest.id,
1110
+ resourceKind: "skill",
1111
+ resourceKey: skillKey,
1112
+ companyId,
1113
+ skillId: skill.id,
1114
+ skill,
1115
+ status: "reset",
1116
+ defaultDrift: null,
1117
+ };
1118
+ },
1119
+ },
1120
+ },
872
1121
  companies: {
873
1122
  async list(input) {
874
1123
  requireCapability(manifest, capabilitySet, "companies.read");
@@ -934,7 +1183,7 @@ export function createTestHarness(options) {
934
1183
  title: input.title,
935
1184
  description: input.description ?? null,
936
1185
  status: input.status ?? "todo",
937
- workMode: input.workMode ?? "standard",
1186
+ workMode: "standard",
938
1187
  priority: input.priority ?? "medium",
939
1188
  assigneeAgentId: input.assigneeAgentId ?? null,
940
1189
  assigneeUserId: input.assigneeUserId ?? null,
@@ -951,7 +1200,7 @@ export function createTestHarness(options) {
951
1200
  originRunId: input.originRunId ?? null,
952
1201
  requestDepth: input.requestDepth ?? 0,
953
1202
  billingCode: input.billingCode ?? null,
954
- assigneeAdapterOverrides: null,
1203
+ assigneeAdapterOverrides: input.assigneeAdapterOverrides ?? null,
955
1204
  executionWorkspaceId: input.executionWorkspaceId ?? null,
956
1205
  executionWorkspacePreference: input.executionWorkspacePreference ?? null,
957
1206
  executionWorkspaceSettings: input.executionWorkspaceSettings ?? null,