@specstream/openclaw-plugin 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @specstream/openclaw-plugin
2
2
 
3
- Ask your OpenClaw agent for a list and get the whole list back. "Dental clinics in Lisbon with online booking, with their website and phone." Every row comes from the live web with the page it was read from, so you can open it and look. You pay per match, and a search that finds nothing costs nothing.
3
+ Ask your OpenClaw agent for a list and get the whole list back. "Dental clinics in Lisbon with online booking, with their website and phone." Every row comes from the live web with the page it was read from, so you can open it and look. You pay for each result delivered, and a search that finds nothing costs nothing.
4
4
 
5
5
  [SpecStream](https://specstream.ai) is the live web data engine for teams and agents. This plugin gives OpenClaw two tools for it and, if you want the full conversation with SpecStream, four skills.
6
6
 
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** The SpecStream REST calls the tools share: read a request, price it, start it within a maximum, read its rows. */
2
2
  export const BASE_URL = "https://api.specstream.ai";
3
- export const USER_AGENT = "@specstream/openclaw-plugin/0.1.0";
3
+ export const USER_AGENT = "@specstream/openclaw-plugin/0.1.1";
4
4
  /** A list runs for up to about five minutes, so the default wait is just under that. */
5
5
  export const DEFAULT_WAIT_SECONDS = 300;
6
6
  export const DEFAULT_POLL_INTERVAL_SECONDS = 3;
@@ -25,16 +25,29 @@ function dollars(cents) {
25
25
  export function listLink(findId) {
26
26
  return `https://specstream.ai/workspace/collections/${encodeURIComponent(findId)}`;
27
27
  }
28
- /** Flattens one record into a row: each column label maps to what the list shows, plus where the row was read. */
29
- export function flattenRow(findId, record) {
28
+ /** Each value's page in the API's flat row: <field>_source beside the <field> it belongs to. */
29
+ function sourcesOf(flat) {
30
+ const sources = {};
31
+ for (const [key, value] of Object.entries(flat ?? {})) {
32
+ if (key.endsWith("_source") && Object.prototype.hasOwnProperty.call(flat, key.slice(0, -7)))
33
+ sources[key] = value;
34
+ }
35
+ return sources;
36
+ }
37
+ /**
38
+ * One row as flat fields: each column's label holds what the list shows. The page of each value (<field>_source)
39
+ * and of the fact that put the row in the list (source_url, checked_at) come from the API's flat row, so every
40
+ * tool shows the same pages.
41
+ */
42
+ export function flattenRow(findId, record, flat) {
30
43
  const row = { list_id: findId, position: record.ordinal ?? null };
31
- let source = null;
44
+ const sources = sourcesOf(flat);
32
45
  const cells = record.details?.length ? record.details : (record.cells ?? []);
33
46
  for (const cell of cells) {
34
47
  let label = String(cell.label || cell.column_id || "").trim();
35
48
  if (!label)
36
49
  continue;
37
- if (RESERVED.has(label))
50
+ if (RESERVED.has(label) || label in sources)
38
51
  label = `${label} (column)`;
39
52
  if (cell.absence)
40
53
  row[label] = "";
@@ -42,12 +55,10 @@ export function flattenRow(findId, record) {
42
55
  row[label] = cell.display;
43
56
  else
44
57
  row[label] = cell.value ?? "";
45
- const provenance = cell.provenance || {};
46
- if (source === null && String(provenance.source_url || "").startsWith("http"))
47
- source = provenance;
48
58
  }
49
- row.source_url = source ? (source.source_url ?? "") : "";
50
- row.checked_at = source ? (source.observed_at ?? "") : "";
59
+ Object.assign(row, sources);
60
+ row.source_url = flat?.source_url ?? "";
61
+ row.checked_at = flat?.checked_at ?? "";
51
62
  row.list_link = listLink(findId);
52
63
  return row;
53
64
  }
@@ -144,8 +155,10 @@ export class SpecStreamClient {
144
155
  const query = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
145
156
  const page = (await this.request("GET", `/v1/finds/${encodeURIComponent(findId)}/rows?${query}`, undefined, { abortSignal })) ?? {};
146
157
  state = typeof page.status === "string" && page.status ? page.status : state;
147
- for (const record of Array.isArray(page.rows) ? page.rows : [])
148
- rows.push(flattenRow(findId, record));
158
+ const flat = new Map((Array.isArray(page.flat_rows) ? page.flat_rows : []).map((item) => [String(item.id), item]));
159
+ for (const record of Array.isArray(page.rows) ? page.rows : []) {
160
+ rows.push(flattenRow(findId, record, flat.get(`${findId}:${String(record.entity_id)}`)));
161
+ }
149
162
  const next = typeof page.next_cursor === "string" && page.next_cursor ? page.next_cursor : null;
150
163
  if (!next || next === cursor)
151
164
  break;
@@ -5,7 +5,7 @@
5
5
  "categories": [
6
6
  "data-analytics"
7
7
  ],
8
- "version": "0.1.0",
8
+ "version": "0.1.1",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specstream/openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SpecStream plugin for OpenClaw: ask for any list in plain words and get every matching row from the live web, each with its source page, priced per match.",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/Specstreamai/specstream-tools.git",
27
+ "url": "git+https://github.com/specstream/specstream-tools.git",
28
28
  "directory": "integrations/openclaw"
29
29
  },
30
30
  "homepage": "https://specstream.ai/docs/mcp",