aamp-openclaw-plugin 0.1.22 → 0.1.25

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.
@@ -62,9 +62,113 @@ function expandHome(pathValue) {
62
62
  return pathValue
63
63
  }
64
64
 
65
+ function stripJsonComments(text) {
66
+ let result = ''
67
+ let inString = false
68
+ let stringQuote = ''
69
+ let escaped = false
70
+
71
+ for (let i = 0; i < text.length; i += 1) {
72
+ const char = text[i]
73
+ const next = text[i + 1]
74
+
75
+ if (inString) {
76
+ result += char
77
+ if (escaped) {
78
+ escaped = false
79
+ } else if (char === '\\') {
80
+ escaped = true
81
+ } else if (char === stringQuote) {
82
+ inString = false
83
+ stringQuote = ''
84
+ }
85
+ continue
86
+ }
87
+
88
+ if (char === '"' || char === "'") {
89
+ inString = true
90
+ stringQuote = char
91
+ result += char
92
+ continue
93
+ }
94
+
95
+ if (char === '/' && next === '/') {
96
+ i += 2
97
+ while (i < text.length && text[i] !== '\n') i += 1
98
+ if (i < text.length) result += text[i]
99
+ continue
100
+ }
101
+
102
+ if (char === '/' && next === '*') {
103
+ i += 2
104
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1
105
+ i += 1
106
+ continue
107
+ }
108
+
109
+ result += char
110
+ }
111
+
112
+ return result
113
+ }
114
+
115
+ function stripTrailingCommas(text) {
116
+ let result = ''
117
+ let inString = false
118
+ let stringQuote = ''
119
+ let escaped = false
120
+
121
+ for (let i = 0; i < text.length; i += 1) {
122
+ const char = text[i]
123
+
124
+ if (inString) {
125
+ result += char
126
+ if (escaped) {
127
+ escaped = false
128
+ } else if (char === '\\') {
129
+ escaped = true
130
+ } else if (char === stringQuote) {
131
+ inString = false
132
+ stringQuote = ''
133
+ }
134
+ continue
135
+ }
136
+
137
+ if (char === '"' || char === "'") {
138
+ inString = true
139
+ stringQuote = char
140
+ result += char
141
+ continue
142
+ }
143
+
144
+ if (char === ',') {
145
+ let lookahead = i + 1
146
+ while (lookahead < text.length && /\s/.test(text[lookahead])) lookahead += 1
147
+ if (text[lookahead] === '}' || text[lookahead] === ']') {
148
+ continue
149
+ }
150
+ }
151
+
152
+ result += char
153
+ }
154
+
155
+ return result
156
+ }
157
+
158
+ function parseJsonConfig(raw, path) {
159
+ const normalized = raw.replace(/^\uFEFF/, '')
160
+ const sanitized = stripTrailingCommas(stripJsonComments(normalized))
161
+ try {
162
+ return JSON.parse(sanitized)
163
+ } catch (error) {
164
+ const reason = error instanceof Error ? error.message : String(error)
165
+ throw new Error(`Failed to parse ${path}: ${reason}`)
166
+ }
167
+ }
168
+
65
169
  function readJsonFile(path) {
66
170
  if (!existsSync(path)) return null
67
- return JSON.parse(readFileSync(path, 'utf-8'))
171
+ return parseJsonConfig(readFileSync(path, 'utf-8'), path)
68
172
  }
69
173
 
