@perkos/perkos-a2a 0.8.35 → 0.9.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.js CHANGED
@@ -18408,7 +18408,7 @@ var require_view = __commonJS({
18408
18408
  var dirname = path.dirname;
18409
18409
  var basename = path.basename;
18410
18410
  var extname = path.extname;
18411
- var join = path.join;
18411
+ var join2 = path.join;
18412
18412
  var resolve = path.resolve;
18413
18413
  module.exports = View;
18414
18414
  function View(name, options) {
@@ -18456,12 +18456,12 @@ var require_view = __commonJS({
18456
18456
  };
18457
18457
  View.prototype.resolve = function resolve2(dir, file) {
18458
18458
  var ext = this.ext;
18459
- var path2 = join(dir, file);
18459
+ var path2 = join2(dir, file);
18460
18460
  var stat = tryStat(path2);
18461
18461
  if (stat && stat.isFile()) {
18462
18462
  return path2;
18463
18463
  }
18464
- path2 = join(dir, basename(file, ext), "index" + ext);
18464
+ path2 = join2(dir, basename(file, ext), "index" + ext);
18465
18465
  stat = tryStat(path2);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path2;
@@ -19094,7 +19094,7 @@ var require_send = __commonJS({
19094
19094
  var Stream = __require("stream");
19095
19095
  var util = __require("util");
19096
19096
  var extname = path.extname;
19097
- var join = path.join;
19097
+ var join2 = path.join;
19098
19098
  var normalize = path.normalize;
19099
19099
  var resolve = path.resolve;
19100
19100
  var sep = path.sep;
@@ -19313,7 +19313,7 @@ var require_send = __commonJS({
19313
19313
  return res;
19314
19314
  }
19315
19315
  parts = path2.split(sep);
19316
- path2 = normalize(join(root, path2));
19316
+ path2 = normalize(join2(root, path2));
19317
19317
  } else {
19318
19318
  if (UP_PATH_REGEXP.test(path2)) {
19319
19319
  debug('malicious path "%s"', path2);
@@ -19448,7 +19448,7 @@ var require_send = __commonJS({
19448
19448
  if (err) return self.onStatError(err);
19449
19449
  return self.error(404);
19450
19450
  }
19451
- var p = join(path2, self._index[i]);
19451
+ var p = join2(path2, self._index[i]);
19452
19452
  debug('stat "%s"', p);
19453
19453
  fs.stat(p, function(err2, stat) {
19454
19454
  if (err2) return next(err2);
@@ -22608,6 +22608,218 @@ var require_constants = __commonJS({
22608
22608
  }
22609
22609
  });
22610
22610
 
22611
+ // ../../../../node_modules/node-gyp-build/node-gyp-build.js
22612
+ var require_node_gyp_build = __commonJS({
22613
+ "../../../../node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
22614
+ var fs = __require("fs");
22615
+ var path = __require("path");
22616
+ var os = __require("os");
22617
+ var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
22618
+ var vars = process.config && process.config.variables || {};
22619
+ var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
22620
+ var abi = process.versions.modules;
22621
+ var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
22622
+ var arch = process.env.npm_config_arch || os.arch();
22623
+ var platform = process.env.npm_config_platform || os.platform();
22624
+ var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
22625
+ var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
22626
+ var uv = (process.versions.uv || "").split(".")[0];
22627
+ module.exports = load;
22628
+ function load(dir) {
22629
+ return runtimeRequire(load.resolve(dir));
22630
+ }
22631
+ load.resolve = load.path = function(dir) {
22632
+ dir = path.resolve(dir || ".");
22633
+ try {
22634
+ var name = runtimeRequire(path.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
22635
+ if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
22636
+ } catch (err) {
22637
+ }
22638
+ if (!prebuildsOnly) {
22639
+ var release = getFirst(path.join(dir, "build/Release"), matchBuild);
22640
+ if (release) return release;
22641
+ var debug = getFirst(path.join(dir, "build/Debug"), matchBuild);
22642
+ if (debug) return debug;
22643
+ }
22644
+ var prebuild = resolve(dir);
22645
+ if (prebuild) return prebuild;
22646
+ var nearby = resolve(path.dirname(process.execPath));
22647
+ if (nearby) return nearby;
22648
+ var target = [
22649
+ "platform=" + platform,
22650
+ "arch=" + arch,
22651
+ "runtime=" + runtime,
22652
+ "abi=" + abi,
22653
+ "uv=" + uv,
22654
+ armv ? "armv=" + armv : "",
22655
+ "libc=" + libc,
22656
+ "node=" + process.versions.node,
22657
+ process.versions.electron ? "electron=" + process.versions.electron : "",
22658
+ typeof __webpack_require__ === "function" ? "webpack=true" : ""
22659
+ // eslint-disable-line
22660
+ ].filter(Boolean).join(" ");
22661
+ throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
22662
+ function resolve(dir2) {
22663
+ var tuples = readdirSync(path.join(dir2, "prebuilds")).map(parseTuple);
22664
+ var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
22665
+ if (!tuple) return;
22666
+ var prebuilds = path.join(dir2, "prebuilds", tuple.name);
22667
+ var parsed = readdirSync(prebuilds).map(parseTags);
22668
+ var candidates = parsed.filter(matchTags(runtime, abi));
22669
+ var winner = candidates.sort(compareTags(runtime))[0];
22670
+ if (winner) return path.join(prebuilds, winner.file);
22671
+ }
22672
+ };
22673
+ function readdirSync(dir) {
22674
+ try {
22675
+ return fs.readdirSync(dir);
22676
+ } catch (err) {
22677
+ return [];
22678
+ }
22679
+ }
22680
+ function getFirst(dir, filter) {
22681
+ var files = readdirSync(dir).filter(filter);
22682
+ return files[0] && path.join(dir, files[0]);
22683
+ }
22684
+ function matchBuild(name) {
22685
+ return /\.node$/.test(name);
22686
+ }
22687
+ function parseTuple(name) {
22688
+ var arr = name.split("-");
22689
+ if (arr.length !== 2) return;
22690
+ var platform2 = arr[0];
22691
+ var architectures = arr[1].split("+");
22692
+ if (!platform2) return;
22693
+ if (!architectures.length) return;
22694
+ if (!architectures.every(Boolean)) return;
22695
+ return { name, platform: platform2, architectures };
22696
+ }
22697
+ function matchTuple(platform2, arch2) {
22698
+ return function(tuple) {
22699
+ if (tuple == null) return false;
22700
+ if (tuple.platform !== platform2) return false;
22701
+ return tuple.architectures.includes(arch2);
22702
+ };
22703
+ }
22704
+ function compareTuples(a, b) {
22705
+ return a.architectures.length - b.architectures.length;
22706
+ }
22707
+ function parseTags(file) {
22708
+ var arr = file.split(".");
22709
+ var extension2 = arr.pop();
22710
+ var tags = { file, specificity: 0 };
22711
+ if (extension2 !== "node") return;
22712
+ for (var i = 0; i < arr.length; i++) {
22713
+ var tag = arr[i];
22714
+ if (tag === "node" || tag === "electron" || tag === "node-webkit") {
22715
+ tags.runtime = tag;
22716
+ } else if (tag === "napi") {
22717
+ tags.napi = true;
22718
+ } else if (tag.slice(0, 3) === "abi") {
22719
+ tags.abi = tag.slice(3);
22720
+ } else if (tag.slice(0, 2) === "uv") {
22721
+ tags.uv = tag.slice(2);
22722
+ } else if (tag.slice(0, 4) === "armv") {
22723
+ tags.armv = tag.slice(4);
22724
+ } else if (tag === "glibc" || tag === "musl") {
22725
+ tags.libc = tag;
22726
+ } else {
22727
+ continue;
22728
+ }
22729
+ tags.specificity++;
22730
+ }
22731
+ return tags;
22732
+ }
22733
+ function matchTags(runtime2, abi2) {
22734
+ return function(tags) {
22735
+ if (tags == null) return false;
22736
+ if (tags.runtime && tags.runtime !== runtime2 && !runtimeAgnostic(tags)) return false;
22737
+ if (tags.abi && tags.abi !== abi2 && !tags.napi) return false;
22738
+ if (tags.uv && tags.uv !== uv) return false;
22739
+ if (tags.armv && tags.armv !== armv) return false;
22740
+ if (tags.libc && tags.libc !== libc) return false;
22741
+ return true;
22742
+ };
22743
+ }
22744
+ function runtimeAgnostic(tags) {
22745
+ return tags.runtime === "node" && tags.napi;
22746
+ }
22747
+ function compareTags(runtime2) {
22748
+ return function(a, b) {
22749
+ if (a.runtime !== b.runtime) {
22750
+ return a.runtime === runtime2 ? -1 : 1;
22751
+ } else if (a.abi !== b.abi) {
22752
+ return a.abi ? -1 : 1;
22753
+ } else if (a.specificity !== b.specificity) {
22754
+ return a.specificity > b.specificity ? -1 : 1;
22755
+ } else {
22756
+ return 0;
22757
+ }
22758
+ };
22759
+ }
22760
+ function isNwjs() {
22761
+ return !!(process.versions && process.versions.nw);
22762
+ }
22763
+ function isElectron() {
22764
+ if (process.versions && process.versions.electron) return true;
22765
+ if (process.env.ELECTRON_RUN_AS_NODE) return true;
22766
+ return typeof window !== "undefined" && window.process && window.process.type === "renderer";
22767
+ }
22768
+ function isAlpine(platform2) {
22769
+ return platform2 === "linux" && fs.existsSync("/etc/alpine-release");
22770
+ }
22771
+ load.parseTags = parseTags;
22772
+ load.matchTags = matchTags;
22773
+ load.compareTags = compareTags;
22774
+ load.parseTuple = parseTuple;
22775
+ load.matchTuple = matchTuple;
22776
+ load.compareTuples = compareTuples;
22777
+ }
22778
+ });
22779
+
22780
+ // ../../../../node_modules/node-gyp-build/index.js
22781
+ var require_node_gyp_build2 = __commonJS({
22782
+ "../../../../node_modules/node-gyp-build/index.js"(exports, module) {
22783
+ var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
22784
+ if (typeof runtimeRequire.addon === "function") {
22785
+ module.exports = runtimeRequire.addon.bind(runtimeRequire);
22786
+ } else {
22787
+ module.exports = require_node_gyp_build();
22788
+ }
22789
+ }
22790
+ });
22791
+
22792
+ // ../../../../node_modules/bufferutil/fallback.js
22793
+ var require_fallback = __commonJS({
22794
+ "../../../../node_modules/bufferutil/fallback.js"(exports, module) {
22795
+ "use strict";
22796
+ var mask = (source, mask2, output, offset, length) => {
22797
+ for (var i = 0; i < length; i++) {
22798
+ output[offset + i] = source[i] ^ mask2[i & 3];
22799
+ }
22800
+ };
22801
+ var unmask = (buffer, mask2) => {
22802
+ const length = buffer.length;
22803
+ for (var i = 0; i < length; i++) {
22804
+ buffer[i] ^= mask2[i & 3];
22805
+ }
22806
+ };
22807
+ module.exports = { mask, unmask };
22808
+ }
22809
+ });
22810
+
22811
+ // ../../../../node_modules/bufferutil/index.js
22812
+ var require_bufferutil = __commonJS({
22813
+ "../../../../node_modules/bufferutil/index.js"(exports, module) {
22814
+ "use strict";
22815
+ try {
22816
+ module.exports = require_node_gyp_build2()(__dirname);
22817
+ } catch (e) {
22818
+ module.exports = require_fallback();
22819
+ }
22820
+ }
22821
+ });
22822
+
22611
22823
  // node_modules/ws/lib/buffer-util.js
22612
22824
  var require_buffer_util = __commonJS({
22613
22825
  "node_modules/ws/lib/buffer-util.js"(exports, module) {
@@ -22668,7 +22880,7 @@ var require_buffer_util = __commonJS({
22668
22880
  };
22669
22881
  if (!process.env.WS_NO_BUFFER_UTIL) {
22670
22882
  try {
22671
- const bufferUtil = __require("bufferutil");
22883
+ const bufferUtil = require_bufferutil();
22672
22884
  module.exports.mask = function(source, mask, output, offset, length) {
22673
22885
  if (length < 48) _mask(source, mask, output, offset, length);
22674
22886
  else bufferUtil.mask(source, mask, output, offset, length);
@@ -23116,6 +23328,55 @@ var require_permessage_deflate = __commonJS({
23116
23328
  }
23117
23329
  });
23118
23330
 
23331
+ // ../../../../node_modules/utf-8-validate/fallback.js
23332
+ var require_fallback2 = __commonJS({
23333
+ "../../../../node_modules/utf-8-validate/fallback.js"(exports, module) {
23334
+ "use strict";
23335
+ function isValidUTF8(buf) {
23336
+ const len = buf.length;
23337
+ let i = 0;
23338
+ while (i < len) {
23339
+ if ((buf[i] & 128) === 0) {
23340
+ i++;
23341
+ } else if ((buf[i] & 224) === 192) {
23342
+ if (i + 1 === len || (buf[i + 1] & 192) !== 128 || (buf[i] & 254) === 192) {
23343
+ return false;
23344
+ }
23345
+ i += 2;
23346
+ } else if ((buf[i] & 240) === 224) {
23347
+ if (i + 2 >= len || (buf[i + 1] & 192) !== 128 || (buf[i + 2] & 192) !== 128 || buf[i] === 224 && (buf[i + 1] & 224) === 128 || // overlong
23348
+ buf[i] === 237 && (buf[i + 1] & 224) === 160) {
23349
+ return false;
23350
+ }
23351
+ i += 3;
23352
+ } else if ((buf[i] & 248) === 240) {
23353
+ if (i + 3 >= len || (buf[i + 1] & 192) !== 128 || (buf[i + 2] & 192) !== 128 || (buf[i + 3] & 192) !== 128 || buf[i] === 240 && (buf[i + 1] & 240) === 128 || // overlong
23354
+ buf[i] === 244 && buf[i + 1] > 143 || buf[i] > 244) {
23355
+ return false;
23356
+ }
23357
+ i += 4;
23358
+ } else {
23359
+ return false;
23360
+ }
23361
+ }
23362
+ return true;
23363
+ }
23364
+ module.exports = isValidUTF8;
23365
+ }
23366
+ });
23367
+
23368
+ // ../../../../node_modules/utf-8-validate/index.js
23369
+ var require_utf_8_validate = __commonJS({
23370
+ "../../../../node_modules/utf-8-validate/index.js"(exports, module) {
23371
+ "use strict";
23372
+ try {
23373
+ module.exports = require_node_gyp_build2()(__dirname);
23374
+ } catch (e) {
23375
+ module.exports = require_fallback2();
23376
+ }
23377
+ }
23378
+ });
23379
+
23119
23380
  // node_modules/ws/lib/validation.js
23120
23381
  var require_validation = __commonJS({
23121
23382
  "node_modules/ws/lib/validation.js"(exports, module) {
@@ -23307,7 +23568,7 @@ var require_validation = __commonJS({
23307
23568
  };
23308
23569
  } else if (!process.env.WS_NO_UTF_8_VALIDATE) {
23309
23570
  try {
23310
- const isValidUTF8 = __require("utf-8-validate");
23571
+ const isValidUTF8 = require_utf_8_validate();
23311
23572
  module.exports.isValidUTF8 = function(buf) {
23312
23573
  return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf);
23313
23574
  };
@@ -24793,7 +25054,7 @@ var require_websocket = __commonJS({
24793
25054
  var http = __require("http");
24794
25055
  var net = __require("net");
24795
25056
  var tls = __require("tls");
24796
- var { randomBytes, createHash } = __require("crypto");
25057
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
24797
25058
  var { Duplex, Readable } = __require("stream");
24798
25059
  var { URL: URL2 } = __require("url");
24799
25060
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -25453,7 +25714,7 @@ var require_websocket = __commonJS({
25453
25714
  abortHandshake(websocket, socket, "Invalid Upgrade header");
25454
25715
  return;
25455
25716
  }
25456
- const digest = createHash("sha1").update(key + GUID).digest("base64");
25717
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
25457
25718
  if (res.headers["sec-websocket-accept"] !== digest) {
25458
25719
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
25459
25720
  return;
@@ -25732,7 +25993,7 @@ var require_stream = __commonJS({
25732
25993
  };
25733
25994
  duplex._final = function(callback) {
25734
25995
  if (ws.readyState === ws.CONNECTING) {
25735
- ws.once("open", function open() {
25996
+ ws.once("open", function open2() {
25736
25997
  duplex._final(callback);
25737
25998
  });
25738
25999
  return;
@@ -25753,7 +26014,7 @@ var require_stream = __commonJS({
25753
26014
  };
25754
26015
  duplex._write = function(chunk, encoding, callback) {
25755
26016
  if (ws.readyState === ws.CONNECTING) {
25756
- ws.once("open", function open() {
26017
+ ws.once("open", function open2() {
25757
26018
  duplex._write(chunk, encoding, callback);
25758
26019
  });
25759
26020
  return;
@@ -25820,7 +26081,7 @@ var require_websocket_server = __commonJS({
25820
26081
  var EventEmitter = __require("events");
25821
26082
  var http = __require("http");
25822
26083
  var { Duplex } = __require("stream");
25823
- var { createHash } = __require("crypto");
26084
+ var { createHash: createHash2 } = __require("crypto");
25824
26085
  var extension2 = require_extension();
25825
26086
  var PerMessageDeflate2 = require_permessage_deflate();
25826
26087
  var subprotocol2 = require_subprotocol();
@@ -26121,7 +26382,7 @@ var require_websocket_server = __commonJS({
26121
26382
  );
26122
26383
  }
26123
26384
  if (this._state > RUNNING) return abortHandshake(socket, 503);
26124
- const digest = createHash("sha1").update(key + GUID).digest("base64");
26385
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
26125
26386
  const headers = [
26126
26387
  "HTTP/1.1 101 Switching Protocols",
26127
26388
  "Upgrade: websocket",
@@ -27261,8 +27522,8 @@ var A2AServer = class {
27261
27522
  const task = response.result;
27262
27523
  if (task?.id && !isTerminalTaskState(task.status?.state)) {
27263
27524
  const terminal = await this.waitForLocalTaskTerminal(task.id).catch((err) => {
27264
- const errMsg = err instanceof Error ? err.message : String(err);
27265
- this.logger.info(`[perkos-a2a] Waiting for relay task ${task.id} terminal state failed: ${errMsg}`);
27525
+ const errMsg2 = err instanceof Error ? err.message : String(err);
27526
+ this.logger.info(`[perkos-a2a] Waiting for relay task ${task.id} terminal state failed: ${errMsg2}`);
27266
27527
  return null;
27267
27528
  });
27268
27529
  if (terminal) {
@@ -27272,9 +27533,9 @@ var A2AServer = class {
27272
27533
  }
27273
27534
  this.relayClient?.sendTaskResponse(msg, response);
27274
27535
  }).catch((err) => {
27275
- const errMsg = err instanceof Error ? err.message : String(err);
27276
- this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg}`);
27277
- this.relayClient?.sendTaskResponse(msg, this.error(rpcId, -32603, errMsg));
27536
+ const errMsg2 = err instanceof Error ? err.message : String(err);
27537
+ this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
27538
+ this.relayClient?.sendTaskResponse(msg, this.error(rpcId, -32603, errMsg2));
27278
27539
  });
27279
27540
  }
27280
27541
  startRelayClient() {
@@ -27390,7 +27651,7 @@ var A2AServer = class {
27390
27651
  };
27391
27652
 
27392
27653
  // src/index.ts
27393
- import { randomUUID as randomUUID5 } from "crypto";
27654
+ import { randomUUID as randomUUID6 } from "crypto";
27394
27655
 
27395
27656
  // src/agentic-actions.ts
27396
27657
  var FALLBACK_AGENT_ALIASES = [
@@ -27805,6 +28066,550 @@ ${replyResultText2}` : void 0,
27805
28066
  return true;
27806
28067
  }
27807
28068
 
28069
+ // src/chat-client.ts
28070
+ import { randomUUID as randomUUID5 } from "node:crypto";
28071
+
28072
+ // src/chat-store.ts
28073
+ import { createHash } from "node:crypto";
28074
+ import { createReadStream } from "node:fs";
28075
+ import {
28076
+ appendFile,
28077
+ mkdir,
28078
+ open,
28079
+ readFile,
28080
+ writeFile
28081
+ } from "node:fs/promises";
28082
+ import { homedir as homedir2 } from "node:os";
28083
+ import { join } from "node:path";
28084
+ function safeConvId(convId) {
28085
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(convId)) {
28086
+ throw new Error(`invalid convId: ${convId}`);
28087
+ }
28088
+ return convId;
28089
+ }
28090
+ var ChatStore = class {
28091
+ root;
28092
+ constructor(options = {}) {
28093
+ this.root = options.storeRoot ?? join(homedir2(), ".perkos", "conversations");
28094
+ }
28095
+ /** Directory for a single conversation. */
28096
+ dirFor(convId) {
28097
+ return join(this.root, safeConvId(convId));
28098
+ }
28099
+ /** Path to the JSONL log for a conv. */
28100
+ jsonlPath(convId) {
28101
+ return join(this.dirFor(convId), "messages.jsonl");
28102
+ }
28103
+ /** Path to the metadata file for a conv. */
28104
+ metaPath(convId) {
28105
+ return join(this.dirFor(convId), "metadata.json");
28106
+ }
28107
+ /** Ensure the conv directory exists. Idempotent. */
28108
+ async ensureDir(convId) {
28109
+ await mkdir(this.dirFor(convId), { recursive: true });
28110
+ }
28111
+ /**
28112
+ * Write metadata for a conversation. Called on `channel_join` so the agent
28113
+ * has a local record of which participants belong to a conv.
28114
+ */
28115
+ async writeMetadata(meta) {
28116
+ await this.ensureDir(meta.convId);
28117
+ await writeFile(this.metaPath(meta.convId), JSON.stringify(meta, null, 2));
28118
+ }
28119
+ async readMetadata(convId) {
28120
+ try {
28121
+ const raw = await readFile(this.metaPath(convId), "utf8");
28122
+ return JSON.parse(raw);
28123
+ } catch (err) {
28124
+ if (isNotFound(err)) return null;
28125
+ throw err;
28126
+ }
28127
+ }
28128
+ /**
28129
+ * Append a message to the conversation log. The line written is exactly
28130
+ * `JSON.stringify(msg) + "\n"`. The store does not validate ordering; if
28131
+ * messages arrive out of timestamp order, that is recorded as-is.
28132
+ */
28133
+ async append(convId, msg) {
28134
+ await this.ensureDir(convId);
28135
+ await appendFile(this.jsonlPath(convId), JSON.stringify(msg) + "\n");
28136
+ }
28137
+ /**
28138
+ * Read a history page, reverse-chronological. Returns messages with
28139
+ * timestamp strictly less than `before` (if provided), up to `limit`.
28140
+ *
28141
+ * The output order is chronological ascending — i.e. callers can `concat`
28142
+ * pages from oldest to newest without sorting.
28143
+ */
28144
+ async readPage(convId, opts) {
28145
+ const limit = Math.max(1, Math.min(500, opts.limit));
28146
+ const before = opts.before ?? null;
28147
+ let lines;
28148
+ try {
28149
+ const raw = await readFile(this.jsonlPath(convId), "utf8");
28150
+ lines = raw.split("\n").filter((l) => l.length > 0);
28151
+ } catch (err) {
28152
+ if (isNotFound(err)) return { messages: [], hasMore: false };
28153
+ throw err;
28154
+ }
28155
+ const collected = [];
28156
+ let hasMore = false;
28157
+ for (let i = lines.length - 1; i >= 0; i--) {
28158
+ let msg;
28159
+ try {
28160
+ msg = JSON.parse(lines[i]);
28161
+ } catch {
28162
+ continue;
28163
+ }
28164
+ if (before && msg.timestamp >= before) continue;
28165
+ if (collected.length >= limit) {
28166
+ hasMore = true;
28167
+ break;
28168
+ }
28169
+ collected.push(msg);
28170
+ }
28171
+ return { messages: collected.reverse(), hasMore };
28172
+ }
28173
+ /**
28174
+ * Produce a tamper-evident summary of the conversation log for receipt
28175
+ * issuance. Streams `messages.jsonl` through sha256 (followed by a
28176
+ * separator + metadata.json content if present), so memory usage stays
28177
+ * constant regardless of conv size.
28178
+ *
28179
+ * The hash is deterministic: same jsonl bytes → same hex string. A
28180
+ * downstream verifier with the jsonl can recompute and confirm.
28181
+ *
28182
+ * Returns counts + first/last timestamps for the receipt manifest.
28183
+ */
28184
+ async computeReceipt(convId) {
28185
+ const path = this.jsonlPath(convId);
28186
+ try {
28187
+ const fd = await open(path, "r");
28188
+ await fd.close();
28189
+ } catch (err) {
28190
+ if (isNotFound(err)) return null;
28191
+ throw err;
28192
+ }
28193
+ const stream = createReadStream(path, { encoding: "utf8", highWaterMark: 64 * 1024 });
28194
+ const hash = createHash("sha256");
28195
+ let messageCount = 0;
28196
+ let firstMessageAt = null;
28197
+ let lastMessageAt = null;
28198
+ let leftover = "";
28199
+ return new Promise((resolve, reject) => {
28200
+ stream.on("data", (chunk) => {
28201
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
28202
+ hash.update(text);
28203
+ const combined = leftover + text;
28204
+ const lines = combined.split("\n");
28205
+ leftover = lines.pop() ?? "";
28206
+ for (const line of lines) {
28207
+ if (!line) continue;
28208
+ messageCount++;
28209
+ try {
28210
+ const parsed = JSON.parse(line);
28211
+ const ts = parsed.timestamp;
28212
+ if (typeof ts === "string") {
28213
+ if (firstMessageAt === null || ts < firstMessageAt) firstMessageAt = ts;
28214
+ if (lastMessageAt === null || ts > lastMessageAt) lastMessageAt = ts;
28215
+ }
28216
+ } catch {
28217
+ }
28218
+ }
28219
+ });
28220
+ stream.on("end", async () => {
28221
+ if (leftover.length > 0) {
28222
+ messageCount++;
28223
+ try {
28224
+ const parsed = JSON.parse(leftover);
28225
+ const ts = parsed.timestamp;
28226
+ if (typeof ts === "string") {
28227
+ if (firstMessageAt === null || ts < firstMessageAt) firstMessageAt = ts;
28228
+ if (lastMessageAt === null || ts > lastMessageAt) lastMessageAt = ts;
28229
+ }
28230
+ } catch {
28231
+ }
28232
+ }
28233
+ try {
28234
+ const metaRaw = await readFile(this.metaPath(convId), "utf8");
28235
+ hash.update("");
28236
+ hash.update(metaRaw);
28237
+ } catch {
28238
+ }
28239
+ resolve({
28240
+ transcriptHash: hash.digest("hex"),
28241
+ hashAlgo: "sha256",
28242
+ messageCount,
28243
+ firstMessageAt,
28244
+ lastMessageAt
28245
+ });
28246
+ });
28247
+ stream.on("error", (err) => reject(err));
28248
+ });
28249
+ }
28250
+ /**
28251
+ * Return the number of messages in the log. Useful for tests and stats.
28252
+ * Avoid using this on the hot path — it reads the full file.
28253
+ */
28254
+ async count(convId) {
28255
+ let fd;
28256
+ try {
28257
+ fd = await open(this.jsonlPath(convId), "r");
28258
+ } catch (err) {
28259
+ if (isNotFound(err)) return 0;
28260
+ throw err;
28261
+ }
28262
+ try {
28263
+ const raw = await fd.readFile({ encoding: "utf8" });
28264
+ return raw.split("\n").filter((l) => l.length > 0).length;
28265
+ } finally {
28266
+ await fd.close();
28267
+ }
28268
+ }
28269
+ };
28270
+ function isNotFound(err) {
28271
+ return !!err && typeof err === "object" && err.code === "ENOENT";
28272
+ }
28273
+
28274
+ // src/chat-client.ts
28275
+ var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
28276
+ var DEFAULT_HISTORY_LIMIT = 50;
28277
+ var DEFAULT_MIN_RECONNECT = 1e3;
28278
+ var DEFAULT_MAX_RECONNECT = 6e4;
28279
+ var DEFAULT_HEARTBEAT_MS = 25e3;
28280
+ var AUTH_TIMEOUT_MS = 1e4;
28281
+ var ChatClient = class {
28282
+ ws = null;
28283
+ config;
28284
+ agentName;
28285
+ handlers;
28286
+ logger;
28287
+ store;
28288
+ reconnectMs;
28289
+ reconnectTimer = null;
28290
+ heartbeatTimer = null;
28291
+ connected = false;
28292
+ authed = false;
28293
+ stopped = false;
28294
+ constructor(opts) {
28295
+ this.agentName = opts.agentName;
28296
+ const cfg = opts.config;
28297
+ this.config = {
28298
+ enabled: cfg.enabled,
28299
+ url: cfg.url || DEFAULT_URL,
28300
+ apiKey: cfg.apiKey,
28301
+ storeRoot: cfg.storeRoot,
28302
+ defaultHistoryLimit: cfg.defaultHistoryLimit ?? DEFAULT_HISTORY_LIMIT,
28303
+ minReconnectMs: cfg.minReconnectMs ?? DEFAULT_MIN_RECONNECT,
28304
+ maxReconnectMs: cfg.maxReconnectMs ?? DEFAULT_MAX_RECONNECT,
28305
+ heartbeatIntervalMs: cfg.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS
28306
+ };
28307
+ this.handlers = opts.handlers ?? {};
28308
+ this.logger = this.handlers.logger ?? { info: console.log, error: console.error };
28309
+ this.store = new ChatStore({ storeRoot: this.config.storeRoot });
28310
+ this.reconnectMs = this.config.minReconnectMs;
28311
+ }
28312
+ /** Storage handle — exposed so consumers can read history without going through the wire. */
28313
+ getStore() {
28314
+ return this.store;
28315
+ }
28316
+ isConnected() {
28317
+ return this.connected && this.authed;
28318
+ }
28319
+ start() {
28320
+ if (!this.config.enabled) {
28321
+ this.logger.info("[perkos-chat] disabled in config; not connecting");
28322
+ return;
28323
+ }
28324
+ if (!this.config.apiKey) {
28325
+ this.logger.error("[perkos-chat] missing apiKey; cannot connect");
28326
+ return;
28327
+ }
28328
+ this.stopped = false;
28329
+ this.connect();
28330
+ }
28331
+ stop() {
28332
+ this.stopped = true;
28333
+ this.clearTimers();
28334
+ if (this.ws) {
28335
+ try {
28336
+ this.ws.close(1e3, "client shutting down");
28337
+ } catch {
28338
+ }
28339
+ this.ws = null;
28340
+ }
28341
+ this.connected = false;
28342
+ this.authed = false;
28343
+ }
28344
+ /**
28345
+ * Send a reply from this agent into a conversation. Appends to the local
28346
+ * store optimistically, then sends to the server. If the WS is offline,
28347
+ * the local store is still updated and the message is dropped from the
28348
+ * wire — callers should re-send after reconnect if guaranteed delivery is
28349
+ * required (the simple agent loop should just regenerate from history).
28350
+ */
28351
+ async sendReply(opts) {
28352
+ const id = randomUUID5();
28353
+ const msg = {
28354
+ id,
28355
+ from: `agent:${this.agentName}`,
28356
+ text: opts.text,
28357
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28358
+ replyTo: opts.replyTo ?? null
28359
+ };
28360
+ await this.store.append(opts.convId, msg);
28361
+ const frame = {
28362
+ type: "chat_reply",
28363
+ id,
28364
+ convId: opts.convId,
28365
+ walletAddress: opts.walletAddress,
28366
+ text: opts.text,
28367
+ replyTo: opts.replyTo ?? null
28368
+ };
28369
+ const delivered = this.sendFrame(frame);
28370
+ return { id, delivered };
28371
+ }
28372
+ // -------------------------------------------------------------------------
28373
+ // WebSocket lifecycle
28374
+ // -------------------------------------------------------------------------
28375
+ connect() {
28376
+ if (this.stopped) return;
28377
+ const url = this.config.url;
28378
+ this.logger.info(`[perkos-chat] connecting to ${url}`);
28379
+ try {
28380
+ this.ws = new wrapper_default(url);
28381
+ } catch (err) {
28382
+ this.logger.error(`[perkos-chat] failed to construct WS: ${errMsg(err)}`);
28383
+ this.scheduleReconnect();
28384
+ return;
28385
+ }
28386
+ let authTimer = setTimeout(() => {
28387
+ if (!this.authed) {
28388
+ this.logger.error("[perkos-chat] auth timeout");
28389
+ try {
28390
+ this.ws?.close(4002, "auth timeout");
28391
+ } catch {
28392
+ }
28393
+ }
28394
+ }, AUTH_TIMEOUT_MS);
28395
+ this.ws.on("open", () => {
28396
+ this.connected = true;
28397
+ this.reconnectMs = this.config.minReconnectMs;
28398
+ this.sendFrame({
28399
+ type: "auth",
28400
+ role: "agent",
28401
+ agentName: this.agentName,
28402
+ apiKey: this.config.apiKey
28403
+ });
28404
+ });
28405
+ this.ws.on("message", (data) => {
28406
+ let frame;
28407
+ try {
28408
+ frame = JSON.parse(data.toString("utf8"));
28409
+ } catch {
28410
+ this.logger.error("[perkos-chat] failed to parse frame");
28411
+ return;
28412
+ }
28413
+ if (authTimer) {
28414
+ clearTimeout(authTimer);
28415
+ authTimer = null;
28416
+ }
28417
+ void this.handleFrame(frame);
28418
+ });
28419
+ this.ws.on("close", (code, reason) => {
28420
+ this.connected = false;
28421
+ this.authed = false;
28422
+ this.clearTimers();
28423
+ if (authTimer) {
28424
+ clearTimeout(authTimer);
28425
+ authTimer = null;
28426
+ }
28427
+ if (!this.stopped) {
28428
+ this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
28429
+ this.scheduleReconnect();
28430
+ }
28431
+ });
28432
+ this.ws.on("error", (err) => {
28433
+ this.logger.error(`[perkos-chat] WS error: ${errMsg(err)}`);
28434
+ });
28435
+ }
28436
+ async handleFrame(frame) {
28437
+ switch (frame.type) {
28438
+ case "auth_ok":
28439
+ this.authed = true;
28440
+ this.logger.info(`[perkos-chat] authed as agent:${this.agentName}`);
28441
+ this.startHeartbeat();
28442
+ return;
28443
+ case "auth_error":
28444
+ this.logger.error(`[perkos-chat] auth_error ${frame.code}: ${frame.message}`);
28445
+ try {
28446
+ this.ws?.close(4401, frame.code);
28447
+ } catch {
28448
+ }
28449
+ return;
28450
+ case "chat_deliver":
28451
+ case "chat_message": {
28452
+ await this.persistInbound(frame);
28453
+ if (this.handlers.onChatDeliver) {
28454
+ try {
28455
+ await this.handlers.onChatDeliver(frame);
28456
+ } catch (err) {
28457
+ this.logger.error(`[perkos-chat] onChatDeliver threw: ${errMsg(err)}`);
28458
+ }
28459
+ }
28460
+ return;
28461
+ }
28462
+ case "history_request":
28463
+ await this.respondToHistoryRequest(frame);
28464
+ return;
28465
+ case "receipt_request":
28466
+ await this.respondToReceiptRequest(frame);
28467
+ return;
28468
+ case "channel_join":
28469
+ await this.handleChannelJoin(frame);
28470
+ return;
28471
+ case "ack":
28472
+ case "pong":
28473
+ case "typing":
28474
+ return;
28475
+ case "error":
28476
+ this.logger.error(`[perkos-chat] server error ${frame.code}: ${frame.message}`);
28477
+ return;
28478
+ default:
28479
+ this.logger.info(`[perkos-chat] unknown frame type: ${frame.type}`);
28480
+ }
28481
+ }
28482
+ async persistInbound(frame) {
28483
+ const msg = {
28484
+ id: frame.id,
28485
+ from: frame.from,
28486
+ text: frame.text,
28487
+ timestamp: frame.timestamp,
28488
+ replyTo: "replyTo" in frame ? frame.replyTo ?? null : null
28489
+ };
28490
+ try {
28491
+ await this.store.append(frame.convId, msg);
28492
+ } catch (err) {
28493
+ this.logger.error(`[perkos-chat] failed to append to store: ${errMsg(err)}`);
28494
+ }
28495
+ }
28496
+ async handleChannelJoin(frame) {
28497
+ const now = (/* @__PURE__ */ new Date()).toISOString();
28498
+ const existing = await this.store.readMetadata(frame.convId);
28499
+ const meta = {
28500
+ convId: frame.convId,
28501
+ participants: frame.participants,
28502
+ historyHost: frame.historyHost,
28503
+ createdAt: existing?.createdAt ?? now,
28504
+ updatedAt: now
28505
+ };
28506
+ try {
28507
+ await this.store.writeMetadata(meta);
28508
+ } catch (err) {
28509
+ this.logger.error(`[perkos-chat] failed to write metadata for ${frame.convId}: ${errMsg(err)}`);
28510
+ }
28511
+ if (this.handlers.onChannelJoin) {
28512
+ try {
28513
+ await this.handlers.onChannelJoin(frame);
28514
+ } catch (err) {
28515
+ this.logger.error(`[perkos-chat] onChannelJoin threw: ${errMsg(err)}`);
28516
+ }
28517
+ }
28518
+ }
28519
+ async respondToReceiptRequest(frame) {
28520
+ let summary;
28521
+ try {
28522
+ summary = await this.store.computeReceipt(frame.convId);
28523
+ } catch (err) {
28524
+ this.logger.error(`[perkos-chat] receipt computation failed: ${errMsg(err)}`);
28525
+ return;
28526
+ }
28527
+ if (!summary) {
28528
+ this.sendFrame({
28529
+ type: "receipt_response",
28530
+ id: frame.id,
28531
+ convId: frame.convId,
28532
+ forWallet: frame.forWallet,
28533
+ transcriptHash: "",
28534
+ hashAlgo: "sha256",
28535
+ messageCount: 0,
28536
+ firstMessageAt: null,
28537
+ lastMessageAt: null,
28538
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
28539
+ });
28540
+ return;
28541
+ }
28542
+ this.sendFrame({
28543
+ type: "receipt_response",
28544
+ id: frame.id,
28545
+ convId: frame.convId,
28546
+ forWallet: frame.forWallet,
28547
+ transcriptHash: summary.transcriptHash,
28548
+ hashAlgo: summary.hashAlgo,
28549
+ messageCount: summary.messageCount,
28550
+ firstMessageAt: summary.firstMessageAt,
28551
+ lastMessageAt: summary.lastMessageAt,
28552
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
28553
+ });
28554
+ }
28555
+ async respondToHistoryRequest(frame) {
28556
+ const limit = frame.limit ?? this.config.defaultHistoryLimit;
28557
+ let page;
28558
+ try {
28559
+ page = await this.store.readPage(frame.convId, { before: frame.before ?? null, limit });
28560
+ } catch (err) {
28561
+ this.logger.error(`[perkos-chat] history read failed: ${errMsg(err)}`);
28562
+ page = { messages: [], hasMore: false };
28563
+ }
28564
+ this.sendFrame({
28565
+ type: "history_chunk",
28566
+ id: frame.id,
28567
+ convId: frame.convId,
28568
+ forWallet: frame.forWallet,
28569
+ messages: page.messages,
28570
+ hasMore: page.hasMore
28571
+ });
28572
+ }
28573
+ sendFrame(frame) {
28574
+ if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
28575
+ this.ws.send(JSON.stringify(frame));
28576
+ return true;
28577
+ }
28578
+ return false;
28579
+ }
28580
+ startHeartbeat() {
28581
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
28582
+ this.heartbeatTimer = setInterval(() => {
28583
+ if (!this.sendFrame({ type: "ping" })) {
28584
+ }
28585
+ }, this.config.heartbeatIntervalMs);
28586
+ }
28587
+ scheduleReconnect() {
28588
+ if (this.stopped) return;
28589
+ const delay = this.reconnectMs;
28590
+ this.reconnectTimer = setTimeout(() => this.connect(), delay);
28591
+ this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
28592
+ }
28593
+ clearTimers() {
28594
+ if (this.heartbeatTimer) {
28595
+ clearInterval(this.heartbeatTimer);
28596
+ this.heartbeatTimer = null;
28597
+ }
28598
+ if (this.reconnectTimer) {
28599
+ clearTimeout(this.reconnectTimer);
28600
+ this.reconnectTimer = null;
28601
+ }
28602
+ }
28603
+ };
28604
+ function errMsg(err) {
28605
+ return err instanceof Error ? err.message : String(err);
28606
+ }
28607
+
28608
+ // src/types.ts
28609
+ function isWebhookEvent(msg) {
28610
+ return msg.type === "webhook_event";
28611
+ }
28612
+
27808
28613
  // src/index.ts
27809
28614
  function wakeGatewayAgent(requestHeartbeatNow, reason, logger, sessionKey = "agent:main") {
27810
28615
  if (typeof requestHeartbeatNow === "function") {
@@ -27827,6 +28632,11 @@ function openclawEventSessionKey(config) {
27827
28632
  if (!key) return "agent:main:main";
27828
28633
  return key.endsWith(":main") ? key : `${key}:main`;
27829
28634
  }
28635
+ function parseWalletFromIdentity(identity) {
28636
+ if (typeof identity !== "string" || !identity.startsWith("user:")) return null;
28637
+ const addr = identity.slice("user:".length);
28638
+ return addr.startsWith("0x") ? addr.toLowerCase() : null;
28639
+ }
27830
28640
  async function deliverToHermes(config, message, logger) {
27831
28641
  const runtime = config.runtime || {};
27832
28642
  const baseUrl = (runtime.hermesUrl || "http://127.0.0.1:8642").replace(/\/+$/, "");
@@ -27916,7 +28726,7 @@ function register(api) {
27916
28726
  await deliverToHermes(pluginConfig, eventText, logger);
27917
28727
  task.artifacts.push({
27918
28728
  kind: "artifact",
27919
- artifactId: randomUUID5(),
28729
+ artifactId: randomUUID6(),
27920
28730
  parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
27921
28731
  });
27922
28732
  const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
@@ -27926,14 +28736,14 @@ function register(api) {
27926
28736
  if (runtimeKind === "none") {
27927
28737
  task.artifacts.push({
27928
28738
  kind: "artifact",
27929
- artifactId: randomUUID5(),
28739
+ artifactId: randomUUID6(),
27930
28740
  parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
27931
28741
  });
27932
28742
  return;
27933
28743
  }
27934
28744
  task.artifacts.push({
27935
28745
  kind: "artifact",
27936
- artifactId: randomUUID5(),
28746
+ artifactId: randomUUID6(),
27937
28747
  parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
27938
28748
  });
27939
28749
  if (enqueueSystemEvent) {
@@ -28017,7 +28827,7 @@ function register(api) {
28017
28827
  if (!finalText) continue;
28018
28828
  task.artifacts.push({
28019
28829
  kind: "artifact",
28020
- artifactId: randomUUID5(),
28830
+ artifactId: randomUUID6(),
28021
28831
  parts: [{ kind: "text", text: finalText }]
28022
28832
  });
28023
28833
  task.status = {
@@ -28041,6 +28851,83 @@ function register(api) {
28041
28851
  logger.info("[perkos-a2a] A2A server stopping");
28042
28852
  }
28043
28853
  });
28854
+ const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
28855
+ const chatClient = pluginConfig.chat?.enabled && chatApiKey ? new ChatClient({
28856
+ agentName: pluginConfig.agentName,
28857
+ config: {
28858
+ enabled: true,
28859
+ url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
28860
+ apiKey: chatApiKey,
28861
+ storeRoot: pluginConfig.chat?.storeRoot,
28862
+ defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
28863
+ minReconnectMs: pluginConfig.chat?.minReconnectMs,
28864
+ maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
28865
+ heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
28866
+ },
28867
+ handlers: {
28868
+ logger,
28869
+ onChatDeliver: async (frame) => {
28870
+ const walletAddress = parseWalletFromIdentity(frame.from);
28871
+ const marker = `[PERKOS_CHAT:${frame.convId}]`;
28872
+ const eventText = [
28873
+ marker,
28874
+ `From: ${frame.from}`,
28875
+ `Conversation: ${frame.convId}`,
28876
+ walletAddress ? `Wallet: ${walletAddress}` : "",
28877
+ "",
28878
+ "A user is messaging you in a PerkOS chat conversation.",
28879
+ "To respond, call the `perkos_chat_reply` tool with:",
28880
+ ` convId: "${frame.convId}"`,
28881
+ walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
28882
+ ` text: <your reply>`,
28883
+ "",
28884
+ "Message body:",
28885
+ frame.text
28886
+ ].filter(Boolean).join("\n");
28887
+ const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
28888
+ if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
28889
+ try {
28890
+ await deliverToHermes(pluginConfig, eventText, logger);
28891
+ } catch (err) {
28892
+ logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
28893
+ }
28894
+ return;
28895
+ }
28896
+ if (runtimeKind === "none") {
28897
+ logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
28898
+ return;
28899
+ }
28900
+ if (enqueueSystemEvent) {
28901
+ enqueueSystemEvent(eventText, { sessionKey: openclawEventSessionKey(pluginConfig) });
28902
+ }
28903
+ wakeGatewayAgent(
28904
+ requestHeartbeatNow,
28905
+ `[PerkOS-Chat] inbound message in ${frame.convId}`,
28906
+ logger,
28907
+ openclawWakeSessionKey(pluginConfig)
28908
+ );
28909
+ },
28910
+ onChannelJoin: async (frame) => {
28911
+ logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
28912
+ }
28913
+ }
28914
+ }) : null;
28915
+ if (pluginConfig.chat?.enabled && !chatApiKey) {
28916
+ logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
28917
+ }
28918
+ if (chatClient) {
28919
+ api.registerService({
28920
+ id: "perkos-a2a-chat",
28921
+ start: () => {
28922
+ chatClient.start();
28923
+ logger.info(`[perkos-chat] client started for ${pluginConfig.agentName}`);
28924
+ },
28925
+ stop: () => {
28926
+ chatClient.stop();
28927
+ logger.info("[perkos-chat] client stopped");
28928
+ }
28929
+ });
28930
+ }
28044
28931
  api.registerTool({
28045
28932
  name: "perkos_a2a_send",
28046
28933
  description: "Send a task to another agent via PerkOS A2A. Use this to delegate work, ask a peer to research, or route a chain/circular workflow. The message may name additional agents and a final notification target; the bridge preserves the route and prevents loops.",
@@ -28201,6 +29088,102 @@ function register(api) {
28201
29088
  }
28202
29089
  }
28203
29090
  });
29091
+ api.registerTool({
29092
+ name: "perkos_chat_reply",
29093
+ description: "Send a reply into a PerkOS chat conversation. Use this when a user (or another agent) sent a chat message you should respond to. The system event that woke you contains `Conversation: <convId>` and `Wallet: <walletAddress>` lines you can copy directly. The reply is appended to local history first, then sent on the wire \u2014 so it's preserved even if the chat server is briefly unreachable.",
29094
+ parameters: {
29095
+ type: "object",
29096
+ properties: {
29097
+ convId: {
29098
+ type: "string",
29099
+ description: "Conversation id (UUID) \u2014 found in the [PERKOS_CHAT:<convId>] marker."
29100
+ },
29101
+ walletAddress: {
29102
+ type: "string",
29103
+ description: "Wallet that owns the conversation. From the `Wallet:` line of the system event."
29104
+ },
29105
+ text: {
29106
+ type: "string",
29107
+ description: "Reply body. Markdown supported."
29108
+ },
29109
+ replyTo: {
29110
+ type: "string",
29111
+ description: "Optional id of the inbound message you are replying to."
29112
+ }
29113
+ },
29114
+ required: ["convId", "walletAddress", "text"]
29115
+ },
29116
+ async execute(_id, params) {
29117
+ if (!chatClient) {
29118
+ return {
29119
+ content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
29120
+ };
29121
+ }
29122
+ try {
29123
+ const result = await chatClient.sendReply({
29124
+ convId: params.convId,
29125
+ walletAddress: params.walletAddress,
29126
+ text: params.text,
29127
+ replyTo: params.replyTo ?? null
29128
+ });
29129
+ return {
29130
+ content: [{
29131
+ type: "text",
29132
+ text: [
29133
+ `Reply ${result.delivered ? "delivered" : "queued (offline)"}.`,
29134
+ `Message id: ${result.id}`,
29135
+ `Conv: ${params.convId}`
29136
+ ].join("\n")
29137
+ }]
29138
+ };
29139
+ } catch (err) {
29140
+ const msg = err instanceof Error ? err.message : String(err);
29141
+ return { content: [{ type: "text", text: `Error sending chat reply: ${msg}` }] };
29142
+ }
29143
+ }
29144
+ });
29145
+ api.registerTool({
29146
+ name: "perkos_chat_history",
29147
+ description: "Read recent messages from a PerkOS chat conversation (this agent's local store). Returns up to `limit` messages older than `before` (ISO timestamp), most-recent-first ordered by timestamp. Use this to recall context before composing a reply.",
29148
+ parameters: {
29149
+ type: "object",
29150
+ properties: {
29151
+ convId: { type: "string", description: "Conversation id (UUID)." },
29152
+ before: {
29153
+ type: "string",
29154
+ description: "Optional ISO timestamp; only messages strictly older than this are returned. Omit to get the newest page."
29155
+ },
29156
+ limit: {
29157
+ type: "number",
29158
+ description: "Max messages to return (1-500, default 50)."
29159
+ }
29160
+ },
29161
+ required: ["convId"]
29162
+ },
29163
+ async execute(_id, params) {
29164
+ if (!chatClient) {
29165
+ return {
29166
+ content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
29167
+ };
29168
+ }
29169
+ try {
29170
+ const store = chatClient.getStore();
29171
+ const page = await store.readPage(params.convId, {
29172
+ before: params.before ?? null,
29173
+ limit: params.limit ?? 50
29174
+ });
29175
+ if (page.messages.length === 0) {
29176
+ return { content: [{ type: "text", text: `No messages found in ${params.convId}.` }] };
29177
+ }
29178
+ const lines = page.messages.map((m) => `[${m.timestamp}] ${m.from}: ${m.text}`);
29179
+ if (page.hasMore) lines.push("", "(older messages available \u2014 call again with `before` set to the earliest timestamp)");
29180
+ return { content: [{ type: "text", text: lines.join("\n") }] };
29181
+ } catch (err) {
29182
+ const msg = err instanceof Error ? err.message : String(err);
29183
+ return { content: [{ type: "text", text: `Error reading chat history: ${msg}` }] };
29184
+ }
29185
+ }
29186
+ });
28204
29187
  api.registerGatewayMethod("perkos-a2a.status", ({ respond }) => {
28205
29188
  respond(true, {
28206
29189
  agent: pluginConfig.agentName,
@@ -28213,7 +29196,12 @@ function register(api) {
28213
29196
  pendingTasks: pendingTasks.length,
28214
29197
  hasSystemEventInjection: !!enqueueSystemEvent,
28215
29198
  protocol: "a2a",
28216
- version: "0.8.4"
29199
+ version: "0.8.4",
29200
+ chat: chatClient ? {
29201
+ enabled: true,
29202
+ connected: chatClient.isConnected(),
29203
+ url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"
29204
+ } : { enabled: false, connected: false, url: null }
28217
29205
  });
28218
29206
  });
28219
29207
  api.registerCli(
@@ -28229,6 +29217,11 @@ function register(api) {
28229
29217
  console.log(`Auth required: ${pluginConfig.auth?.requireApiKey || false}`);
28230
29218
  console.log(`Peers: ${Object.keys(pluginConfig.peers).join(", ") || "(none)"}`);
28231
29219
  console.log(`System event injection: ${!!enqueueSystemEvent}`);
29220
+ if (chatClient) {
29221
+ console.log(`Chat: enabled (connected=${chatClient.isConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
29222
+ } else {
29223
+ console.log("Chat: disabled");
29224
+ }
28232
29225
  });
28233
29226
  cmd.command("discover").description("Discover peer agents").action(async () => {
28234
29227
  const peers = await server.discoverPeers();
@@ -28312,10 +29305,13 @@ Relay: ${pluginConfig.relay?.enabled ? "enabled" : "disabled"}`);
28312
29305
  }
28313
29306
  export {
28314
29307
  A2AServer,
29308
+ ChatClient,
29309
+ ChatStore,
28315
29310
  RelayClient,
28316
29311
  RelayHub,
28317
29312
  register as default,
28318
- detectNetworking
29313
+ detectNetworking,
29314
+ isWebhookEvent
28319
29315
  };
28320
29316
  /*! Bundled license information:
28321
29317