@specstream/openclaw-plugin 0.1.0 → 0.1.2

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.2",
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.2",
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",
@@ -12,7 +12,7 @@ Read `list_find_rows` and follow each `next_cursor` through the `cursor` argumen
12
12
 
13
13
  When the person asks, `send_to_destination` sends selected results to Google Sheets, Excel, Notion, Airtable, HubSpot, Salesforce or an Outlook draft. An Outlook draft lands in the person's own Drafts folder and is never sent. `export_find` makes a download link as CSV, Excel, JSON or HTML. Reading, exporting and sharing cost nothing.
14
14
 
15
- Finding more, adding facts or rechecking is new work with its own reviewed price. Rechecks cost half.
15
+ Finding more, adding facts or rechecking is new work with its own reviewed price. A recheck costs half the rate for a row that changed and 1 cent for a row that did not.
16
16
 
17
17
  `create_capacity_schedule` sets up a monitor and is a write. It needs a scope, a cadence, a validity period and limits per run and per period. A monitor reruns on its schedule and labels each change: still matches, no longer matches, unresolved or unavailable. Creating one does not fund it. A blocked or missed run is not an unchanged result and is not bought later. Pause or revoke a monitor when the owner says so.
18
18
 
@@ -10,6 +10,6 @@ SpecStream is the live web data engine for teams and agents. Use the configured
10
10
 
11
11
  Call `quote_capacity_work` with the `ast_hash` from `compile`, the operation, the number of results wanted, the useful minimum, and the original `parent_find_id`, `project_id` or `revision_id` when one applies. Asking for more uses `extend`, adding facts uses `add_facts` and rechecking uses `refresh`.
12
12
 
13
- Say the scope, the conditions, the maximum price and the expiry in text. Results cost 10 cents, 30 cents or $1 each, plus 10 cents for a person's email and 25 cents for a phone, and nothing when nothing matches. A price lasts 30 minutes. The price does not spend any balance and does not start a job.
13
+ Say the scope, the conditions, the maximum price and the expiry in text. Results cost 5 cents, 15 cents or $1 each, plus 5 cents for a person's email and 15 cents for a phone, and nothing when nothing matches. A price lasts 30 minutes. The price does not spend any balance and does not start a job.
14
14
 
15
15
  Use `get_capacity_balance` to read the balance. A balance is not permission to spend. Do not copy the maximum into an approval unless the person said to, or an existing bounded task already allows it. Plans and top-ups live in the person's account, not in chat.