sitelooper 0.3.0
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/LICENSE +21 -0
- package/README.md +625 -0
- package/bin/sitelooper.js +6 -0
- package/dist/agent/llm.js +460 -0
- package/dist/agent/llm.js.map +1 -0
- package/dist/agent/loop.js +870 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/agent/prompt.js +40 -0
- package/dist/agent/prompt.js.map +1 -0
- package/dist/agent/report.js +545 -0
- package/dist/agent/report.js.map +1 -0
- package/dist/agent/tools.js +1147 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/cli.js +1692 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon/browser.js +218 -0
- package/dist/daemon/browser.js.map +1 -0
- package/dist/daemon/codegen.js +241 -0
- package/dist/daemon/codegen.js.map +1 -0
- package/dist/daemon/dialogs.js +57 -0
- package/dist/daemon/dialogs.js.map +1 -0
- package/dist/daemon/diff.js +198 -0
- package/dist/daemon/diff.js.map +1 -0
- package/dist/daemon/fingerprint.js +98 -0
- package/dist/daemon/fingerprint.js.map +1 -0
- package/dist/daemon/inputs.js +134 -0
- package/dist/daemon/inputs.js.map +1 -0
- package/dist/daemon/recorder.js +1232 -0
- package/dist/daemon/recorder.js.map +1 -0
- package/dist/daemon/refs.js +194 -0
- package/dist/daemon/refs.js.map +1 -0
- package/dist/daemon/server.js +1724 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/state.js +239 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/doctor.js +90 -0
- package/dist/doctor.js.map +1 -0
- package/dist/shared/paths.js +80 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/protocol.js +28 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/secrets.js +92 -0
- package/dist/shared/secrets.js.map +1 -0
- package/dist/shared/text.js +39 -0
- package/dist/shared/text.js.map +1 -0
- package/dist/skills/compile.js +1420 -0
- package/dist/skills/compile.js.map +1 -0
- package/dist/skills/components.js +456 -0
- package/dist/skills/components.js.map +1 -0
- package/dist/skills/flow.js +1041 -0
- package/dist/skills/flow.js.map +1 -0
- package/dist/skills/learn.js +406 -0
- package/dist/skills/learn.js.map +1 -0
- package/dist/skills/ledger.js +304 -0
- package/dist/skills/ledger.js.map +1 -0
- package/dist/skills/relabel.js +206 -0
- package/dist/skills/relabel.js.map +1 -0
- package/dist/skills/repair.js +570 -0
- package/dist/skills/repair.js.map +1 -0
- package/dist/skills/replay.js +1281 -0
- package/dist/skills/replay.js.map +1 -0
- package/dist/skills/store.js +147 -0
- package/dist/skills/store.js.map +1 -0
- package/dist/spec/check.js +428 -0
- package/dist/spec/check.js.map +1 -0
- package/dist/spec/diagnostics.js +58 -0
- package/dist/spec/diagnostics.js.map +1 -0
- package/dist/spec/emit.js +2084 -0
- package/dist/spec/emit.js.map +1 -0
- package/dist/spec/index.js +62 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/ir.js +216 -0
- package/dist/spec/ir.js.map +1 -0
- package/dist/spec/lift.js +162 -0
- package/dist/spec/lift.js.map +1 -0
- package/dist/spec/locators.js +270 -0
- package/dist/spec/locators.js.map +1 -0
- package/dist/spec/lower.js +124 -0
- package/dist/spec/lower.js.map +1 -0
- package/dist/spec/repair.js +657 -0
- package/dist/spec/repair.js.map +1 -0
- package/dist/spec/rerecord.js +169 -0
- package/dist/spec/rerecord.js.map +1 -0
- package/dist/spec/rethread.js +120 -0
- package/dist/spec/rethread.js.map +1 -0
- package/package.json +50 -0
- package/skills/sitelooper/SKILL.md +228 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret interpolation: `{{env:NAME}}` markers resolve to environment
|
|
3
|
+
* variables at the moment a tool executes, and ONLY there.
|
|
4
|
+
*
|
|
5
|
+
* The marker — not the value — is what travels everywhere else: the
|
|
6
|
+
* instruction the model reads, the recorded steps, compiled skills, flows,
|
|
7
|
+
* transcripts, and every artifact the bench publishes. The inner model
|
|
8
|
+
* copies the marker into its fill/type call verbatim and never sees the
|
|
9
|
+
* secret at all, so nothing upstream of the browser can leak it — the
|
|
10
|
+
* provider included. Skills recorded this way also survive credential
|
|
11
|
+
* rotation for free: replay re-resolves the marker each run.
|
|
12
|
+
*
|
|
13
|
+
* Scrubbing is the backstop for the one remaining channel: the PAGE. A form
|
|
14
|
+
* that echoes a typed value (a visible username, a confirmation banner)
|
|
15
|
+
* would put it into snapshots, reads, and diffs — so every tool result and
|
|
16
|
+
* diff is passed back through `scrubSecrets`, which replaces any value the
|
|
17
|
+
* session has resolved with its marker.
|
|
18
|
+
*
|
|
19
|
+
* The ledger is process-wide, which is session-wide by construction: one
|
|
20
|
+
* daemon process per session.
|
|
21
|
+
*/
|
|
22
|
+
const SECRET_RE = /\{\{env:([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
|
|
23
|
+
/** Values resolved this session, marker name → value. */
|
|
24
|
+
const ledger = new Map();
|
|
25
|
+
/** Minimum value length the scrubber will replace: shorter values would
|
|
26
|
+
* false-positive on ordinary page text ("1234" in a price). A secret this
|
|
27
|
+
* short is unsafe for unrelated reasons; the resolver still resolves it. */
|
|
28
|
+
const MIN_SCRUB_LEN = 4;
|
|
29
|
+
export function hasSecretMarker(text) {
|
|
30
|
+
SECRET_RE.lastIndex = 0;
|
|
31
|
+
return SECRET_RE.test(text);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Replace every `{{env:NAME}}` in `text` with the environment's value.
|
|
35
|
+
* Throws on an unset variable — a silently-unresolved marker would be typed
|
|
36
|
+
* into the page literally, which is never what the caller meant.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveSecrets(text) {
|
|
39
|
+
return text.replace(SECRET_RE, (_m, name) => {
|
|
40
|
+
const value = process.env[name];
|
|
41
|
+
if (value === undefined || value === '') {
|
|
42
|
+
throw new Error(`secret {{env:${name}}} cannot be resolved — the environment variable ${name} is not set where the daemon runs. ` +
|
|
43
|
+
`Set it and restart the session (sitelooper stop, then re-run with ${name} exported).`);
|
|
44
|
+
}
|
|
45
|
+
ledger.set(name, value);
|
|
46
|
+
return value;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** resolveSecrets over every string field of a tool's args (non-mutating). */
|
|
50
|
+
export function resolveSecretsDeep(value) {
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return (hasSecretMarker(value) ? resolveSecrets(value) : value);
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.map((v) => resolveSecretsDeep(v));
|
|
55
|
+
if (value && typeof value === 'object') {
|
|
56
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveSecretsDeep(v)]));
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Replace every session-resolved secret VALUE in `text` with its marker.
|
|
62
|
+
* Applied to tool results and page diffs — anything the page echoed back.
|
|
63
|
+
*/
|
|
64
|
+
export function scrubSecrets(text) {
|
|
65
|
+
let out = text;
|
|
66
|
+
// Longest first: a shorter secret that is a substring of a longer one
|
|
67
|
+
// (USER=james, PASS=james2024!) would otherwise split the longer value and
|
|
68
|
+
// let its tail through.
|
|
69
|
+
for (const [name, value] of [...ledger].sort((a, b) => b[1].length - a[1].length)) {
|
|
70
|
+
if (value.length < MIN_SCRUB_LEN)
|
|
71
|
+
continue;
|
|
72
|
+
if (out.includes(value))
|
|
73
|
+
out = out.split(value).join(`{{env:${name}}}`);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** scrubSecrets over every string in a structure (non-mutating). */
|
|
78
|
+
export function scrubSecretsDeep(value) {
|
|
79
|
+
if (typeof value === 'string')
|
|
80
|
+
return scrubSecrets(value);
|
|
81
|
+
if (Array.isArray(value))
|
|
82
|
+
return value.map((v) => scrubSecretsDeep(v));
|
|
83
|
+
if (value && typeof value === 'object') {
|
|
84
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrubSecretsDeep(v)]));
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
/** Test seam: forget resolved values (a daemon process never needs this). */
|
|
89
|
+
export function clearSecretLedger() {
|
|
90
|
+
ledger.clear();
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/shared/secrets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** `text` cut to `max` characters with an ellipsis, or unchanged when it fits. */
|
|
2
|
+
export function clip(text, max) {
|
|
3
|
+
return text.length <= max ? text : text.slice(0, max) + '…';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Clock and calendar tokens are the RECORDING's moment, not part of what a
|
|
7
|
+
* page element IS: kanboard names its due-date textbox after the current
|
|
8
|
+
* minute ("09/03/2026 07:22") and labels a summary row "Due date:
|
|
9
|
+
* 12/31/2026 07:40". Recorded verbatim, such a name matches nothing nine
|
|
10
|
+
* minutes later. maskVolatile() turns each token into the `{{*}}` wildcard
|
|
11
|
+
* for stored expectations; volatileMatcher() turns a recorded locator name
|
|
12
|
+
* into a RegExp that treats the same tokens as wildcards.
|
|
13
|
+
*/
|
|
14
|
+
export const WILDCARD = '{{*}}';
|
|
15
|
+
const VOLATILE_TOKEN = /\b\d{1,2}:\d{2}(?::\d{2})?\b|\b\d{1,4}[/.-]\d{1,2}[/.-]\d{1,4}\b/g;
|
|
16
|
+
export function maskVolatile(line) {
|
|
17
|
+
return line.replace(VOLATILE_TOKEN, WILDCARD);
|
|
18
|
+
}
|
|
19
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
/**
|
|
21
|
+
* The recorded string itself when it carries no volatile token, else an
|
|
22
|
+
* anchored RegExp in which each token matches any token of the same shape —
|
|
23
|
+
* another time, another date — and nothing else, so the rest of the name is
|
|
24
|
+
* still matched exactly.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* What one masked token stood for, as regex SOURCE. Exported because a
|
|
28
|
+
* compiled spec has to rebuild the same matcher from a line the store already
|
|
29
|
+
* masked (see spec/locators.ts, maskedMatcherSource) and a second copy of this
|
|
30
|
+
* shape would be free to drift away from this one.
|
|
31
|
+
*/
|
|
32
|
+
export const VOLATILE_TOKEN_SHAPE = '(?:\\d{1,2}:\\d{2}(?::\\d{2})?|\\d{1,4}[/.-]\\d{1,2}[/.-]\\d{1,4})';
|
|
33
|
+
export function volatileMatcher(text) {
|
|
34
|
+
const masked = maskVolatile(text);
|
|
35
|
+
if (masked === text)
|
|
36
|
+
return text;
|
|
37
|
+
return new RegExp(`^${masked.split(WILDCARD).map(escapeRe).join(VOLATILE_TOKEN_SHAPE)}$`);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/shared/text.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW;IAC5C,OAAO,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AAC9D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAChC,MAAM,cAAc,GAAG,mEAAmE,CAAC;AAE3F,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAEzE;;;;;GAKG;AACH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,oEAAoE,CAAC;AACzG,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;AAC5F,CAAC"}
|