@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
|
|
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.
|
|
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
|
-
/**
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
row.
|
|
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
|
-
|
|
148
|
-
|
|
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;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specstream/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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/
|
|
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.
|
|
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
|
|
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.
|