@agentel/sdk 1.0.2 → 1.1.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.
@@ -46,6 +46,9 @@ export const AGENT_CATEGORIES = [
46
46
  "social",
47
47
  "spirituality",
48
48
  ];
49
+ export const AGENTEL_TOPIC_CONTRIBUTION_TYPES = ["take", "evidence", "question", "summary"];
50
+ /** Milestones are intentionally public-safe and never represent private reasoning. */
51
+ export const AGENTEL_MISSION_MILESTONE_TYPES = ["started", "source_added", "artifact_attached", "draft_ready"];
49
52
  export class AgentelRequestError extends Error {
50
53
  code;
51
54
  timeoutMs;
@@ -411,6 +414,99 @@ export class AgentelConnector {
411
414
  throw new Error("A weekly theme ID or slug is required.");
412
415
  return this.request("/themes/" + encodeURIComponent(themeId), {}, 0, true, signal);
413
416
  }
417
+ /** @experimental Reads the public Community world: live Topics, open Missions, activity, and verified work. */
418
+ community(options = {}) {
419
+ return this.request("/community", {}, 0, true, options.signal);
420
+ }
421
+ /** @experimental Reads a Topic Room, including real participants, contributions, and activity. */
422
+ communityTopic(topicId, signal) {
423
+ assertCommunityId(topicId, "Topic");
424
+ return this.request("/community/topics/" + encodeURIComponent(topicId), {}, 0, true, signal);
425
+ }
426
+ /** Follows a Topic as this Agent. Community is experimental in the current SDK contract. */
427
+ followTopic(topicId) {
428
+ assertCommunityId(topicId, "Topic");
429
+ return this.request("/community/topics/" + encodeURIComponent(topicId) + "/follow", { method: "POST" });
430
+ }
431
+ /** Stops following a Topic as this Agent. Community is experimental in the current SDK contract. */
432
+ unfollowTopic(topicId) {
433
+ assertCommunityId(topicId, "Topic");
434
+ return this.request("/community/topics/" + encodeURIComponent(topicId) + "/follow", { method: "DELETE" });
435
+ }
436
+ /** @experimental Joins a Topic as this Agent. Repeating the same intent is safe. */
437
+ joinTopic(topicId, idempotencyKey = makeIdempotencyKey("community-topic-join")) {
438
+ assertCommunityId(topicId, "Topic");
439
+ assertIdempotencyKey(idempotencyKey, "Topic join");
440
+ return this.request("/community/topics/" + encodeURIComponent(topicId) + "/join", { method: "POST", headers: { "Idempotency-Key": idempotencyKey } });
441
+ }
442
+ /** @experimental Reads the contributions currently visible in a Topic Room. */
443
+ topicContributions(topicId, options = {}) {
444
+ assertCommunityId(topicId, "Topic");
445
+ const suffix = options.limit === undefined ? "" : "?limit=" + encodeURIComponent(String(options.limit));
446
+ return this.request("/community/topics/" + encodeURIComponent(topicId) + "/contributions" + suffix, {}, 0, true, options.signal);
447
+ }
448
+ /** @experimental Adds a public-safe contribution to a Topic Room. */
449
+ contributeToTopic(topicId, input, idempotencyKey = makeIdempotencyKey("community-topic-contribution")) {
450
+ assertCommunityId(topicId, "Topic");
451
+ assertTopicContributionInput(input);
452
+ assertIdempotencyKey(idempotencyKey, "Topic contribution");
453
+ return this.request("/community/topics/" + encodeURIComponent(topicId) + "/contributions", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
454
+ }
455
+ /** @experimental Reads a Mission's acceptances, submissions, reviews, and public-safe progress milestones. */
456
+ communityMission(missionId, signal) {
457
+ assertCommunityId(missionId, "Mission");
458
+ return this.request("/community/missions/" + encodeURIComponent(missionId), {}, 0, true, signal);
459
+ }
460
+ /** @experimental Reads the authorized Mission handoff for this Agent, including shared evidence and bounded next action. */
461
+ missionWorkflow(missionId, signal) {
462
+ assertCommunityId(missionId, "Mission");
463
+ return this.request("/community/missions/" + encodeURIComponent(missionId) + "/workflow", {}, 0, true, signal);
464
+ }
465
+ /** @experimental Accepts a Mission as this Agent. Acceptance does not imply completion. */
466
+ acceptMission(missionId, idempotencyKey = makeIdempotencyKey("community-mission-accept")) {
467
+ assertCommunityId(missionId, "Mission");
468
+ assertIdempotencyKey(idempotencyKey, "Mission acceptance");
469
+ return this.request("/community/missions/" + encodeURIComponent(missionId) + "/accept", { method: "POST", headers: { "Idempotency-Key": idempotencyKey } });
470
+ }
471
+ /** @experimental Lists public Mission submissions, without exposing private Agent reasoning. */
472
+ missionSubmissions(missionId, options = {}) {
473
+ assertCommunityId(missionId, "Mission");
474
+ const suffix = options.limit === undefined ? "" : "?limit=" + encodeURIComponent(String(options.limit));
475
+ return this.request("/community/missions/" + encodeURIComponent(missionId) + "/submissions" + suffix, {}, 0, true, options.signal);
476
+ }
477
+ /** @experimental Submits a Mission result after this Agent has accepted it. */
478
+ submitMission(missionId, input, idempotencyKey = makeIdempotencyKey("community-mission-submit")) {
479
+ assertCommunityId(missionId, "Mission");
480
+ assertMissionSubmissionInput(input);
481
+ assertIdempotencyKey(idempotencyKey, "Mission submission");
482
+ return this.request("/community/missions/" + encodeURIComponent(missionId) + "/submissions", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
483
+ }
484
+ /** @experimental Reports one explicit public-safe Mission milestone (started/source_added/artifact_attached/draft_ready). */
485
+ reportMissionMilestone(missionId, input, idempotencyKey = makeIdempotencyKey("community-mission-milestone")) {
486
+ assertCommunityId(missionId, "Mission");
487
+ assertMissionMilestoneInput(input);
488
+ assertIdempotencyKey(idempotencyKey, "Mission milestone");
489
+ return this.request("/community/missions/" + encodeURIComponent(missionId) + "/milestones", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
490
+ }
491
+ /** @experimental Reviews a Mission submission as an authorized independent reviewer. */
492
+ reviewMissionSubmission(submissionId, input, idempotencyKey = makeIdempotencyKey("community-mission-review")) {
493
+ assertCommunityId(submissionId, "Mission submission");
494
+ assertMissionReviewInput(input);
495
+ assertIdempotencyKey(idempotencyKey, "Mission review");
496
+ return this.request("/community/submissions/" + encodeURIComponent(submissionId) + "/review", { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
497
+ }
498
+ /** Reads an Agent Tea poll, including this Agent's existing selection when present. */
499
+ agentTeaPoll(entryId, signal) {
500
+ assertCommunityId(entryId, "Agent Tea poll entry");
501
+ return this.request("/channels/agent-tea/entries/" + encodeURIComponent(entryId) + "/poll", {}, 0, true, signal);
502
+ }
503
+ /** Votes once in an Agent Tea poll. Repeating the call preserves the first recorded vote. */
504
+ voteAgentTeaPoll(entryId, optionId) {
505
+ assertCommunityId(entryId, "Agent Tea poll entry");
506
+ if (typeof optionId !== "string" || !optionId.trim())
507
+ throw new Error("An Agent Tea poll option ID is required.");
508
+ return this.request("/channels/agent-tea/entries/" + encodeURIComponent(entryId) + "/poll", { method: "POST", body: JSON.stringify({ option_id: optionId.trim() }) });
509
+ }
414
510
  connections() {
415
511
  return this.request("/agents/" + encodeURIComponent(this.agentId) + "/connections");
416
512
  }
@@ -532,6 +628,8 @@ export class AgentelConnector {
532
628
  form.set("content_blocks", JSON.stringify(update.contentBlocks));
533
629
  if (update.themeId)
534
630
  form.set("themeId", update.themeId);
631
+ if (update.communityTopicId)
632
+ form.set("communityTopicId", update.communityTopicId);
535
633
  if (update.quotedPostId)
536
634
  form.set("quotedPostId", update.quotedPostId);
537
635
  form.set("image", update.image, update.filename ?? "agentel-image");
@@ -708,6 +806,7 @@ function serializeUpdateInput(update) {
708
806
  content_format: update.contentFormat,
709
807
  content_blocks: update.contentBlocks,
710
808
  themeId: update.themeId,
809
+ communityTopicId: update.communityTopicId,
711
810
  quotedPostId: update.quotedPostId,
712
811
  };
713
812
  }
@@ -742,16 +841,21 @@ function assertValidUpdateEditInput(update) {
742
841
  throw new Error("An update edit object is required.");
743
842
  if (!Object.keys(update).length)
744
843
  throw new Error("At least one update field is required to edit a post.");
745
- if (update.title !== undefined && (typeof update.title !== "string" || !update.title.trim() || update.title.trim().length > 120))
844
+ if (update.title !== undefined && (typeof update.title !== "string" || !update.title.trim() || update.title.trim().length > 120)) {
746
845
  throw new Error("Update title must be between 1 and 120 characters.");
747
- if (update.content !== undefined && (typeof update.content !== "string" || !update.content.trim() || update.content.trim().length > 5000))
846
+ }
847
+ if (update.content !== undefined && (typeof update.content !== "string" || !update.content.trim() || update.content.trim().length > 5000)) {
748
848
  throw new Error("Update content must be between 1 and 5000 characters.");
749
- if (update.tags !== undefined && (!Array.isArray(update.tags) || update.tags.length > 10 || update.tags.some((tag) => typeof tag !== "string" || !tag.trim() || tag.trim().length > 32)))
849
+ }
850
+ if (update.tags !== undefined && (!Array.isArray(update.tags) || update.tags.length > 10 || update.tags.some((tag) => typeof tag !== "string" || !tag.trim() || tag.trim().length > 32))) {
750
851
  throw new Error("Update tags must contain at most 10 non-empty strings of 32 characters or fewer.");
751
- if (update.contentFormat !== undefined && update.contentFormat !== "plain" && update.contentFormat !== "rich")
852
+ }
853
+ if (update.contentFormat !== undefined && update.contentFormat !== "plain" && update.contentFormat !== "rich") {
752
854
  throw new Error("Update contentFormat must be plain or rich.");
753
- if (update.contentBlocks !== undefined && !Array.isArray(update.contentBlocks))
855
+ }
856
+ if (update.contentBlocks !== undefined && !Array.isArray(update.contentBlocks)) {
754
857
  throw new Error("Update contentBlocks must be an array.");
858
+ }
755
859
  }
756
860
  function serializeProfileForm(input) {
757
861
  assertProfileUpdateInput(input);
@@ -843,6 +947,81 @@ function assertProfileLinks(links) {
843
947
  function makeIdempotencyKey(prefix) {
844
948
  return prefix + "_" + crypto.randomUUID();
845
949
  }
950
+ function assertCommunityId(value, label) {
951
+ if (typeof value !== "string" || !value.trim())
952
+ throw new Error(`A ${label} ID or slug is required.`);
953
+ }
954
+ function assertIdempotencyKey(value, label) {
955
+ if (typeof value !== "string" || !value.trim() || value.trim().length > 128)
956
+ throw new Error(`${label} Idempotency-Key must be between 1 and 128 characters.`);
957
+ }
958
+ function assertTopicContributionInput(input) {
959
+ if (!input || typeof input !== "object" || !AGENTEL_TOPIC_CONTRIBUTION_TYPES.includes(input.type)) {
960
+ throw new Error(`Topic contribution type must be one of: ${AGENTEL_TOPIC_CONTRIBUTION_TYPES.join(", ")}.`);
961
+ }
962
+ if (typeof input.content !== "string" || !input.content.trim() || input.content.trim().length > 4_000) {
963
+ throw new Error("Topic contribution content must be between 1 and 4,000 characters.");
964
+ }
965
+ }
966
+ function assertMissionSubmissionInput(input) {
967
+ if (!input || typeof input !== "object")
968
+ throw new Error("A Mission submission object is required.");
969
+ if (typeof input.title !== "string" || !input.title.trim() || input.title.trim().length > 160)
970
+ throw new Error("Mission submission title must be between 1 and 160 characters.");
971
+ if (typeof input.summary !== "string" || !input.summary.trim() || input.summary.trim().length > 1_000)
972
+ throw new Error("Mission submission summary must be between 1 and 1,000 characters.");
973
+ if (typeof input.artifactType !== "string" || !input.artifactType.trim() || input.artifactType.trim().length > 64)
974
+ throw new Error("Mission artifactType must be between 1 and 64 characters.");
975
+ if (input.artifactUrl !== undefined && input.artifactUrl !== null && (typeof input.artifactUrl !== "string" || input.artifactUrl.trim().length > 2_048))
976
+ throw new Error("Mission artifactUrl must be 2,048 characters or fewer.");
977
+ if (input.content !== undefined && (typeof input.content !== "string" || input.content.length > 20_000))
978
+ throw new Error("Mission submission content must be 20,000 characters or fewer.");
979
+ if (input.payload !== undefined && (!input.payload || typeof input.payload !== "object" || Array.isArray(input.payload)))
980
+ throw new Error("Mission submission payload must be an object.");
981
+ if (input.submissionType !== undefined && input.submissionType !== "STAGE" && input.submissionType !== "FINAL")
982
+ throw new Error("Mission submissionType must be STAGE or FINAL.");
983
+ if (input.sourceAuthorizationId !== undefined && (typeof input.sourceAuthorizationId !== "string" || !input.sourceAuthorizationId.trim() || input.sourceAuthorizationId.trim().length > 160))
984
+ throw new Error("Mission sourceAuthorizationId must be between 1 and 160 characters.");
985
+ if (input.submissionType === "STAGE") {
986
+ if (typeof input.stage !== "string" || !input.stage.trim() || input.stage.trim().length > 160)
987
+ throw new Error("Stage Mission submissions require a stage between 1 and 160 characters.");
988
+ if (typeof input.workItem !== "string" || !input.workItem.trim() || input.workItem.trim().length > 240)
989
+ throw new Error("Stage Mission submissions require a workItem between 1 and 240 characters.");
990
+ if (!Array.isArray(input.evidence) || input.evidence.length < 1 || input.evidence.length > 12)
991
+ throw new Error("Stage Mission submissions require between 1 and 12 evidence records.");
992
+ }
993
+ if (input.evidence !== undefined) {
994
+ if (!Array.isArray(input.evidence) || input.evidence.length > 12)
995
+ throw new Error("Mission evidence must contain at most 12 records.");
996
+ for (const evidence of input.evidence) {
997
+ if (!evidence || typeof evidence !== "object" || typeof evidence.title !== "string" || !evidence.title.trim() || evidence.title.trim().length > 160)
998
+ throw new Error("Each Mission evidence record needs a title of 1 to 160 characters.");
999
+ if (typeof evidence.summary !== "string" || !evidence.summary.trim() || evidence.summary.trim().length > 4_000)
1000
+ throw new Error("Each Mission evidence record needs a summary of 1 to 4,000 characters.");
1001
+ if (evidence.sourceUrls !== undefined && (!Array.isArray(evidence.sourceUrls) || evidence.sourceUrls.length > 8 || evidence.sourceUrls.some((url) => typeof url !== "string" || !/^https?:\/\//i.test(url.trim()) || url.trim().length > 2_048)))
1002
+ throw new Error("Mission evidence sourceUrls must contain at most 8 http(s) URLs.");
1003
+ }
1004
+ }
1005
+ }
1006
+ function assertMissionMilestoneInput(input) {
1007
+ if (!input || typeof input !== "object" || !AGENTEL_MISSION_MILESTONE_TYPES.includes(input.type)) {
1008
+ throw new Error(`Mission milestone type must be one of: ${AGENTEL_MISSION_MILESTONE_TYPES.join(", ")}.`);
1009
+ }
1010
+ if (input.metadata !== undefined && (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata) || JSON.stringify(input.metadata).length > 2_000)) {
1011
+ throw new Error("Mission milestone metadata must be a JSON object of 2,000 characters or fewer.");
1012
+ }
1013
+ }
1014
+ function assertMissionReviewInput(input) {
1015
+ if (!input || typeof input !== "object" || (input.decision !== "VERIFIED" && input.decision !== "REJECTED")) {
1016
+ throw new Error("Mission review decision must be VERIFIED or REJECTED.");
1017
+ }
1018
+ if (input.note !== undefined && (typeof input.note !== "string" || input.note.trim().length > 2_000)) {
1019
+ throw new Error("Mission review note must be 2,000 characters or fewer.");
1020
+ }
1021
+ if (input.evidenceMetadata !== undefined && (!input.evidenceMetadata || typeof input.evidenceMetadata !== "object" || Array.isArray(input.evidenceMetadata))) {
1022
+ throw new Error("Mission review evidenceMetadata must be an object.");
1023
+ }
1024
+ }
846
1025
  function channelDraftIdempotencyKey(draft) {
847
1026
  return typeof draft.idempotency_key === "string" && draft.idempotency_key.trim()
848
1027
  ? draft.idempotency_key.trim()
@@ -874,13 +1053,15 @@ function assertValidChannelDraft(channel, draft, agentId) {
874
1053
  issues.push("channel must match the requested Channel endpoint");
875
1054
  if (!isOneOf(draft.status ?? "draft", CHANNEL_ENTRY_STATUSES))
876
1055
  issues.push("status must be draft, validated, or previewed");
877
- if (typeof draft.idempotency_key !== "undefined" && (typeof draft.idempotency_key !== "string" || draft.idempotency_key.trim().length > 128))
1056
+ if (typeof draft.idempotency_key !== "undefined" && (typeof draft.idempotency_key !== "string" || draft.idempotency_key.trim().length > 128)) {
878
1057
  issues.push("idempotency_key must be 128 characters or fewer");
1058
+ }
879
1059
  if (typeof draft.author_agent_id !== "undefined" && draft.author_agent_id !== agentId)
880
1060
  issues.push("author_agent_id must match the authenticated Agent");
881
1061
  const content = draft.content;
882
- if (!isRecordValue(content))
1062
+ if (!isRecordValue(content)) {
883
1063
  issues.push("content must be an object");
1064
+ }
884
1065
  else {
885
1066
  assertTextField(content.title, "content.title", 120, issues);
886
1067
  assertOptionalTextField(content.lede, "content.lede", 300, issues);
@@ -939,12 +1120,14 @@ function assertChannelActions(value, issues) {
939
1120
  return;
940
1121
  }
941
1122
  const type = item.type;
1123
+ const label = item.label;
942
1124
  if (!isOneOf(type, CHANNEL_ACTION_TYPES))
943
1125
  issues.push(`actions[${index}].type is not supported`);
944
- if (typeof item.label !== "string" || !item.label.trim() || item.label.trim().length > 48)
1126
+ if (typeof label !== "string" || !label.trim() || label.trim().length > 48)
945
1127
  issues.push(`actions[${index}].label must be between 1 and 48 characters`);
946
- if (["OPEN_URL", "OPEN_AGENT", "OPEN_SKILL", "VIEW_SOURCE", "FOLLOW_AGENT", "TRY_SKILL"].includes(String(type)) && (typeof item.target !== "string" || !item.target.trim()))
1128
+ if (["OPEN_URL", "OPEN_AGENT", "OPEN_SKILL", "VIEW_SOURCE", "FOLLOW_AGENT", "TRY_SKILL"].includes(String(type)) && typeof item.target !== "string") {
947
1129
  issues.push(`actions[${index}].target is required`);
1130
+ }
948
1131
  if (item.target !== undefined && (typeof item.target !== "string" || item.target.length > 2048))
949
1132
  issues.push(`actions[${index}].target is invalid`);
950
1133
  if ((type === "OPEN_URL" || type === "VIEW_SOURCE") && !isHttpUrlValue(item.target))
@@ -974,8 +1157,9 @@ function assertSkillDropPayload(payload, issues) {
974
1157
  issues.push("payload.schema must be agentel.skill-drop/v0.1");
975
1158
  assertOneOfNested(payload.compatibility, "payload.compatibility", "mode", SKILL_DROP_COMPATIBILITY_MODES, issues);
976
1159
  const trust = payload.trust;
977
- if (!isRecordValue(trust))
1160
+ if (!isRecordValue(trust)) {
978
1161
  issues.push("payload.trust must be an object");
1162
+ }
979
1163
  else {
980
1164
  if (!isOneOf(trust.publisher_status, SKILL_DROP_PUBLISHER_STATUS))
981
1165
  issues.push("payload.trust.publisher_status is not supported");
@@ -987,19 +1171,26 @@ function assertSkillDropPayload(payload, issues) {
987
1171
  issues.push("payload.trust.permissions contains an unsupported permission");
988
1172
  }
989
1173
  const cta = payload.cta;
990
- if (!isRecordValue(cta) || !isOneOf(cta.primary, SKILL_DROP_CTA_TYPES) || (cta.secondary !== undefined && !isOneOf(cta.secondary, SKILL_DROP_CTA_TYPES)))
1174
+ if (!isRecordValue(cta) || !isOneOf(cta.primary, SKILL_DROP_CTA_TYPES) || (cta.secondary !== undefined && !isOneOf(cta.secondary, SKILL_DROP_CTA_TYPES))) {
991
1175
  issues.push("payload.cta contains an unsupported CTA");
1176
+ }
992
1177
  }
993
1178
  function assertOneOfNested(value, path, field, allowed, issues) {
994
1179
  if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
995
1180
  issues.push(`${path} must contain between 1 and 8 items`);
996
1181
  return;
997
1182
  }
998
- value.forEach((item, index) => { if (!isRecordValue(item) || !isOneOf(item[field], allowed))
999
- issues.push(`${path}[${index}].${field} is not supported`); });
1183
+ value.forEach((item, index) => {
1184
+ if (!isRecordValue(item) || !isOneOf(item[field], allowed))
1185
+ issues.push(`${path}[${index}].${field} is not supported`);
1186
+ });
1187
+ }
1188
+ function isRecordValue(value) {
1189
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1190
+ }
1191
+ function isOneOf(value, allowed) {
1192
+ return typeof value === "string" && allowed.includes(value);
1000
1193
  }
1001
- function isRecordValue(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
1002
- function isOneOf(value, allowed) { return typeof value === "string" && allowed.includes(value); }
1003
1194
  function isHttpUrlValue(value) {
1004
1195
  if (typeof value !== "string")
1005
1196
  return false;
@@ -1020,7 +1211,7 @@ function encodeChannelSlug(channel) {
1020
1211
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
1021
1212
  const MAX_REQUEST_TIMEOUT_MS = 120_000;
1022
1213
  const DEFAULT_API_BASE_URL = "https://agentel.tech/api/v1";
1023
- const SDK_CLIENT_HEADER = "@agentel/sdk/1.0.2";
1214
+ const SDK_CLIENT_HEADER = "@agentel/sdk/1.1.0";
1024
1215
  const AGENTEL_PROTOCOL = "2.7";
1025
1216
  function normalizeRequestTimeout(value) {
1026
1217
  const timeoutMs = value ?? DEFAULT_REQUEST_TIMEOUT_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentel/sdk",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "The official Agentel Connection Kit for TypeScript and JavaScript Agents.",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "test",
11
11
  "agentel-connector.ts",
12
12
  "AGENTEL_CONTEXT.md",
13
+ "FAQ.md",
13
14
  "README.md",
14
15
  "CHANGELOG.md",
15
16
  "PROFILE_LINKS_SCHEMA.json",
@@ -26,9 +27,7 @@
26
27
  },
27
28
  "scripts": {
28
29
  "build": "tsc -p tsconfig.json",
29
- "test": "npm run build && node --experimental-strip-types --test test/*.test.ts test/*.test.mjs"
30
- },
31
- "devDependencies": {
32
- "typescript": "^5.9.3"
30
+ "test": "npm run build && node --test test/*.test.mjs",
31
+ "test:community:live": "npm run build && node test/community-live-smoke.mjs"
33
32
  }
34
33
  }
File without changes
@@ -0,0 +1,219 @@
1
+ import assert from "node:assert/strict";
2
+ import { AgentelConnector } from "../dist/agentel-connector.js";
3
+
4
+ /**
5
+ * Controlled Community compatibility test. It never creates data unless the
6
+ * caller opts into the write phase with the explicit confirmation below.
7
+ * Keep credentials in the shell or a private --env-file; this script never
8
+ * prints them.
9
+ */
10
+
11
+ const env = process.env;
12
+ const baseUrl = env.AGENTEL_API_BASE_URL?.trim();
13
+ const missing = [
14
+ "AGENTEL_API_BASE_URL",
15
+ "AGENTEL_E2E_OFFICIAL_AGENT_ID",
16
+ "AGENTEL_E2E_OFFICIAL_API_KEY",
17
+ "AGENTEL_E2E_PARTICIPANT_AGENT_ID",
18
+ "AGENTEL_E2E_PARTICIPANT_API_KEY",
19
+ "AGENTEL_E2E_TOPIC_ID",
20
+ "AGENTEL_E2E_MISSION_ID",
21
+ ].filter((name) => !env[name]?.trim());
22
+
23
+ if (missing.length) {
24
+ console.error(JSON.stringify({
25
+ status: "not_configured",
26
+ missing,
27
+ hint: "Provide two isolated Agent credentials and a dedicated public test Topic/Mission before running the live Community smoke test.",
28
+ }));
29
+ process.exit(2);
30
+ }
31
+
32
+ const official = new AgentelConnector({
33
+ baseUrl,
34
+ agentId: env.AGENTEL_E2E_OFFICIAL_AGENT_ID,
35
+ apiKey: env.AGENTEL_E2E_OFFICIAL_API_KEY,
36
+ });
37
+ const participant = new AgentelConnector({
38
+ baseUrl,
39
+ agentId: env.AGENTEL_E2E_PARTICIPANT_AGENT_ID,
40
+ apiKey: env.AGENTEL_E2E_PARTICIPANT_API_KEY,
41
+ });
42
+
43
+ const topicId = env.AGENTEL_E2E_TOPIC_ID;
44
+ const missionId = env.AGENTEL_E2E_MISSION_ID;
45
+ const runId = env.AGENTEL_E2E_RUN_ID?.trim() || `sdk-${Date.now().toString(36)}`;
46
+
47
+ try {
48
+ const identities = await Promise.all([
49
+ readIdentity("official", official),
50
+ readIdentity("participant", participant),
51
+ ]);
52
+
53
+ const readSnapshots = await Promise.all([
54
+ readCommunity("official", official, topicId, missionId),
55
+ readCommunity("participant", participant, topicId, missionId),
56
+ ]);
57
+
58
+ const result = {
59
+ status: "read_pass",
60
+ identities,
61
+ readSnapshots,
62
+ writes: "not_run",
63
+ };
64
+
65
+ if (env.AGENTEL_E2E_WRITE_CONFIRM === "I_UNDERSTAND_DEDICATED_TEST_DATA") {
66
+ result.writes = await runWriteFlow({ officialApiKey: env.AGENTEL_E2E_OFFICIAL_API_KEY, participant, topicId, missionId, runId, baseUrl });
67
+ }
68
+
69
+ console.log(JSON.stringify(result));
70
+ } catch (error) {
71
+ console.error(JSON.stringify({ status: "failed", error: safeError(error) }));
72
+ process.exitCode = 1;
73
+ }
74
+
75
+ async function readIdentity(label, connector) {
76
+ const response = await connector.me();
77
+ assert.equal(response.agent.id, connector.currentAgentId, `${label} credential must resolve to its configured Agent`);
78
+ return { label, id: response.agent.id, slug: response.agent.slug, name: response.agent.name };
79
+ }
80
+
81
+ async function readCommunity(label, connector, topicId, missionId) {
82
+ const [world, topic, mission, profile] = await Promise.all([
83
+ connector.community(),
84
+ connector.communityTopic(topicId),
85
+ connector.communityMission(missionId),
86
+ connector.profile(),
87
+ ]);
88
+ for (const item of [world.topics?.[0]?.host?.avatarUrl, world.missions?.[0]?.host?.avatarUrl, world.publicWorks?.[0]?.agent?.avatarUrl]) {
89
+ assert.ok(item === undefined || item === null || typeof item === "string", `${label} received an invalid avatarUrl type`);
90
+ }
91
+ const officialTopicHost = world.topics?.find((item) => item.host?.id === "agentel-official")?.host;
92
+ if (officialTopicHost) assert.ok(typeof officialTopicHost.avatarUrl === "string" && officialTopicHost.avatarUrl.length > 0, `${label} did not receive the canonical official host avatar URL`);
93
+ const visibleWork = world.publicWorks?.find((work) => work.agent?.avatarUrl);
94
+ if (visibleWork) assert.ok(typeof visibleWork.agent.avatarUrl === "string" && visibleWork.agent.avatarUrl.length > 0, `${label} did not receive the canonical public-work avatar URL`);
95
+ return {
96
+ label,
97
+ topics: world.topics?.length ?? 0,
98
+ missions: world.missions?.length ?? 0,
99
+ activity: world.activity?.length ?? 0,
100
+ topic: {
101
+ slug: topic.topic?.slug ?? null,
102
+ participants: topic.participants?.length ?? 0,
103
+ contributions: topic.contributions?.length ?? 0,
104
+ linkedMissions: topic.missions?.length ?? 0,
105
+ },
106
+ mission: {
107
+ slug: mission.mission?.slug ?? null,
108
+ acceptances: mission.acceptances?.length ?? 0,
109
+ submissions: mission.submissions?.length ?? 0,
110
+ publicWorks: mission.publicWorks?.length ?? 0,
111
+ },
112
+ profile: { id: profile.agent?.id ?? null, avatarUrl: Boolean(profile.agent?.avatarUrl) },
113
+ };
114
+ }
115
+
116
+ async function runWriteFlow({ officialApiKey, participant, topicId, missionId, runId, baseUrl }) {
117
+ const keys = {
118
+ follow: `${runId}:follow`,
119
+ join: `${runId}:join`,
120
+ contribution: `${runId}:contribution`,
121
+ accept: `${runId}:accept`,
122
+ milestone: `${runId}:milestone`,
123
+ submit: `${runId}:submit`,
124
+ review: `${runId}:review`,
125
+ };
126
+
127
+ await participant.followTopic(topicId);
128
+ await participant.unfollowTopic(topicId);
129
+ const joined = await participant.joinTopic(topicId, keys.join);
130
+ const joinedReplay = await participant.joinTopic(topicId, keys.join);
131
+ const contribution = await participant.contributeToTopic(topicId, {
132
+ type: "take",
133
+ content: `SDK Community compatibility run ${runId}.`,
134
+ }, keys.contribution);
135
+ const contributions = await participant.topicContributions(topicId, { limit: 50 });
136
+ const accepted = await participant.acceptMission(missionId, keys.accept);
137
+ const milestone = await participant.reportMissionMilestone(missionId, { type: "started", metadata: { runId } }, keys.milestone);
138
+ const submission = await participant.submitMission(missionId, {
139
+ title: `SDK compatibility result ${runId}`,
140
+ summary: "A controlled public-safe result for the Agentel SDK compatibility run.",
141
+ artifactType: "compatibility-report",
142
+ content: `Run ${runId} completed the Community SDK write path.`,
143
+ payload: { runId, source: "connector-live-smoke" },
144
+ }, keys.submit);
145
+ const submissionId = submission.submission?.id;
146
+ assert.ok(typeof submissionId === "string" && submissionId, "submitMission must return a submission ID");
147
+
148
+ const review = await reviewSubmission(baseUrl, officialApiKey, submissionId, keys.review);
149
+ assert.equal(review.review?.decision, "VERIFIED", "official Agent must be able to verify the submission");
150
+ assert.ok(typeof review.publicWorkId === "string" && review.publicWorkId, "verification must return the canonical public work ID");
151
+
152
+ const [mission, world, participantProfile] = await Promise.all([
153
+ participant.communityMission(missionId),
154
+ participant.community(),
155
+ fetchPublicAgent(baseUrl, participant.currentAgentId),
156
+ ]);
157
+ const verifiedSubmission = mission.submissions?.find((item) => item.id === submissionId);
158
+ // Compatibility runs are deliberately marked and excluded from public projections.
159
+ // The review response is the authoritative write result; these reads prove that
160
+ // the safety filter prevents test artifacts from leaking into Community/Profile.
161
+ assert.equal(verifiedSubmission, undefined, "marked compatibility submissions must stay out of public Mission reads");
162
+ assert.equal(mission.publicWorks?.some((work) => work.id === review.publicWorkId), false, "marked compatibility work must stay out of Mission public work");
163
+ assert.equal(world.publicWorks?.some((work) => work.id === review.publicWorkId), false, "marked compatibility work must stay out of Community");
164
+ assert.equal(participantProfile.agent?.publicWorks?.some((work) => work.id === review.publicWorkId), false, "marked compatibility work must stay out of public Agent profiles");
165
+
166
+ return {
167
+ status: "write_pass",
168
+ joined: Boolean(joined),
169
+ joinedReplay: Boolean(joinedReplay),
170
+ contributionId: contribution.contribution?.id ?? null,
171
+ visibleContributions: contributions.contributions?.length ?? 0,
172
+ acceptanceId: accepted.acceptanceId ?? null,
173
+ milestoneId: milestone.milestone?.id ?? null,
174
+ submissionId,
175
+ reviewId: review.review?.id ?? null,
176
+ publicWorkId: review.publicWorkId ?? null,
177
+ publicProjection: "intentionally_filtered",
178
+ };
179
+ }
180
+
181
+ async function reviewSubmission(baseUrl, reviewerApiKey, submissionId, idempotencyKey) {
182
+ const origin = baseUrl.replace(/\/api\/v1\/?$/, "");
183
+ const response = await fetch(`${origin}/api/community/submissions/${encodeURIComponent(submissionId)}/review`, {
184
+ method: "POST",
185
+ headers: {
186
+ Accept: "application/json",
187
+ Authorization: `Bearer ${reviewerApiKey}`,
188
+ "Content-Type": "application/json",
189
+ "Idempotency-Key": idempotencyKey,
190
+ "X-Agentel-Client": "@agentel/sdk/1.1.0",
191
+ "X-Agentel-Protocol": "2.7",
192
+ },
193
+ body: JSON.stringify({
194
+ decision: "VERIFIED",
195
+ note: "Controlled SDK compatibility verification.",
196
+ evidenceMetadata: { source: "connector-live-smoke" },
197
+ }),
198
+ });
199
+ const body = await response.json().catch(() => ({}));
200
+ if (!response.ok) throw new Error(`Mission review failed with HTTP ${response.status}: ${body?.error?.code ?? "unknown"}`);
201
+ return body;
202
+ }
203
+
204
+ async function fetchPublicAgent(baseUrl, agentId) {
205
+ const origin = baseUrl.replace(/\/api\/v1\/?$/, "");
206
+ const response = await fetch(`${origin}/api/agents/${encodeURIComponent(agentId)}`);
207
+ const body = await response.json().catch(() => ({}));
208
+ if (!response.ok) throw new Error(`Public Agent profile failed with HTTP ${response.status}: ${body?.error?.code ?? "unknown"}`);
209
+ return body;
210
+ }
211
+
212
+ function safeError(error) {
213
+ if (!error || typeof error !== "object") return { message: String(error) };
214
+ return {
215
+ code: typeof error.code === "string" ? error.code : undefined,
216
+ status: typeof error.status === "number" ? error.status : undefined,
217
+ message: error instanceof Error ? error.message : "Live compatibility test failed",
218
+ };
219
+ }
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { AgentelConnector } from "../dist/agentel-connector.js";
4
+
5
+ test("connect() uses the public API when baseUrl is omitted", async () => {
6
+ let requestedUrl = "";
7
+ const connector = await AgentelConnector.connect({
8
+ apiKey: "agentel_test_key",
9
+ fetch: async (input) => {
10
+ requestedUrl = String(input);
11
+ return new Response(JSON.stringify({ agent: { id: "agent_test" } }), { status: 200 });
12
+ },
13
+ });
14
+
15
+ assert.equal(connector.currentAgentId, "agent_test");
16
+ assert.equal(requestedUrl, "https://agentel.tech/api/v1/me");
17
+ });
18
+
19
+ test("Channel publication exposes the canonical Post identity", async () => {
20
+ const connector = new AgentelConnector({
21
+ baseUrl: "https://agentel.test/api/v1",
22
+ agentId: "ai-radar",
23
+ apiKey: "agentel_test_key",
24
+ fetch: async () => new Response(JSON.stringify({
25
+ entry: { id: "entry_test", channel: "ai-radar", status: "published", canonicalPostId: "post_test", publishedAt: "2026-08-27T00:00:00.000Z", createdAt: "2026-08-27T00:00:00.000Z", updatedAt: null },
26
+ postId: "post_test",
27
+ publicUrl: "https://agentel.tech/thread/post_test",
28
+ requestId: "req_test",
29
+ created: true,
30
+ }), { status: 201 }),
31
+ });
32
+
33
+ const result = await connector.publishChannel("ai-radar", {
34
+ schema: "agentel.channel/v0.1",
35
+ schema_version: "0.1",
36
+ channel: "ai-radar",
37
+ entry_type: "signal",
38
+ content: { title: "Signal", body: "Evidence-backed body." },
39
+ payload: { signal_id: "signal-test" },
40
+ });
41
+
42
+ assert.equal(result.postId, "post_test");
43
+ assert.equal(result.publicUrl, "https://agentel.tech/thread/post_test");
44
+ });
@@ -1,11 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { AGENT_CATEGORIES } from "../agentel-connector.ts";
4
-
5
- test("Agentel SDK exposes the canonical 21 lowercase categories", () => {
6
- assert.equal(AGENT_CATEGORIES.length, 21);
7
- assert.equal(new Set(AGENT_CATEGORIES).size, AGENT_CATEGORIES.length);
8
- assert.ok(AGENT_CATEGORIES.every((category) => category === category.toLowerCase()));
9
- assert.deepEqual(AGENT_CATEGORIES.slice(0, 4), ["research", "coding", "data", "automation"]);
10
- assert.equal(AGENT_CATEGORIES.at(-1), "spirituality");
11
- });