@cairnvibe/sdk 0.2.5 → 0.2.6
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/cairn-widget.js +2 -2
- package/dist/index.js +56 -19
- package/dist/realtime-server.d.ts +19 -2
- package/dist/realtime-server.js +75 -15
- package/dist/server.js +38 -13
- package/dist/verb-executor.js +45 -5
- package/package.json +1 -1
- package/src/index.tsx +55 -19
- package/src/realtime-server.ts +89 -16
- package/src/server.ts +38 -13
- package/src/verb-executor.ts +45 -5
package/src/server.ts
CHANGED
|
@@ -146,7 +146,22 @@ export async function resolveVerb(
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (parsedVerb.data.verb === "do" && !registeredActions.includes(parsedVerb.data.action)) {
|
|
149
|
-
|
|
149
|
+
// Not a manually registered action — the auto-discovery fallback: does
|
|
150
|
+
// "target" name a real element on the CURRENT page that the indexer
|
|
151
|
+
// itself found a real, mutating handler call on? If so, attach that
|
|
152
|
+
// call here (never something the model emitted itself — see
|
|
153
|
+
// ApiCallSchema's doc comment) so the client can execute exactly the
|
|
154
|
+
// same request a real click on that element would already make.
|
|
155
|
+
// Anything else — no target, an unknown target, or a real target with
|
|
156
|
+
// no discoverable apiCall (a client-only handler, a dynamic per-row
|
|
157
|
+
// URL — see manifest.ts's parseApiCall) — stays refused, same as before.
|
|
158
|
+
const target = parsedVerb.data.target;
|
|
159
|
+
const pageElements = manifest.pages.find((p) => p.route === input.route)?.elements ?? [];
|
|
160
|
+
const targetElement = target ? pageElements.find((e) => e.id === target) : undefined;
|
|
161
|
+
if (!targetElement?.apiCall) {
|
|
162
|
+
return { verb: "explain", text: "That action isn't available here." };
|
|
163
|
+
}
|
|
164
|
+
return { ...parsedVerb.data, apiCall: targetElement.apiCall };
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
// tour is allowed at every tier (see TIER_ALLOWED_VERBS) because
|
|
@@ -310,9 +325,11 @@ function buildVerbToolSchema(registeredActions: string[]): Record<string, unknow
|
|
|
310
325
|
),
|
|
311
326
|
route: nullableString("A route from the manifest. Required for navigate. null (or omitted) if not applicable."),
|
|
312
327
|
action: nullableString(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
328
|
+
"Required for do. A short label for what's being done, e.g. \"archive-invoice\" " +
|
|
329
|
+
(registeredActions.length
|
|
330
|
+
? `— either one of this deployment's registered actions [${registeredActions.join(", ")}], or, for any other element from currentPageElements whose own description says it performs a real action, any short label describing it.`
|
|
331
|
+
: "for any element from currentPageElements whose own description says it performs a real action — no actions are separately registered in this deployment, but currentPageElements-driven actions still work.") +
|
|
332
|
+
" null (or omitted) if not applicable.",
|
|
316
333
|
),
|
|
317
334
|
steps: {
|
|
318
335
|
type: "array",
|
|
@@ -383,15 +400,23 @@ Always call ${VERB_TOOL_NAME} exactly once with one of these verbs:
|
|
|
383
400
|
spans more than one page (e.g. "how do I get from here to Settings and
|
|
384
401
|
turn on X"), a step may also carry a "route" to move there first — most
|
|
385
402
|
steps should NOT set this; only the step where the page actually changes.
|
|
386
|
-
- do:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
403
|
+
- do: trigger a real action. Two ways this is allowed — anything else, refuse:
|
|
404
|
+
1. One of this deployment's registered action ids: [${registeredActions.join(", ") || "none registered"}].
|
|
405
|
+
Put that exact id in "action".
|
|
406
|
+
2. Any element in "currentPageElements" whose own description says it
|
|
407
|
+
performs a real action (e.g. "Archives this invoice", "Starts a phone
|
|
408
|
+
call", "Submits the form") — put that element's id in "target" and a
|
|
409
|
+
short label describing what it does in "action". This only works for
|
|
410
|
+
an element on the CURRENT page (it must be in currentPageElements) and
|
|
411
|
+
only for an action that doesn't depend on which specific row/instance
|
|
412
|
+
— a generic page-level button, not "archive row 3 of this table". If
|
|
413
|
+
the user means one specific item among several repeated ones, that's
|
|
414
|
+
not currently supported through this path — use "explain" and say so,
|
|
415
|
+
don't guess at a specific instance.
|
|
416
|
+
If neither applies — the action isn't registered and isn't a real element
|
|
417
|
+
on this page, or it needs picking a specific instance — use "explain" and
|
|
418
|
+
say you can't do that from here. Never invent a target or action id that
|
|
419
|
+
isn't in currentPageElements or the registered list above.
|
|
395
420
|
|
|
396
421
|
Every "text" field (in explain, or per-step in tour, or the optional text on
|
|
397
422
|
any other verb) is read aloud AND shown on screen, so it must sound like a
|
package/src/verb-executor.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// explain — never guess, never wrong-click"). The server (`server.ts`)
|
|
6
6
|
// enforces the same schema independently — never trust the client alone.
|
|
7
7
|
|
|
8
|
-
import { VerbResponseSchema, type TourStep, type VerbResponse } from "@cairnvibe/core";
|
|
8
|
+
import { VerbResponseSchema, type ApiCall, type TourStep, type VerbResponse } from "@cairnvibe/core";
|
|
9
9
|
import { findElement, highlightElement, logMiss, type MissContext } from "./element-ladder";
|
|
10
10
|
|
|
11
11
|
export interface VerbExecutorOptions {
|
|
@@ -61,12 +61,32 @@ function dispatchVerb(verb: VerbResponse, route: string, options: VerbExecutorOp
|
|
|
61
61
|
|
|
62
62
|
case "do": {
|
|
63
63
|
const allowed = options.registeredActions ?? [];
|
|
64
|
-
if (
|
|
65
|
-
|
|
64
|
+
if (allowed.includes(verb.action)) {
|
|
65
|
+
// Explicit, developer-owned path — unchanged.
|
|
66
|
+
options.onDo?.(verb.action, verb.target);
|
|
67
|
+
if (verb.text) options.onExplain(verb.text);
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
if (verb.
|
|
70
|
+
|
|
71
|
+
if (verb.apiCall) {
|
|
72
|
+
// Auto-discovered path: a real, indexer-found handler call on this
|
|
73
|
+
// exact target element, attached server-side after looking the
|
|
74
|
+
// target up in the manifest — never something the model emitted
|
|
75
|
+
// itself (see ApiCallSchema's doc comment in @cairnvibe/core).
|
|
76
|
+
const el = verb.target ? findElement(verb.target) : null;
|
|
77
|
+
if (el) highlightElement(el);
|
|
78
|
+
else if (verb.target) (options.onMiss ?? logMiss)({ attempted: verb.target, route });
|
|
79
|
+
|
|
80
|
+
if (verb.text) options.onExplain(verb.text);
|
|
81
|
+
void executeApiCall(verb.apiCall).then((result) => {
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
options.onExplain("I tried to do that, but something went wrong — try again in a moment.");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
options.onExplain(verb.text ?? "That action isn't available here.");
|
|
70
90
|
return;
|
|
71
91
|
}
|
|
72
92
|
|
|
@@ -82,3 +102,23 @@ function dispatchVerb(verb: VerbResponse, route: string, options: VerbExecutorOp
|
|
|
82
102
|
return;
|
|
83
103
|
}
|
|
84
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fires exactly the same request a real click on the target element would
|
|
108
|
+
* already make — same-origin only (apiCall.url is always relative, never a
|
|
109
|
+
* different host), and `credentials: "same-origin"` so the browser attaches
|
|
110
|
+
* the user's own real session cookies, the same way a manual click would.
|
|
111
|
+
* No body is sent: l1-scan.ts's static capture only ever traces method+url,
|
|
112
|
+
* never a request body (which usually depends on runtime state a build-time
|
|
113
|
+
* scan can't see) — fine for the common trigger-style action (an id already
|
|
114
|
+
* baked into the URL, no other payload needed), a real gap for one that
|
|
115
|
+
* requires one.
|
|
116
|
+
*/
|
|
117
|
+
async function executeApiCall(apiCall: ApiCall): Promise<{ ok: boolean; status?: number }> {
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(apiCall.url, { method: apiCall.method, credentials: "same-origin" });
|
|
120
|
+
return { ok: res.ok, status: res.status };
|
|
121
|
+
} catch {
|
|
122
|
+
return { ok: false };
|
|
123
|
+
}
|
|
124
|
+
}
|