@penclipai/plugin-sdk 2026.508.2 → 2026.511.0-canary.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/host-client-factory.d.ts +7 -0
- package/dist/host-client-factory.d.ts.map +1 -1
- package/dist/host-client-factory.js +17 -0
- package/dist/host-client-factory.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +31 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +285 -36
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +20 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-rpc-host.d.ts.map +1 -1
- package/dist/worker-rpc-host.js +17 -1
- package/dist/worker-rpc-host.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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(
|
|
425
|
+
async writeTextAtomic(companyId, folderKey, relativePath, contents) {
|
|
359
426
|
requireCapability(manifest, capabilitySet, "local.folders");
|
|
360
|
-
|
|
427
|
+
const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? {
|
|
361
428
|
folderKey,
|
|
362
|
-
configured:
|
|
363
|
-
path:
|
|
364
|
-
realPath:
|
|
429
|
+
configured: true,
|
|
430
|
+
path: `memory://${manifest.id}/${companyId}/${folderKey}`,
|
|
431
|
+
realPath: `memory://${manifest.id}/${companyId}/${folderKey}`,
|
|
365
432
|
access: "readWrite",
|
|
366
|
-
readable:
|
|
367
|
-
writable:
|
|
433
|
+
readable: true,
|
|
434
|
+
writable: true,
|
|
368
435
|
requiredDirectories: [],
|
|
369
436
|
requiredFiles: [],
|
|
370
437
|
missingDirectories: [],
|
|
371
438
|
missingFiles: [],
|
|
372
|
-
healthy:
|
|
373
|
-
problems: [
|
|
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:
|
|
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,
|