70
174
  function writeJsonFile(path, value) {
@@ -82,6 +186,7 @@ function ensurePluginConfig(config, pluginConfig, options = {}) {
82
186
  if (!next.plugins || typeof next.plugins !== 'object') next.plugins = {}
83
187
  if (!Array.isArray(next.plugins.allow)) next.plugins.allow = []
84
188
  if (!next.plugins.entries || typeof next.plugins.entries !== 'object') next.plugins.entries = {}
189
+ if (!next.channels || typeof next.channels !== 'object') next.channels = {}
85
190
 
86
191
  if (!next.plugins.allow.includes(PLUGIN_ID)) {
87
192
  next.plugins.allow.push(PLUGIN_ID)
@@ -89,29 +194,51 @@ function ensurePluginConfig(config, pluginConfig, options = {}) {
89
194
 
90
195
  const legacyEntry = next.plugins.entries.aamp
91
196
  const prevEntry = next.plugins.entries[PLUGIN_ID] ?? legacyEntry
92
- const mergedConfig = {
93
- ...(prevEntry?.config && typeof prevEntry.config === 'object' ? prevEntry.config : {}),
94
- ...pluginConfig,
95
- }
96
- if (!pluginConfig.senderPolicies) {
97
- delete mergedConfig.senderPolicies
98
- }
99
-
100
197
  next.plugins.entries[PLUGIN_ID] = {
101
198
  enabled: true,
102
199
  ...(prevEntry && typeof prevEntry === 'object' ? prevEntry : {}),
103
- config: mergedConfig,
104
200
  }
105
201
 
106
202
  if (next.plugins.entries.aamp) {
107
203
  delete next.plugins.entries.aamp
108
204
  }
109
205
 
206
+ const previousChannelConfig =
207
+ next.channels.aamp && typeof next.channels.aamp === 'object' ? next.channels.aamp : {}
208
+ const mergedChannelConfig = {
209
+ ...previousChannelConfig,
210
+ ...pluginConfig,
211
+ enabled: true,
212
+ }
213
+ if (!pluginConfig.senderPolicies) {
214
+ delete mergedChannelConfig.senderPolicies
215
+ }
216
+ next.channels.aamp = mergedChannelConfig
217
+
110
218
  next.tools = ensureAampToolAllowlist(next.tools, options)
111
219
 
112
220
  return next
113
221
  }
114
222
 
223
+ function ensurePluginInstallRecord(config, installRecord) {
224
+ const next = config && typeof config === 'object' ? structuredClone(config) : {}
225
+ if (!next.plugins || typeof next.plugins !== 'object') next.plugins = {}
226
+ if (!next.plugins.installs || typeof next.plugins.installs !== 'object') next.plugins.installs = {}
227
+
228
+ next.plugins.installs[PLUGIN_ID] = {
229
+ ...(next.plugins.installs[PLUGIN_ID] && typeof next.plugins.installs[PLUGIN_ID] === 'object'
230
+ ? next.plugins.installs[PLUGIN_ID]
231
+ : {}),
232
+ ...installRecord,
233
+ }
234
+
235
+ if (next.plugins.installs.aamp) {
236
+ delete next.plugins.installs.aamp
237
+ }
238
+
239
+ return next
240
+ }
241
+
115
242
  function ensureAampToolAllowlist(toolsConfig, options = {}) {
116
243
  const next = toolsConfig && typeof toolsConfig === 'object' ? structuredClone(toolsConfig) : {}
117
244
  const existingAllow = Array.isArray(next.allow) ? next.allow.filter((value) => typeof value === 'string' && value.trim()) : []
@@ -278,7 +405,7 @@ function installPluginFiles(credentialsFile = DEFAULT_CREDENTIALS_FILE) {
278
405
  writeFileSync(credentialsPath, existingCredentials)
279
406
  }
280
407
 
281
- return extensionDir
408
+ return { extensionDir, packageJson, packageRoot }
282
409
  }
283
410
 
284
411
  function restartGateway() {
@@ -371,9 +498,12 @@ async function runInit() {
371
498
  const configPath = resolveOpenClawConfigPath()
372
499
  const existing = readJsonFile(configPath)
373
500
  const previousEntry = existing?.plugins?.entries?.[PLUGIN_ID] ?? existing?.plugins?.entries?.aamp
374
- const previousConfig = previousEntry?.config && typeof previousEntry.config === 'object'
375
- ? previousEntry.config
376
- : null
501
+ const previousConfig =
502
+ existing?.channels?.aamp && typeof existing.channels.aamp === 'object'
503
+ ? existing.channels.aamp
504
+ : previousEntry?.config && typeof previousEntry.config === 'object'
505
+ ? previousEntry.config
506
+ : null
377
507
  const previousCredentialsFile = previousConfig?.credentialsFile || DEFAULT_CREDENTIALS_FILE
378
508
  const previousSlug = previousConfig?.slug || 'openclaw-agent'
379
509
 
@@ -466,10 +596,10 @@ async function runInit() {
466
596
  }
467
597
 
468
598
  output.write('\nInstalling OpenClaw plugin files...\n')
469
- const extensionDir = installPluginFiles(previousCredentialsFile)
599
+ const { extensionDir, packageJson, packageRoot } = installPluginFiles(previousCredentialsFile)
470
600
 
471
601
  const toolPolicyPlan = planToolPolicyUpdate(existing?.tools, { includeCodingBaseline })
472
- const next = ensurePluginConfig(existing, {
602
+ let next = ensurePluginConfig(existing, {
473
603
  aampHost,
474
604
  slug,
475
605
  credentialsFile: DEFAULT_CREDENTIALS_FILE,
@@ -478,6 +608,20 @@ async function runInit() {
478
608
  includeCodingBaseline,
479
609
  })
480
610
 
611
+ const now = new Date().toISOString()
612
+ next = ensurePluginInstallRecord(next, {
613
+ source: 'npm',
614
+ spec: packageJson?.name || PLUGIN_ID,
615
+ sourcePath: packageRoot,
616
+ installPath: extensionDir,
617
+ version: packageJson?.version || '0.0.0',
618
+ resolvedName: packageJson?.name || PLUGIN_ID,
619
+ resolvedVersion: packageJson?.version || '0.0.0',
620
+ resolvedSpec: `${packageJson?.name || PLUGIN_ID}@${packageJson?.version || '0.0.0'}`,
621
+ installedAt: now,
622
+ resolvedAt: now,
623
+ })
624
+
481
625
  writeJsonFile(configPath, next)
482
626
 
483
627
  const identityResult = await ensureMailboxIdentity({
@@ -496,6 +640,8 @@ async function runInit() {
496
640
  '',
497
641
  'Configured plugin entry:',
498
642
  ` plugins.entries["${PLUGIN_ID}"]`,
643
+ ` plugins.installs["${PLUGIN_ID}"]`,
644
+ ` channels.aamp.enabled: ${next.channels?.aamp?.enabled === true ? 'true' : 'false'}`,
499
645
  ` aampHost: ${aampHost}`,
500
646
  ` credentialsFile: ${DEFAULT_CREDENTIALS_FILE}`,
501
647
  ` senderPolicies: ${senderPolicies ? JSON.stringify(senderPolicies) : '(allow all)'}`,
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@
2
2
  import WebSocket from "ws";
3
3
 
4
4
  // ../sdk/src/types.ts
5
+ var AAMP_PROTOCOL_VERSION = "1.0";
5
6
  var AAMP_HEADER = {
7
+ VERSION: "X-AAMP-Version",
6
8
  INTENT: "X-AAMP-Intent",
7
9
  TASK_ID: "X-AAMP-TaskId",
8
10
  TIMEOUT: "X-AAMP-Timeout",
@@ -122,6 +124,7 @@ function parseAampHeaders(meta) {
122
124
  const headers = normalizeHeaders(meta.headers);
123
125
  const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
124
126
  const taskId = getAampHeader(headers, AAMP_HEADER.TASK_ID);
127
+ const protocolVersion = getAampHeader(headers, AAMP_HEADER.VERSION) ?? AAMP_PROTOCOL_VERSION;
125
128
  if (!intent || !taskId)
126
129
  return null;
127
130
  const from = meta.from.replace(/^<|>$/g, "");
@@ -135,6 +138,7 @@ function parseAampHeaders(meta) {
135
138
  );
136
139
  const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
137
140
  const dispatch = {
141
+ protocolVersion,
138
142
  intent: "task.dispatch",
139
143
  taskId,
140
144
  title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
@@ -159,6 +163,7 @@ function parseAampHeaders(meta) {
159
163
  getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT)
160
164
  );
161
165
  const result = {
166
+ protocolVersion,
162
167
  intent: "task.result",
163
168
  taskId,
164
169
  status,
@@ -171,12 +176,13 @@ function parseAampHeaders(meta) {
171
176
  };
172
177
  return result;
173
178
  }
174
- if (intent === "task.help") {
179
+ if (intent === "task.help_needed" || intent === "task.help") {
175
180
  const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? "";
176
181
  const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? "";
177
182
  const suggestedOptionsStr = getAampHeader(headers, AAMP_HEADER.SUGGESTED_OPTIONS) ?? "";
178
183
  const help = {
179
- intent: "task.help",
184
+ protocolVersion,
185
+ intent: "task.help_needed",
180
186
  taskId,
181
187
  question: decodeMimeEncodedWords(question),
182
188
  blockedReason: decodeMimeEncodedWords(blockedReason),
@@ -189,6 +195,7 @@ function parseAampHeaders(meta) {
189
195
  }
190
196
  if (intent === "task.ack") {
191
197
  const ack = {
198
+ protocolVersion,
192
199
  intent: "task.ack",
193
200
  taskId,
194
201
  from,
@@ -201,6 +208,7 @@ function parseAampHeaders(meta) {
201
208
  }
202
209
  function buildDispatchHeaders(params) {
203
210
  const headers = {
211
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
204
212
  [AAMP_HEADER.INTENT]: "task.dispatch",
205
213
  [AAMP_HEADER.TASK_ID]: params.taskId
206
214
  };
@@ -221,12 +229,14 @@ function buildDispatchHeaders(params) {
221
229
  }
222
230
  function buildAckHeaders(opts) {
223
231
  return {
232
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
224
233
  [AAMP_HEADER.INTENT]: "task.ack",
225
234
  [AAMP_HEADER.TASK_ID]: opts.taskId
226
235
  };
227
236
  }
228
237
  function buildResultHeaders(params) {
229
238
  const headers = {
239
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
230
240
  [AAMP_HEADER.INTENT]: "task.result",
231
241
  [AAMP_HEADER.TASK_ID]: params.taskId,
232
242
  [AAMP_HEADER.STATUS]: params.status,
@@ -243,7 +253,8 @@ function buildResultHeaders(params) {
243
253
  }
244
254
  function buildHelpHeaders(params) {
245
255
  return {
246
- [AAMP_HEADER.INTENT]: "task.help",
256
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
257
+ [AAMP_HEADER.INTENT]: "task.help_needed",
247
258
  [AAMP_HEADER.TASK_ID]: params.taskId,
248
259
  [AAMP_HEADER.QUESTION]: params.question,
249
260
  [AAMP_HEADER.BLOCKED_REASON]: params.blockedReason,
@@ -493,7 +504,7 @@ var JmapPushClient = class extends TinyEmitter {
493
504
  * Process a received email.
494
505
  *
495
506
  * Priority:
496
- * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.result / task.help)
507
+ * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.result / task.help_needed)
497
508
  * 2. If In-Reply-To is present → emit 'reply' event so the application layer can
498
509
  * resolve the thread (inReplyTo → taskId via Redis/DB) and handle human replies.
499
510
  * 3. Otherwise → ignore (not an AAMP-related email)
@@ -541,7 +552,8 @@ var JmapPushClient = class extends TinyEmitter {
541
552
  case "task.result":
542
553
  this.emit("task.result", aampMsg);
543
554
  break;
544
- case "task.help":
555
+ case "task.help_needed":
556
+ this.emit("task.help_needed", aampMsg);
545
557
  this.emit("task.help", aampMsg);
546
558
  break;
547
559
  case "task.ack":
@@ -1240,7 +1252,8 @@ var AampClient = class extends TinyEmitter {
1240
1252
  this.jmapClient.on("task.result", (result) => {
1241
1253
  this.emit("task.result", result);
1242
1254
  });
1243
- this.jmapClient.on("task.help", (help) => {
1255
+ this.jmapClient.on("task.help_needed", (help) => {
1256
+ this.emit("task.help_needed", help);
1244
1257
  this.emit("task.help", help);
1245
1258
  });
1246
1259
  this.jmapClient.on("task.ack", (ack) => {
@@ -1523,7 +1536,7 @@ var src_default = {
1523
1536
  },
1524
1537
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1525
1538
  register(api) {
1526
- const cfg = api.pluginConfig ?? {};
1539
+ const cfg = api.config?.channels?.aamp ?? api.pluginConfig ?? {};
1527
1540
  api.registerChannel({
1528
1541
  id: "aamp",
1529
1542
  meta: { label: "AAMP" },
@@ -1803,8 +1816,8 @@ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
1803
1816
  api.logger.error(`[AAMP] Sub-task result processing failed: ${err.message}`);
1804
1817
  });
1805
1818
  });
1806
- aampClient.on("task.help", (help) => {
1807
- api.logger.info(`[AAMP] \u2190 task.help ${help.taskId} question="${help.question}" from=${help.from}`);
1819
+ aampClient.on("task.help_needed", (help) => {
1820
+ api.logger.info(`[AAMP] \u2190 task.help_needed ${help.taskId} question="${help.question}" from=${help.from}`);
1808
1821
  const waiter = waitingDispatches.get(help.taskId);
1809
1822
  if (waiter) {
1810
1823
  waitingDispatches.delete(help.taskId);
@@ -2264,7 +2277,7 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2264
2277
  suggestedOptions: p.suggestedOptions ?? [],
2265
2278
  inReplyTo: task.messageId || void 0
2266
2279
  });
2267
- api.logger.info(`[AAMP] \u2192 task.help ${task.taskId}`);
2280
+ api.logger.info(`[AAMP] \u2192 task.help_needed ${task.taskId}`);
2268
2281
  return {
2269
2282
  content: [
2270
2283
  {
@@ -2458,7 +2471,14 @@ Question: ${h.question}`,
2458
2471
  return { content: [{ type: "text", text: "Error: email parameter is required" }] };
2459
2472
  }
2460
2473
  try {
2461
- const res = await fetch(`${base}/api/aamp-check?email=${encodeURIComponent(email)}`);
2474
+ const discoveryRes = await fetch(`${base}/.well-known/aamp`);
2475
+ if (!discoveryRes.ok)
2476
+ throw new Error(`HTTP ${discoveryRes.status}`);
2477
+ const discovery = await discoveryRes.json();
2478
+ const apiUrl = discovery.api?.url;
2479
+ if (!apiUrl)
2480
+ throw new Error("AAMP discovery did not return api.url");
2481
+ const res = await fetch(`${base}${apiUrl}?action=aamp.mailbox.check&email=${encodeURIComponent(email)}`);
2462
2482
  if (!res.ok)
2463
2483
  throw new Error(`HTTP ${res.status}`);
2464
2484
  const data = await res.json();