@ai-ecoverse/slicc-agent 3.2.0 → 3.4.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/README.md +45 -1
- package/dist/control.js +44 -1
- package/dist/control.js.map +2 -2
- package/dist/host.js +5 -1
- package/dist/host.js.map +2 -2
- package/dist/kernel/activity.js +39 -0
- package/dist/kernel/activity.js.map +7 -0
- package/dist/kernel/env.js +16 -8
- package/dist/kernel/env.js.map +2 -2
- package/dist/licks/boot.js +66 -0
- package/dist/licks/boot.js.map +7 -0
- package/dist/licks/config.js +126 -0
- package/dist/licks/config.js.map +7 -0
- package/dist/licks/cron.js +120 -0
- package/dist/licks/cron.js.map +7 -0
- package/dist/licks/extension.js +123 -0
- package/dist/licks/extension.js.map +7 -0
- package/dist/licks/fswatch.js +149 -0
- package/dist/licks/fswatch.js.map +7 -0
- package/dist/licks/glob.js +47 -0
- package/dist/licks/glob.js.map +7 -0
- package/dist/licks/index.js +36 -0
- package/dist/licks/index.js.map +7 -0
- package/dist/licks/lick.js +102 -0
- package/dist/licks/lick.js.map +7 -0
- package/dist/licks/licks.js +181 -0
- package/dist/licks/licks.js.map +7 -0
- package/dist/licks/schedules.js +50 -0
- package/dist/licks/schedules.js.map +7 -0
- package/dist/licks/sources.js +138 -0
- package/dist/licks/sources.js.map +7 -0
- package/dist/licks/state.js +56 -0
- package/dist/licks/state.js.map +7 -0
- package/dist/main.js +23 -4
- package/dist/main.js.map +2 -2
- package/dist/services.js.map +2 -2
- package/dist/spectrum/agent-port.js +6 -1
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/spectrum/messages.js +94 -6
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/system.js +16 -0
- package/dist/system.js.map +2 -2
- package/dist/types/control.d.ts +6 -1
- package/dist/types/host.d.ts +2 -0
- package/dist/types/kernel/activity.d.ts +10 -0
- package/dist/types/kernel/env.d.ts +2 -0
- package/dist/types/licks/boot.d.ts +9 -0
- package/dist/types/licks/config.d.ts +35 -0
- package/dist/types/licks/cron.d.ts +16 -0
- package/dist/types/licks/extension.d.ts +22 -0
- package/dist/types/licks/fswatch.d.ts +20 -0
- package/dist/types/licks/glob.d.ts +1 -0
- package/dist/types/licks/index.d.ts +24 -0
- package/dist/types/licks/lick.d.ts +21 -0
- package/dist/types/licks/licks.d.ts +56 -0
- package/dist/types/licks/schedules.d.ts +25 -0
- package/dist/types/licks/sources.d.ts +38 -0
- package/dist/types/licks/state.d.ts +14 -0
- package/dist/types/services.d.ts +18 -0
- package/dist/types/spectrum/agent-port.d.ts +1 -1
- package/dist/types/spectrum/messages.d.ts +5 -1
- package/dist/types/system.d.ts +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SPECIAL = /[.+^$()|\\]/;
|
|
2
|
+
function braces(pattern, at) {
|
|
3
|
+
const end = pattern.indexOf("}", at);
|
|
4
|
+
if (end < 0) return void 0;
|
|
5
|
+
const options = pattern.slice(at + 1, end).split(",");
|
|
6
|
+
if (options.length < 2) return void 0;
|
|
7
|
+
return [`(?:${options.map((option) => source(option)).join("|")})`, end];
|
|
8
|
+
}
|
|
9
|
+
function klass(pattern, at) {
|
|
10
|
+
const end = pattern.indexOf("]", at + 2);
|
|
11
|
+
if (end < 0) return void 0;
|
|
12
|
+
const inner = pattern.slice(at + 1, end);
|
|
13
|
+
const negated = inner.startsWith("!");
|
|
14
|
+
const body = (negated ? inner.slice(1) : inner).replace(/[\\\]^]/g, "\\$&");
|
|
15
|
+
return [`[${negated ? "^/" : ""}${body}]`, end];
|
|
16
|
+
}
|
|
17
|
+
function source(pattern) {
|
|
18
|
+
let out = "";
|
|
19
|
+
for (let at = 0; at < pattern.length; at++) {
|
|
20
|
+
const char = pattern[at];
|
|
21
|
+
if (char === "*" && pattern[at + 1] === "*") {
|
|
22
|
+
const slash = pattern[at + 2] === "/";
|
|
23
|
+
out += slash ? "(?:.*/)?" : ".*";
|
|
24
|
+
at += slash ? 2 : 1;
|
|
25
|
+
} else if (char === "*") out += "[^/]*";
|
|
26
|
+
else if (char === "?") out += "[^/]";
|
|
27
|
+
else if (char === "{" && braces(pattern, at)) {
|
|
28
|
+
const [group, end] = braces(pattern, at);
|
|
29
|
+
out += group;
|
|
30
|
+
at = end;
|
|
31
|
+
} else if (char === "[" && klass(pattern, at)) {
|
|
32
|
+
const [group, end] = klass(pattern, at);
|
|
33
|
+
out += group;
|
|
34
|
+
at = end;
|
|
35
|
+
} else
|
|
36
|
+
out += SPECIAL.test(char) || char === "{" || char === "}" || char === "[" || char === "]" ? `\\${char}` : char;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function glob(pattern) {
|
|
41
|
+
const expression = new RegExp(`^${source(pattern)}$`);
|
|
42
|
+
return (path) => expression.test(path);
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
glob
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=glob.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/glob.ts"],
|
|
4
|
+
"sourcesContent": ["const SPECIAL = /[.+^$()|\\\\]/;\n\nfunction braces(pattern: string, at: number): [string, number] | undefined {\n const end = pattern.indexOf('}', at);\n if (end < 0) return undefined;\n const options = pattern.slice(at + 1, end).split(',');\n if (options.length < 2) return undefined;\n return [`(?:${options.map((option) => source(option)).join('|')})`, end];\n}\n\nfunction klass(pattern: string, at: number): [string, number] | undefined {\n const end = pattern.indexOf(']', at + 2);\n if (end < 0) return undefined;\n const inner = pattern.slice(at + 1, end);\n const negated = inner.startsWith('!');\n const body = (negated ? inner.slice(1) : inner).replace(/[\\\\\\]^]/g, '\\\\$&');\n return [`[${negated ? '^/' : ''}${body}]`, end];\n}\n\nfunction source(pattern: string): string {\n let out = '';\n for (let at = 0; at < pattern.length; at++) {\n const char = pattern[at] as string;\n if (char === '*' && pattern[at + 1] === '*') {\n const slash = pattern[at + 2] === '/';\n out += slash ? '(?:.*/)?' : '.*';\n at += slash ? 2 : 1;\n } else if (char === '*') out += '[^/]*';\n else if (char === '?') out += '[^/]';\n else if (char === '{' && braces(pattern, at)) {\n const [group, end] = braces(pattern, at) as [string, number];\n out += group;\n at = end;\n } else if (char === '[' && klass(pattern, at)) {\n const [group, end] = klass(pattern, at) as [string, number];\n out += group;\n at = end;\n } else\n out +=\n SPECIAL.test(char) || char === '{' || char === '}' || char === '[' || char === ']'\n ? `\\\\${char}`\n : char;\n }\n return out;\n}\n\nexport function glob(pattern: string): (path: string) => boolean {\n const expression = new RegExp(`^${source(pattern)}$`);\n return (path) => expression.test(path);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,UAAU;AAEhB,SAAS,OAAO,SAAiB,IAA0C;AACzE,QAAM,MAAM,QAAQ,QAAQ,KAAK,EAAE;AACnC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,UAAU,QAAQ,MAAM,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG;AACpD,MAAI,QAAQ,SAAS,EAAG,QAAO;AAC/B,SAAO,CAAC,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,KAAK,GAAG;AACzE;AAEA,SAAS,MAAM,SAAiB,IAA0C;AACxE,QAAM,MAAM,QAAQ,QAAQ,KAAK,KAAK,CAAC;AACvC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,QAAQ,MAAM,KAAK,GAAG,GAAG;AACvC,QAAM,UAAU,MAAM,WAAW,GAAG;AACpC,QAAM,QAAQ,UAAU,MAAM,MAAM,CAAC,IAAI,OAAO,QAAQ,YAAY,MAAM;AAC1E,SAAO,CAAC,IAAI,UAAU,OAAO,EAAE,GAAG,IAAI,KAAK,GAAG;AAChD;AAEA,SAAS,OAAO,SAAyB;AACvC,MAAI,MAAM;AACV,WAAS,KAAK,GAAG,KAAK,QAAQ,QAAQ,MAAM;AAC1C,UAAM,OAAO,QAAQ,EAAE;AACvB,QAAI,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,KAAK;AAC3C,YAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM;AAClC,aAAO,QAAQ,aAAa;AAC5B,YAAM,QAAQ,IAAI;AAAA,IACpB,WAAW,SAAS,IAAK,QAAO;AAAA,aACvB,SAAS,IAAK,QAAO;AAAA,aACrB,SAAS,OAAO,OAAO,SAAS,EAAE,GAAG;AAC5C,YAAM,CAAC,OAAO,GAAG,IAAI,OAAO,SAAS,EAAE;AACvC,aAAO;AACP,WAAK;AAAA,IACP,WAAW,SAAS,OAAO,MAAM,SAAS,EAAE,GAAG;AAC7C,YAAM,CAAC,OAAO,GAAG,IAAI,MAAM,SAAS,EAAE;AACtC,aAAO;AACP,WAAK;AAAA,IACP;AACE,aACE,QAAQ,KAAK,IAAI,KAAK,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS,MAC3E,KAAK,IAAI,KACT;AAAA,EACV;AACA,SAAO;AACT;AAEO,SAAS,KAAK,SAA4C;AAC/D,QAAM,aAAa,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG;AACpD,SAAO,CAAC,SAAS,WAAW,KAAK,IAAI;AACvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cronTask, licksExtension } from "./extension.js";
|
|
2
|
+
import { createLicks } from "./licks.js";
|
|
3
|
+
import { createLickSources } from "./sources.js";
|
|
4
|
+
function setupLicks(registry) {
|
|
5
|
+
let bind = () => void 0;
|
|
6
|
+
const host = new Promise((resolve) => {
|
|
7
|
+
bind = resolve;
|
|
8
|
+
});
|
|
9
|
+
const licks = createLicks(host);
|
|
10
|
+
const cron = cronTask(licks);
|
|
11
|
+
const lookup = async (id, context) => {
|
|
12
|
+
const found = await (await host).harness.conversation(id, context);
|
|
13
|
+
if (!found) throw new Error(`conversation ${id} is gone`);
|
|
14
|
+
return found;
|
|
15
|
+
};
|
|
16
|
+
registry.install(licksExtension(licks, lookup, cron));
|
|
17
|
+
return {
|
|
18
|
+
licks,
|
|
19
|
+
attach(agent, options) {
|
|
20
|
+
const { harness, root } = agent;
|
|
21
|
+
bind({ harness, resolve: async (_target) => agent.cone() });
|
|
22
|
+
return createLickSources({
|
|
23
|
+
...options,
|
|
24
|
+
harness,
|
|
25
|
+
root,
|
|
26
|
+
cone: () => agent.cone(),
|
|
27
|
+
licks,
|
|
28
|
+
cron
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
setupLicks
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { cronTask, licksExtension } from './extension.ts';\nimport type { LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n cone(): Conversation;\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const cron = cronTask(licks);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup, cron));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: async (_target: LickTarget) => agent.cone() });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n cron,\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,UAAU,sBAAsB;AAEzC,SAAS,mBAA+C;AACxD,SAAS,yBAA2C;AAsB7C,SAAS,WAAW,UAAgC;AACzD,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,OAAO,SAAS,KAAK;AAC3B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,QAAQ,IAAI,CAAC;AACpD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,OAAO,YAAwB,MAAM,KAAK,EAAE,CAAC;AACtE,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const LICK_CHANNELS = [
|
|
2
|
+
"webhook",
|
|
3
|
+
"cron",
|
|
4
|
+
"sprinkle",
|
|
5
|
+
"fswatch",
|
|
6
|
+
"session-reload",
|
|
7
|
+
"navigate",
|
|
8
|
+
"discovery",
|
|
9
|
+
"upgrade",
|
|
10
|
+
"workflow",
|
|
11
|
+
"bash",
|
|
12
|
+
"jshd",
|
|
13
|
+
"preview",
|
|
14
|
+
"cherry",
|
|
15
|
+
"scoop-notify",
|
|
16
|
+
"scoop-idle",
|
|
17
|
+
"scoop-wait",
|
|
18
|
+
"sudo-request"
|
|
19
|
+
];
|
|
20
|
+
const ENTITIES = {
|
|
21
|
+
"&": "&",
|
|
22
|
+
"<": "<",
|
|
23
|
+
">": ">",
|
|
24
|
+
'"': """,
|
|
25
|
+
"\n": " ",
|
|
26
|
+
"\r": " "
|
|
27
|
+
};
|
|
28
|
+
const DECODE = Object.fromEntries(
|
|
29
|
+
Object.entries(ENTITIES).map(([raw, entity]) => [entity, raw])
|
|
30
|
+
);
|
|
31
|
+
function escapeText(value) {
|
|
32
|
+
return value.replace(/[&<>]/g, (char) => ENTITIES[char]).replace(/\r/g, " ");
|
|
33
|
+
}
|
|
34
|
+
function escapeAttribute(value) {
|
|
35
|
+
return value.replace(/[&<>"\n\r]/g, (char) => ENTITIES[char]);
|
|
36
|
+
}
|
|
37
|
+
function unescape(value) {
|
|
38
|
+
return value.replace(/&(?:amp|lt|gt|quot|#10|#13);/g, (entity) => DECODE[entity]);
|
|
39
|
+
}
|
|
40
|
+
function lickId() {
|
|
41
|
+
return `lk-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
42
|
+
}
|
|
43
|
+
function oneLine(value) {
|
|
44
|
+
return value.replace(/\s+/g, " ").trim();
|
|
45
|
+
}
|
|
46
|
+
function formatLick(lick) {
|
|
47
|
+
const attributes = [
|
|
48
|
+
["id", lick.id],
|
|
49
|
+
["channel", lick.channel],
|
|
50
|
+
["source", lick.source],
|
|
51
|
+
["title", oneLine(lick.title)],
|
|
52
|
+
["count", String(lick.count)],
|
|
53
|
+
["at", new Date(lick.at).toISOString()]
|
|
54
|
+
];
|
|
55
|
+
if (lick.actions?.length) attributes.push(["actions", lick.actions.join(" ")]);
|
|
56
|
+
const head = attributes.map(([name, value]) => `${name}="${escapeAttribute(value)}"`).join(" ");
|
|
57
|
+
const lines = [escapeText(oneLine(lick.text))];
|
|
58
|
+
if (lick.body) lines.push(escapeText(lick.body));
|
|
59
|
+
return `<lick ${head}>
|
|
60
|
+
${lines.join("\n")}
|
|
61
|
+
</lick>`;
|
|
62
|
+
}
|
|
63
|
+
const LICK = /^<lick ((?:[a-z]+="[^"<>\n]*" ?)+)>\n([^\n]*)(?:\n([\s\S]*?))?\n<\/lick>$/;
|
|
64
|
+
const ATTRIBUTE = /([a-z]+)="([^"]*)"/g;
|
|
65
|
+
function isChannel(value) {
|
|
66
|
+
return LICK_CHANNELS.includes(value ?? "");
|
|
67
|
+
}
|
|
68
|
+
function parseLick(text) {
|
|
69
|
+
const match = LICK.exec(text);
|
|
70
|
+
if (!match) return void 0;
|
|
71
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const [, name, value] of match[1].matchAll(ATTRIBUTE))
|
|
73
|
+
attributes.set(name, unescape(value));
|
|
74
|
+
const channel = attributes.get("channel");
|
|
75
|
+
const id = attributes.get("id");
|
|
76
|
+
if (!id || !isChannel(channel)) return void 0;
|
|
77
|
+
const at = Date.parse(attributes.get("at") ?? "");
|
|
78
|
+
const count = Number(attributes.get("count"));
|
|
79
|
+
const actions = (attributes.get("actions") ?? "").split(" ").filter((action) => action === "confirm" || action === "dismiss");
|
|
80
|
+
const body = match[3] === void 0 ? void 0 : unescape(match[3]);
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
channel,
|
|
84
|
+
source: attributes.get("source") ?? "",
|
|
85
|
+
title: attributes.get("title") ?? "",
|
|
86
|
+
text: unescape(match[2]),
|
|
87
|
+
...body ? { body } : {},
|
|
88
|
+
count: Number.isSafeInteger(count) && count > 0 ? count : 1,
|
|
89
|
+
at: Number.isNaN(at) ? 0 : at,
|
|
90
|
+
...actions.length ? { actions } : {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
LICK_CHANNELS,
|
|
95
|
+
escapeAttribute,
|
|
96
|
+
escapeText,
|
|
97
|
+
formatLick,
|
|
98
|
+
lickId,
|
|
99
|
+
parseLick,
|
|
100
|
+
unescape
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=lick.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/lick.ts"],
|
|
4
|
+
"sourcesContent": ["export const LICK_CHANNELS = [\n 'webhook',\n 'cron',\n 'sprinkle',\n 'fswatch',\n 'session-reload',\n 'navigate',\n 'discovery',\n 'upgrade',\n 'workflow',\n 'bash',\n 'jshd',\n 'preview',\n 'cherry',\n 'scoop-notify',\n 'scoop-idle',\n 'scoop-wait',\n 'sudo-request',\n] as const;\n\nexport type LickChannel = (typeof LICK_CHANNELS)[number];\n\nexport type LickAction = 'confirm' | 'dismiss';\n\nexport type LickTarget = 'cone' | `scoop:${string}`;\n\nexport type Lick = {\n id: string;\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n count: number;\n at: number;\n actions?: LickAction[];\n};\n\nconst ENTITIES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n '\\n': ' ',\n '\\r': ' ',\n};\n\nconst DECODE: Record<string, string> = Object.fromEntries(\n Object.entries(ENTITIES).map(([raw, entity]) => [entity, raw])\n);\n\nexport function escapeText(value: string): string {\n return value.replace(/[&<>]/g, (char) => ENTITIES[char] as string).replace(/\\r/g, ' ');\n}\n\nexport function escapeAttribute(value: string): string {\n return value.replace(/[&<>\"\\n\\r]/g, (char) => ENTITIES[char] as string);\n}\n\nexport function unescape(value: string): string {\n return value.replace(/&(?:amp|lt|gt|quot|#10|#13);/g, (entity) => DECODE[entity] as string);\n}\n\nexport function lickId(): string {\n return `lk-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nexport function formatLick(lick: Lick): string {\n const attributes: [string, string][] = [\n ['id', lick.id],\n ['channel', lick.channel],\n ['source', lick.source],\n ['title', oneLine(lick.title)],\n ['count', String(lick.count)],\n ['at', new Date(lick.at).toISOString()],\n ];\n if (lick.actions?.length) attributes.push(['actions', lick.actions.join(' ')]);\n const head = attributes.map(([name, value]) => `${name}=\"${escapeAttribute(value)}\"`).join(' ');\n const lines = [escapeText(oneLine(lick.text))];\n if (lick.body) lines.push(escapeText(lick.body));\n return `<lick ${head}>\\n${lines.join('\\n')}\\n</lick>`;\n}\n\nconst LICK = /^<lick ((?:[a-z]+=\"[^\"<>\\n]*\" ?)+)>\\n([^\\n]*)(?:\\n([\\s\\S]*?))?\\n<\\/lick>$/;\nconst ATTRIBUTE = /([a-z]+)=\"([^\"]*)\"/g;\n\nfunction isChannel(value: string | undefined): value is LickChannel {\n return (LICK_CHANNELS as readonly string[]).includes(value ?? '');\n}\n\nexport function parseLick(text: string): Lick | undefined {\n const match = LICK.exec(text);\n if (!match) return undefined;\n const attributes = new Map<string, string>();\n for (const [, name, value] of (match[1] as string).matchAll(ATTRIBUTE))\n attributes.set(name as string, unescape(value as string));\n const channel = attributes.get('channel');\n const id = attributes.get('id');\n if (!id || !isChannel(channel)) return undefined;\n const at = Date.parse(attributes.get('at') ?? '');\n const count = Number(attributes.get('count'));\n const actions = (attributes.get('actions') ?? '')\n .split(' ')\n .filter((action): action is LickAction => action === 'confirm' || action === 'dismiss');\n const body = match[3] === undefined ? undefined : unescape(match[3]);\n return {\n id,\n channel,\n source: attributes.get('source') ?? '',\n title: attributes.get('title') ?? '',\n text: unescape(match[2] as string),\n ...(body ? { body } : {}),\n count: Number.isSafeInteger(count) && count > 0 ? count : 1,\n at: Number.isNaN(at) ? 0 : at,\n ...(actions.length ? { actions } : {}),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineDoc
|
|
3
|
+
} from "@earendil-works/pi-durable";
|
|
4
|
+
import {
|
|
5
|
+
formatLick,
|
|
6
|
+
lickId
|
|
7
|
+
} from "./lick.js";
|
|
8
|
+
import { entryLick, LICK_STATE_KIND, lickDecisions } from "./state.js";
|
|
9
|
+
const LicksOutbox = defineDoc({
|
|
10
|
+
kind: "slicc.licks",
|
|
11
|
+
version: 1,
|
|
12
|
+
scope: "session",
|
|
13
|
+
initial: () => ({ queues: {}, events: {} })
|
|
14
|
+
});
|
|
15
|
+
const MAX_ITEMS = 50;
|
|
16
|
+
const MAX_EVENTS = 64;
|
|
17
|
+
function render(pending) {
|
|
18
|
+
const shown = pending.items.slice(0, MAX_ITEMS);
|
|
19
|
+
const more = pending.items.length - shown.length;
|
|
20
|
+
const lines = [
|
|
21
|
+
...pending.body ? [pending.body] : [],
|
|
22
|
+
...shown,
|
|
23
|
+
...more > 0 ? [`\u2026 and ${more} more`] : []
|
|
24
|
+
];
|
|
25
|
+
const { body: _, ...lick } = pending.lick;
|
|
26
|
+
return lines.length ? { ...lick, body: lines.join("\n") } : lick;
|
|
27
|
+
}
|
|
28
|
+
function merge(pending, event, now) {
|
|
29
|
+
const items = [...pending.items];
|
|
30
|
+
for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);
|
|
31
|
+
return {
|
|
32
|
+
lick: {
|
|
33
|
+
...pending.lick,
|
|
34
|
+
title: event.title,
|
|
35
|
+
text: event.text,
|
|
36
|
+
count: pending.lick.count + (event.count ?? 1),
|
|
37
|
+
at: event.at ?? now
|
|
38
|
+
},
|
|
39
|
+
items,
|
|
40
|
+
body: event.body ?? pending.body
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function keyOf(event) {
|
|
44
|
+
const base = `${event.target}|${event.channel}|${event.source}`;
|
|
45
|
+
return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;
|
|
46
|
+
}
|
|
47
|
+
function createLicks(host) {
|
|
48
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
49
|
+
let lock = Promise.resolve();
|
|
50
|
+
const serial = (operation) => {
|
|
51
|
+
const run = lock.then(operation, operation);
|
|
52
|
+
lock = run.catch(() => void 0);
|
|
53
|
+
return run;
|
|
54
|
+
};
|
|
55
|
+
const actions = (channel) => {
|
|
56
|
+
const handler = handlers.get(channel);
|
|
57
|
+
const list = [];
|
|
58
|
+
if (handler?.confirm) list.push("confirm");
|
|
59
|
+
if (handler?.dismiss) list.push("dismiss");
|
|
60
|
+
return list.length ? list : void 0;
|
|
61
|
+
};
|
|
62
|
+
async function previous(harness, id, context) {
|
|
63
|
+
if (id === null) return "gone";
|
|
64
|
+
const submission = await harness.submission(id, context);
|
|
65
|
+
const record = await submission?.status(context);
|
|
66
|
+
if (record?.status === "queued") return "queued";
|
|
67
|
+
return record?.status === "unanswered" && record.reason === "aborted" ? "withdrawn" : "gone";
|
|
68
|
+
}
|
|
69
|
+
async function settled(key, context) {
|
|
70
|
+
const { harness } = await host;
|
|
71
|
+
const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];
|
|
72
|
+
if (!queue) return void 0;
|
|
73
|
+
const before = await previous(harness, queue.submission, context);
|
|
74
|
+
if (before === "queued") return void 0;
|
|
75
|
+
if (queue.pending && before === "gone") return queue;
|
|
76
|
+
await harness.commit(async (tx) => {
|
|
77
|
+
const doc = await tx.doc(LicksOutbox);
|
|
78
|
+
if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];
|
|
79
|
+
}, context);
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
async function flushKey(key, context) {
|
|
83
|
+
const { harness, resolve } = await host;
|
|
84
|
+
const queue = await settled(key, context);
|
|
85
|
+
if (!queue?.pending) return;
|
|
86
|
+
const lick = render(queue.pending);
|
|
87
|
+
const target = key.split("|")[0];
|
|
88
|
+
const conversation = await resolve(target, context);
|
|
89
|
+
const submission = await conversation.submit(
|
|
90
|
+
{
|
|
91
|
+
type: "input",
|
|
92
|
+
content: formatLick(lick),
|
|
93
|
+
whenBusy: "steer",
|
|
94
|
+
requestId: `lick:${lick.id}`
|
|
95
|
+
},
|
|
96
|
+
context
|
|
97
|
+
);
|
|
98
|
+
await harness.commit(async (tx) => {
|
|
99
|
+
const doc = await tx.doc(LicksOutbox);
|
|
100
|
+
doc.queues[key] = { submission: submission.id, pending: null };
|
|
101
|
+
}, context);
|
|
102
|
+
}
|
|
103
|
+
async function flushAll(context) {
|
|
104
|
+
const { harness } = await host;
|
|
105
|
+
const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});
|
|
106
|
+
for (const key of keys) await flushKey(key, context);
|
|
107
|
+
}
|
|
108
|
+
async function find(conversation, id, context) {
|
|
109
|
+
const { entries } = await conversation.context(context);
|
|
110
|
+
const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);
|
|
111
|
+
if (!lick) return void 0;
|
|
112
|
+
return { lick, decision: lickDecisions(entries).get(id) };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
deliver(event, context) {
|
|
116
|
+
return serial(async () => {
|
|
117
|
+
const { harness } = await host;
|
|
118
|
+
const key = keyOf(event);
|
|
119
|
+
await settled(key, context);
|
|
120
|
+
const stream = `${event.channel}|${event.source}`;
|
|
121
|
+
const now = event.at ?? Date.now();
|
|
122
|
+
const accepted = await harness.commit(async (tx) => {
|
|
123
|
+
const doc = await tx.doc(LicksOutbox);
|
|
124
|
+
const seen = doc.events[stream] ?? [];
|
|
125
|
+
if (event.eventId !== void 0) {
|
|
126
|
+
if (seen.includes(event.eventId)) return false;
|
|
127
|
+
doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);
|
|
128
|
+
}
|
|
129
|
+
const queue = doc.queues[key] ?? { submission: null, pending: null };
|
|
130
|
+
const listed = actions(event.channel);
|
|
131
|
+
queue.pending = queue.pending ? merge(queue.pending, event, now) : {
|
|
132
|
+
lick: {
|
|
133
|
+
id: lickId(),
|
|
134
|
+
channel: event.channel,
|
|
135
|
+
source: event.source,
|
|
136
|
+
title: event.title,
|
|
137
|
+
text: event.text,
|
|
138
|
+
count: event.count ?? 1,
|
|
139
|
+
at: now,
|
|
140
|
+
...listed ? { actions: listed } : {}
|
|
141
|
+
},
|
|
142
|
+
items: [...new Set(event.items ?? [])],
|
|
143
|
+
body: event.body ?? null
|
|
144
|
+
};
|
|
145
|
+
doc.queues[key] = queue;
|
|
146
|
+
return true;
|
|
147
|
+
}, context);
|
|
148
|
+
if (accepted) await flushKey(key, context);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
flush: (context) => serial(() => flushAll(context)),
|
|
152
|
+
find,
|
|
153
|
+
async decide(conversation, id, action, reason, context) {
|
|
154
|
+
const found = await find(conversation, id, context);
|
|
155
|
+
if (!found) throw new Error(`There is no lick ${id} in this conversation.`);
|
|
156
|
+
if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);
|
|
157
|
+
if (!found.lick.actions?.includes(action))
|
|
158
|
+
throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);
|
|
159
|
+
const run = handlers.get(found.lick.channel)?.[action];
|
|
160
|
+
if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);
|
|
161
|
+
return await run(found.lick, reason, context) ?? `Lick ${id} ${action}ed.`;
|
|
162
|
+
},
|
|
163
|
+
handle(channel, handler) {
|
|
164
|
+
handlers.set(channel, handler);
|
|
165
|
+
},
|
|
166
|
+
async pending(context) {
|
|
167
|
+
const { harness } = await host;
|
|
168
|
+
const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};
|
|
169
|
+
return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending).length;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
LICK_STATE_KIND,
|
|
175
|
+
LicksOutbox,
|
|
176
|
+
MAX_EVENTS,
|
|
177
|
+
MAX_ITEMS,
|
|
178
|
+
createLicks,
|
|
179
|
+
render
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=licks.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/licks.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n eventId?: string;\n at?: number;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n return {\n lick: {\n ...pending.lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n return {\n deliver(event, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n const listed = actions(event.channel);\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineDoc
|
|
3
|
+
} from "@earendil-works/pi-durable";
|
|
4
|
+
const LicksHostDoc = defineDoc({
|
|
5
|
+
kind: "slicc.licks.host",
|
|
6
|
+
version: 1,
|
|
7
|
+
scope: "session",
|
|
8
|
+
initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 })
|
|
9
|
+
});
|
|
10
|
+
function same(entry, record) {
|
|
11
|
+
return entry !== void 0 && entry.schedule === record.schedule && entry.target === record.target && entry.message === record.message;
|
|
12
|
+
}
|
|
13
|
+
async function live(harness, task, context) {
|
|
14
|
+
const record = await harness.getTask(task, context);
|
|
15
|
+
return record !== void 0 && record.state.status !== "terminal";
|
|
16
|
+
}
|
|
17
|
+
async function reconcileCron(options, entries, context) {
|
|
18
|
+
const { harness } = options;
|
|
19
|
+
const wanted = new Map(entries.map((entry) => [entry.name, entry]));
|
|
20
|
+
const stored = (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};
|
|
21
|
+
const stale = [];
|
|
22
|
+
const create = entries.filter((entry) => !stored[entry.name]);
|
|
23
|
+
for (const [name, record] of Object.entries(stored)) {
|
|
24
|
+
const entry = wanted.get(name);
|
|
25
|
+
const running = await live(harness, record.task, context);
|
|
26
|
+
if (same(entry, record) && running) continue;
|
|
27
|
+
stale.push(name);
|
|
28
|
+
if (running) await harness.abortTask(record.task, context);
|
|
29
|
+
if (entry) create.push(entry);
|
|
30
|
+
}
|
|
31
|
+
if (!stale.length && !create.length) return;
|
|
32
|
+
await harness.commit(async (tx) => {
|
|
33
|
+
const doc = await tx.doc(LicksHostDoc);
|
|
34
|
+
for (const name of stale) delete doc.cron[name];
|
|
35
|
+
for (const entry of create) {
|
|
36
|
+
const input = { ...entry, from: options.now() };
|
|
37
|
+
const task = await tx.createTask(options.cron, input, {
|
|
38
|
+
ownership: { kind: "conversation" },
|
|
39
|
+
conversationId: options.root.id,
|
|
40
|
+
background: true
|
|
41
|
+
});
|
|
42
|
+
doc.cron[entry.name] = { ...entry, task };
|
|
43
|
+
}
|
|
44
|
+
}, context);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
LicksHostDoc,
|
|
48
|
+
reconcileCron
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=schedules.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/schedules.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type TaskId,\n} from '@earendil-works/pi-durable';\nimport type { CronEntry } from './config.ts';\nimport type { CronInput, cronTask } from './extension.ts';\n\ntype CronRecord = {\n schedule: string;\n target: string;\n message: string;\n task: number;\n};\n\ntype HostState = {\n cron: Record<string, CronRecord>;\n agent: string | null;\n boot: string | null;\n mark: number;\n upgrades: number;\n};\n\nexport const LicksHostDoc = defineDoc<HostState>({\n kind: 'slicc.licks.host',\n version: 1,\n scope: 'session',\n initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 }),\n});\n\nfunction same(entry: CronEntry | undefined, record: CronRecord): boolean {\n return (\n entry !== undefined &&\n entry.schedule === record.schedule &&\n entry.target === record.target &&\n entry.message === record.message\n );\n}\n\nasync function live(harness: Harness, task: number, context: Context): Promise<boolean> {\n const record = await harness.getTask(task as TaskId, context);\n return record !== undefined && record.state.status !== 'terminal';\n}\n\nexport async function reconcileCron(\n options: {\n harness: Harness;\n root: Conversation;\n cron: ReturnType<typeof cronTask>;\n now: () => number;\n },\n entries: readonly CronEntry[],\n context: Context\n): Promise<void> {\n const { harness } = options;\n const wanted = new Map(entries.map((entry) => [entry.name, entry]));\n const stored: Readonly<Record<string, CronRecord>> =\n (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};\n const stale: string[] = [];\n const create: CronEntry[] = entries.filter((entry) => !stored[entry.name]);\n for (const [name, record] of Object.entries(stored)) {\n const entry = wanted.get(name);\n const running = await live(harness, record.task, context);\n if (same(entry, record) && running) continue;\n stale.push(name);\n if (running) await harness.abortTask(record.task as TaskId, context);\n if (entry) create.push(entry);\n }\n if (!stale.length && !create.length) return;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksHostDoc);\n for (const name of stale) delete doc.cron[name];\n for (const entry of create) {\n const input: CronInput = { ...entry, from: options.now() };\n const task = await tx.createTask(options.cron, input, {\n ownership: { kind: 'conversation' },\n conversationId: options.root.id,\n background: true,\n });\n doc.cron[entry.name] = { ...entry, task };\n }\n }, context);\n}\n"],
|
|
5
|
+
"mappings": "AACA;AAAA,EAEE;AAAA,OAGK;AAmBA,MAAM,eAAe,UAAqB;AAAA,EAC/C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,MAAM,MAAM,GAAG,UAAU,EAAE;AAC5E,CAAC;AAED,SAAS,KAAK,OAA8B,QAA6B;AACvE,SACE,UAAU,UACV,MAAM,aAAa,OAAO,YAC1B,MAAM,WAAW,OAAO,UACxB,MAAM,YAAY,OAAO;AAE7B;AAEA,eAAe,KAAK,SAAkB,MAAc,SAAoC;AACtF,QAAM,SAAS,MAAM,QAAQ,QAAQ,MAAgB,OAAO;AAC5D,SAAO,WAAW,UAAa,OAAO,MAAM,WAAW;AACzD;AAEA,eAAsB,cACpB,SAMA,SACA,SACe;AACf,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAClE,QAAM,UACH,MAAM,QAAQ,SAAS,cAAc,OAAO,IAAI,QAAQ,CAAC;AAC5D,QAAM,QAAkB,CAAC;AACzB,QAAM,SAAsB,QAAQ,OAAO,CAAC,UAAU,CAAC,OAAO,MAAM,IAAI,CAAC;AACzE,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,QAAQ,OAAO,IAAI,IAAI;AAC7B,UAAM,UAAU,MAAM,KAAK,SAAS,OAAO,MAAM,OAAO;AACxD,QAAI,KAAK,OAAO,MAAM,KAAK,QAAS;AACpC,UAAM,KAAK,IAAI;AACf,QAAI,QAAS,OAAM,QAAQ,UAAU,OAAO,MAAgB,OAAO;AACnE,QAAI,MAAO,QAAO,KAAK,KAAK;AAAA,EAC9B;AACA,MAAI,CAAC,MAAM,UAAU,CAAC,OAAO,OAAQ;AACrC,QAAM,QAAQ,OAAO,OAAO,OAAO;AACjC,UAAM,MAAM,MAAM,GAAG,IAAI,YAAY;AACrC,eAAW,QAAQ,MAAO,QAAO,IAAI,KAAK,IAAI;AAC9C,eAAW,SAAS,QAAQ;AAC1B,YAAM,QAAmB,EAAE,GAAG,OAAO,MAAM,QAAQ,IAAI,EAAE;AACzD,YAAM,OAAO,MAAM,GAAG,WAAW,QAAQ,MAAM,OAAO;AAAA,QACpD,WAAW,EAAE,MAAM,eAAe;AAAA,QAClC,gBAAgB,QAAQ,KAAK;AAAA,QAC7B,YAAY;AAAA,MACd,CAAC;AACD,UAAI,KAAK,MAAM,IAAI,IAAI,EAAE,GAAG,OAAO,KAAK;AAAA,IAC1C;AAAA,EACF,GAAG,OAAO;AACZ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { bootLicks } from "./boot.js";
|
|
2
|
+
import { parseCrontab, parseWebhook } from "./config.js";
|
|
3
|
+
import { configFiles, Watches } from "./fswatch.js";
|
|
4
|
+
import { reconcileCron } from "./schedules.js";
|
|
5
|
+
const SLICC_DIR = ".slicc";
|
|
6
|
+
function hash(text) {
|
|
7
|
+
let value = 2166136261;
|
|
8
|
+
for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);
|
|
9
|
+
return (value >>> 0).toString(36);
|
|
10
|
+
}
|
|
11
|
+
function shortPath(path, home) {
|
|
12
|
+
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
13
|
+
}
|
|
14
|
+
function configLick(error, home) {
|
|
15
|
+
const file = shortPath(error.file, home);
|
|
16
|
+
const channel = error.file.includes("/watches/") ? "fswatch" : error.file.includes("/webhooks/") ? "webhook" : "cron";
|
|
17
|
+
const text = `${error.line ? `line ${error.line}: ` : ""}${error.error}`;
|
|
18
|
+
return {
|
|
19
|
+
channel,
|
|
20
|
+
source: `config:${file}`,
|
|
21
|
+
title: `Invalid ${file}`,
|
|
22
|
+
text,
|
|
23
|
+
body: "Ignored until the file is fixed.\nThe formats are in the slicc-agent README.",
|
|
24
|
+
target: "cone",
|
|
25
|
+
eventId: hash(`${file}
|
|
26
|
+
${text}`)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function webhookLick(name, entry, delivery) {
|
|
30
|
+
const payload = typeof delivery.body === "string" ? delivery.body : JSON.stringify(delivery.body, null, 2);
|
|
31
|
+
const headers = Object.entries(delivery.headers ?? {}).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
32
|
+
return {
|
|
33
|
+
channel: "webhook",
|
|
34
|
+
source: name,
|
|
35
|
+
title: name,
|
|
36
|
+
text: entry.message || `Webhook ${name} received a request`,
|
|
37
|
+
body: [headers, payload].filter(Boolean).join("\n\n"),
|
|
38
|
+
target: entry.target,
|
|
39
|
+
coalesce: false,
|
|
40
|
+
...delivery.id ? { eventId: delivery.id } : {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function webhooksIn(files, dir, into, context) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
into.clear();
|
|
46
|
+
for (const name of await files.list(dir, context)) {
|
|
47
|
+
const file = `${dir}/${name}`;
|
|
48
|
+
const json = await files.read(file, context);
|
|
49
|
+
if (json === void 0) continue;
|
|
50
|
+
try {
|
|
51
|
+
const entry = parseWebhook(file, json);
|
|
52
|
+
into.set(entry.name, entry);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
errors.push({ file, error: error.message });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
function createLickSources(options) {
|
|
60
|
+
const { harness, env, licks, home } = options;
|
|
61
|
+
const dir = `${home}/${SLICC_DIR}`;
|
|
62
|
+
const now = options.now ?? Date.now;
|
|
63
|
+
const files = configFiles(env);
|
|
64
|
+
const deliver = (event, context) => licks.deliver(event, context);
|
|
65
|
+
const watches = new Watches(env, deliver, home, options.activity);
|
|
66
|
+
const webhooks = /* @__PURE__ */ new Map();
|
|
67
|
+
let control;
|
|
68
|
+
let reconcileTimer;
|
|
69
|
+
let flushTimer;
|
|
70
|
+
let closed = false;
|
|
71
|
+
let lock = Promise.resolve();
|
|
72
|
+
const serial = (operation) => {
|
|
73
|
+
const run = lock.then(operation, operation);
|
|
74
|
+
lock = run.catch(() => void 0);
|
|
75
|
+
return run;
|
|
76
|
+
};
|
|
77
|
+
async function reconcileNow(context) {
|
|
78
|
+
if (closed) return;
|
|
79
|
+
const crontabFile = `${dir}/crontab`;
|
|
80
|
+
const crontab = parseCrontab(crontabFile, await files.read(crontabFile, context) ?? "");
|
|
81
|
+
await reconcileCron({ ...options, now }, crontab.entries, context);
|
|
82
|
+
const errors = [
|
|
83
|
+
...crontab.errors,
|
|
84
|
+
...await watches.reconcile(`${dir}/watches`, context),
|
|
85
|
+
...await webhooksIn(files, `${dir}/webhooks`, webhooks, context)
|
|
86
|
+
];
|
|
87
|
+
for (const error of errors) await deliver(configLick(error, home), context);
|
|
88
|
+
}
|
|
89
|
+
const reconcile = (context) => serial(() => reconcileNow(context));
|
|
90
|
+
function scheduleReconcile(context) {
|
|
91
|
+
clearTimeout(reconcileTimer);
|
|
92
|
+
reconcileTimer = setTimeout(() => {
|
|
93
|
+
void reconcile(context).catch(() => void 0);
|
|
94
|
+
}, 300);
|
|
95
|
+
}
|
|
96
|
+
async function flushIfWaiting(context) {
|
|
97
|
+
if (await licks.pending(context)) await licks.flush(context);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
async start(context) {
|
|
101
|
+
await licks.flush(context);
|
|
102
|
+
await reconcile(context);
|
|
103
|
+
const result = await env.watch(
|
|
104
|
+
[{ path: dir, recursive: true }],
|
|
105
|
+
() => scheduleReconcile(context),
|
|
106
|
+
context
|
|
107
|
+
);
|
|
108
|
+
if (result.ok) control = result.value;
|
|
109
|
+
flushTimer = setInterval(() => {
|
|
110
|
+
void flushIfWaiting(context).catch(() => void 0);
|
|
111
|
+
}, options.flushEvery ?? 2e3);
|
|
112
|
+
},
|
|
113
|
+
reconcile,
|
|
114
|
+
boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),
|
|
115
|
+
async webhook(name, delivery, context) {
|
|
116
|
+
const entry = webhooks.get(name);
|
|
117
|
+
if (!entry) return false;
|
|
118
|
+
await deliver(webhookLick(name, entry, delivery), context);
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
async close(context) {
|
|
122
|
+
closed = true;
|
|
123
|
+
clearTimeout(reconcileTimer);
|
|
124
|
+
clearInterval(flushTimer);
|
|
125
|
+
await control?.close(context);
|
|
126
|
+
await watches.close(context);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
SLICC_DIR,
|
|
132
|
+
configLick,
|
|
133
|
+
createLickSources,
|
|
134
|
+
hash,
|
|
135
|
+
shortPath,
|
|
136
|
+
webhookLick
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=sources.js.map
|