toolroll 0.9.34 → 0.9.37
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/CHANGELOG.md +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finished zone hands the zones after it (docs/plans/zod-revamp.md, item 8): its `{{stage.<id>}}` words and, by
|
|
3
|
+
* kind, more fields (a research zone's `{{stage.<id>.items}}` and `{{stage.<id>.report}}`), kept on the card as one
|
|
4
|
+
* versioned payload (`flow_card.outputs_json`) with any output too long to pass on attached whole.
|
|
5
|
+
*
|
|
6
|
+
* - `stageHandoffSchema` is one zone's handoff; `STAGE_HANDOFFS` says which of its fields each zone kind hands on. A
|
|
7
|
+
* template's `{{stage.<id>.<field>}}` is checked against it when a flow is saved (`stageReferenceProblems`), so a
|
|
8
|
+
* refusal names the path: `stages[1].instructions: stage.research.unknown is not available`.
|
|
9
|
+
* - `cardOutputsSchema` is what the card keeps: every handoff's fields flat (`<zone>`, `<zone>.items`, `<zone>.report`,
|
|
10
|
+
* as `{{stage.…}}` names them) and, for a zone whose output was over TEXT_LIMITS.stageOutput, the output whole: its
|
|
11
|
+
* `{{stage.<zone>}}` says where it is kept, and a task filed after it is given it whole (flowGoalCuts).
|
|
12
|
+
*
|
|
13
|
+
* Saved before this contract (version 0), a card's outputs were the flat map alone; they read exactly as every release
|
|
14
|
+
* read them: the string values, anything else skipped.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { parseContract, readVersioned, versioned } from "./contract.js";
|
|
18
|
+
/** One zone's handoff to the zones after it. */
|
|
19
|
+
export const stageHandoffSchema = z.strictObject({
|
|
20
|
+
text: z.string().describe("{{stage.<id>}}: what the zone produced, whole up to TEXT_LIMITS.stageOutput characters; a longer one says where it is kept whole."),
|
|
21
|
+
items: z.string().optional().describe("{{stage.<id>.items}}: what a research zone found, as a numbered list (title, why, link)."),
|
|
22
|
+
report: z.string().optional().describe("{{stage.<id>.report}}: a research zone's whole report (REPORT_FILL_LIMIT)."),
|
|
23
|
+
});
|
|
24
|
+
const words = stageHandoffSchema.pick({ text: true });
|
|
25
|
+
/**
|
|
26
|
+
* What each zone kind hands on, or null when it hands nothing on (it holds, asks, messages or ends). A sort hands on its
|
|
27
|
+
* decision in words (`sortWords` of the small-answers contract's SortDecision, src/contracts/sort-answer.ts), and a
|
|
28
|
+
* Send to me zone what it sent.
|
|
29
|
+
*/
|
|
30
|
+
export const STAGE_HANDOFFS = {
|
|
31
|
+
inbox: null, task: words, report: stageHandoffSchema, approval: null, check: words, "pull-request": words, update: words, notify: null,
|
|
32
|
+
sort: words, draft: words, request: words, email: words, tool: words, wait: words, teammate: words, send: words, choose: null, done: null,
|
|
33
|
+
};
|
|
34
|
+
/** The extra `{{stage.<id>.<field>}}` fields a zone kind hands on. */
|
|
35
|
+
export function stageFieldsOf(kind) {
|
|
36
|
+
const schema = STAGE_HANDOFFS[kind];
|
|
37
|
+
return schema === null ? [] : Object.keys(schema.shape).filter(field => field !== "text");
|
|
38
|
+
}
|
|
39
|
+
/** One zone's handoff, read from the card's flat outputs (a field it never wrote is left out; no words read as ""). */
|
|
40
|
+
export function stageHandoffOf(outputs, zone) {
|
|
41
|
+
const fields = Object.keys(stageHandoffSchema.shape).filter(field => field !== "text");
|
|
42
|
+
return { text: outputs[zone] ?? "", ...Object.fromEntries(fields.flatMap(field => outputs[`${zone}.${field}`] === undefined ? [] : [[field, outputs[`${zone}.${field}`]]])) };
|
|
43
|
+
}
|
|
44
|
+
/** The card's flat outputs with one zone's handoff in place of its last: a field this visit didn't hand on is gone, so
|
|
45
|
+
* nothing stale is left behind. */
|
|
46
|
+
export function withStageHandoff(outputs, zone, handoff) {
|
|
47
|
+
const read = stageHandoffSchema.parse(handoff);
|
|
48
|
+
const next = { ...outputs, [zone]: read.text };
|
|
49
|
+
for (const field of Object.keys(stageHandoffSchema.shape).filter(one => one !== "text")) {
|
|
50
|
+
const value = read[field];
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
delete next[`${zone}.${field}`];
|
|
53
|
+
else
|
|
54
|
+
next[`${zone}.${field}`] = value;
|
|
55
|
+
}
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
// ------------------------------------------------------------------ what the card keeps
|
|
59
|
+
export const CARD_OUTPUTS_VERSION = 1;
|
|
60
|
+
export const cardOutputsSchema = versioned(CARD_OUTPUTS_VERSION, {
|
|
61
|
+
outputs: z.object({}).catchall(z.string()).describe("Every {{stage.…}} value by its name: <zone>, and a research zone's <zone>.items and <zone>.report."),
|
|
62
|
+
attached: z.object({}).catchall(z.string()).describe("A zone's output in full when it was over TEXT_LIMITS.stageOutput characters, by zone."),
|
|
63
|
+
});
|
|
64
|
+
/** Version 0, the flat map every release before wrote: its string values, as they read them; anything else is skipped. */
|
|
65
|
+
export function upgradeFlatOutputs(raw) {
|
|
66
|
+
const outputs = raw !== null && typeof raw === "object" ? Object.fromEntries(Object.entries(raw).filter((entry) => typeof entry[1] === "string")) : {};
|
|
67
|
+
return { version: CARD_OUTPUTS_VERSION, outputs, attached: {} };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read a card's outputs. Version 1 as itself and a newer version refused plainly. Anything without a numeric `version`
|
|
71
|
+
* was saved before outputs carried one (a zone may be called `version`; its words were a string), and is read as the
|
|
72
|
+
* flat map it is.
|
|
73
|
+
*/
|
|
74
|
+
export function readCardOutputs(input) {
|
|
75
|
+
const legacy = input === null || typeof input !== "object" || Array.isArray(input) || typeof input["version"] !== "number";
|
|
76
|
+
return legacy ? parseContract(cardOutputsSchema, upgradeFlatOutputs(input)) : readVersioned(cardOutputsSchema, input);
|
|
77
|
+
}
|
|
78
|
+
/** A card's outputs as the store keeps them: unreadable JSON is the empty card it always was; `newer` is a row a newer
|
|
79
|
+
* Toolroll wrote, which this one reads as empty and never writes over. */
|
|
80
|
+
export function cardOutputsFromStore(json) {
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = JSON.parse(json);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
raw = {};
|
|
87
|
+
}
|
|
88
|
+
const read = readCardOutputs(raw);
|
|
89
|
+
if (read.ok)
|
|
90
|
+
return { outputs: read.value.outputs, attached: read.value.attached, issues: [], newer: false };
|
|
91
|
+
return { outputs: {}, attached: {}, issues: read.issues, newer: read.issues.some(one => one.kind === "newer-version") };
|
|
92
|
+
}
|
|
93
|
+
/** A card's outputs as the store saves them. A zone's attachment is kept only while its output is the pointer to it. */
|
|
94
|
+
export function cardOutputsForStore(outputs, attached) {
|
|
95
|
+
const kept = Object.fromEntries(Object.entries(attached).filter(([zone]) => outputs[zone] !== undefined));
|
|
96
|
+
return JSON.stringify(cardOutputsSchema.parse({ version: CARD_OUTPUTS_VERSION, outputs, attached: kept }));
|
|
97
|
+
}
|
|
98
|
+
// ------------------------------------------------------------------ template references, checked at save
|
|
99
|
+
/** Where a zone's words take fill-ins, by the drawing's path: its instructions and message, a request's address,
|
|
100
|
+
* headers and body, an email's fields and a tool's arguments. */
|
|
101
|
+
export function templateFields(stage) {
|
|
102
|
+
const fields = [];
|
|
103
|
+
const add = (path, text) => { if (typeof text === "string")
|
|
104
|
+
fields.push({ path, text }); };
|
|
105
|
+
add("instructions", stage.instructions);
|
|
106
|
+
add("message", stage.message);
|
|
107
|
+
if (stage.request !== undefined) {
|
|
108
|
+
add("request.url", stage.request.url);
|
|
109
|
+
for (const [name, value] of Object.entries(stage.request.headers))
|
|
110
|
+
add(`request.headers.${name}`, value);
|
|
111
|
+
add("request.body", stage.request.body);
|
|
112
|
+
}
|
|
113
|
+
if (stage.email !== undefined) {
|
|
114
|
+
add("email.to", stage.email.to);
|
|
115
|
+
add("email.subject", stage.email.subject);
|
|
116
|
+
add("email.body", stage.email.body);
|
|
117
|
+
}
|
|
118
|
+
if (stage.tool !== undefined)
|
|
119
|
+
add("tool.args", stage.tool.args);
|
|
120
|
+
return fields;
|
|
121
|
+
}
|
|
122
|
+
/** Every `{{stage.…}}` in some words, as written between the braces (`stage.research.items`). */
|
|
123
|
+
const STAGE_REFERENCE = /\{\{\s*(stage\.[^{}]*?)\s*\}\}/g;
|
|
124
|
+
export function stageReferences(text) {
|
|
125
|
+
return [...text.matchAll(STAGE_REFERENCE)].map(match => match[1]);
|
|
126
|
+
}
|
|
127
|
+
/** What one `{{stage.…}}` reference can't be, against the drawing: null when its zone hands that field on. */
|
|
128
|
+
export function stageReferenceProblem(definition, reference) {
|
|
129
|
+
const [zone = "", ...rest] = reference.slice("stage.".length).split(".");
|
|
130
|
+
const field = rest.join(".");
|
|
131
|
+
const stage = definition.stages.find(one => one.id === zone);
|
|
132
|
+
if (stage === undefined)
|
|
133
|
+
return `${reference} is not available: there's no zone called ${zone === "" ? "(none)" : zone}`;
|
|
134
|
+
if (STAGE_HANDOFFS[stage.kind] === null)
|
|
135
|
+
return `${reference} is not available: ${stage.title} hands nothing on`;
|
|
136
|
+
if (field === "")
|
|
137
|
+
return null;
|
|
138
|
+
const fields = stageFieldsOf(stage.kind);
|
|
139
|
+
if (fields.includes(field))
|
|
140
|
+
return null;
|
|
141
|
+
const offered = [`{{stage.${zone}}}`, ...fields.map(one => `{{stage.${zone}.${one}}}`)];
|
|
142
|
+
return `${reference} is not available (${stage.title} hands on ${offered.length === 1 ? offered[0] : `${offered.slice(0, -1).join(", ")} and ${offered.at(-1)}`})`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Template references a save adds that its zones don't hand on, each named by its path
|
|
146
|
+
* (`stages[1].instructions: stage.research.unknown is not available ...`). A reference the flow already had before
|
|
147
|
+
* this save (`previous`, the saved drawing) is never refused, nor is any reference when a flow is only read or run:
|
|
148
|
+
* this checks new and changed words alone.
|
|
149
|
+
*/
|
|
150
|
+
export function stageReferenceProblems(definition, previous) {
|
|
151
|
+
const had = new Set((previous?.stages ?? []).flatMap(stage => templateFields(stage).flatMap(field => stageReferences(field.text))));
|
|
152
|
+
const problems = [];
|
|
153
|
+
definition.stages.forEach((stage, index) => {
|
|
154
|
+
for (const field of templateFields(stage)) {
|
|
155
|
+
for (const reference of new Set(stageReferences(field.text))) {
|
|
156
|
+
if (had.has(reference))
|
|
157
|
+
continue;
|
|
158
|
+
const problem = stageReferenceProblem(definition, reference);
|
|
159
|
+
const path = `stages[${index}].${field.path}`;
|
|
160
|
+
if (problem !== null)
|
|
161
|
+
problems.push({ path, kind: "bad-value", line: `${path}: ${problem}` });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return problems;
|
|
166
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task sizing classifier's answer (task-sizing.ts): one schema for the JSON Schema a Claude sizer is given
|
|
3
|
+
* (`--json-schema`) and the reader that takes an answer back. Jev answers through OpenRouter's Decisions API, which
|
|
4
|
+
* takes questions rather than a JSON Schema; its choice and yes/no are read into this same answer before they count.
|
|
5
|
+
*
|
|
6
|
+
* An answer is never kept as it is (it becomes the task's sizing, phase-routing.ts `TaskSizing`), so it carries no
|
|
7
|
+
* version.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { type ContractResult } from "./contract.js";
|
|
11
|
+
export declare const sizeAnswerSchema: z.ZodObject<{
|
|
12
|
+
size: z.ZodEnum<{
|
|
13
|
+
large: "large";
|
|
14
|
+
medium: "medium";
|
|
15
|
+
small: "small";
|
|
16
|
+
}>;
|
|
17
|
+
risky: z.ZodBoolean;
|
|
18
|
+
reason: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export type SizeAnswer = z.infer<typeof sizeAnswerSchema>;
|
|
21
|
+
/** What a Claude sizer's `--json-schema` is: the answer schema, exactly. */
|
|
22
|
+
export declare const SIZING_MODEL_SCHEMA: Readonly<Record<string, unknown>>;
|
|
23
|
+
/** Keep the old reason defaults and clipping before checking the normalized answer's contract. */
|
|
24
|
+
export declare function readSizeAnswer(value: unknown): ContractResult<SizeAnswer>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task sizing classifier's answer (task-sizing.ts): one schema for the JSON Schema a Claude sizer is given
|
|
3
|
+
* (`--json-schema`) and the reader that takes an answer back. Jev answers through OpenRouter's Decisions API, which
|
|
4
|
+
* takes questions rather than a JSON Schema; its choice and yes/no are read into this same answer before they count.
|
|
5
|
+
*
|
|
6
|
+
* An answer is never kept as it is (it becomes the task's sizing, phase-routing.ts `TaskSizing`), so it carries no
|
|
7
|
+
* version.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { TASK_SIZES } from "../phase-routing.js";
|
|
11
|
+
import { TEXT_LIMITS } from "../text-limits.js";
|
|
12
|
+
import { limited, parseContract, toModelSchema } from "./contract.js";
|
|
13
|
+
export const sizeAnswerSchema = z.object({
|
|
14
|
+
size: z.enum(TASK_SIZES),
|
|
15
|
+
risky: z.boolean(),
|
|
16
|
+
reason: limited("reason", "sizingReason").describe(`reason: at most ${TEXT_LIMITS.sizingReason} characters.`),
|
|
17
|
+
});
|
|
18
|
+
/** What a Claude sizer's `--json-schema` is: the answer schema, exactly. */
|
|
19
|
+
export const SIZING_MODEL_SCHEMA = toModelSchema(sizeAnswerSchema);
|
|
20
|
+
/** Keep the old reason defaults and clipping before checking the normalized answer's contract. */
|
|
21
|
+
export function readSizeAnswer(value) {
|
|
22
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
23
|
+
const body = value;
|
|
24
|
+
const reason = typeof body["reason"] === "string" ? body["reason"].replace(/\s+/g, " ").trim() : "";
|
|
25
|
+
value = { ...body, reason: reason.length <= TEXT_LIMITS.sizingReason ? reason : `${reason.slice(0, TEXT_LIMITS.sizingReason - 1)}…` };
|
|
26
|
+
}
|
|
27
|
+
return parseContract(sizeAnswerSchema, value);
|
|
28
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A teammate's turn (teammates.ts): one schema for the JSON Schema Claude answers in (`--json-schema`) and the reader
|
|
3
|
+
* that takes the answer back. One flat object with every field (a root union is refused by the API); fields a turn
|
|
4
|
+
* does not use are "" (or [] for options).
|
|
5
|
+
*
|
|
6
|
+
* The text limits (TURN_LIMITS, from TEXT_LIMITS) are deliberately not in the schema: the CLI refuses an answer a few
|
|
7
|
+
* characters over one whole, so they are checked after parsing and an answer over one is asked once to shorten
|
|
8
|
+
* (teammate-work.ts), then kept whole. What the zone allows — which actions, which answers, a note for a send-back, a
|
|
9
|
+
* question for an ask, a tool for a tool call — is checked after parsing too (`readTurn`), by path.
|
|
10
|
+
*
|
|
11
|
+
* A turn is kept as the step's decision (`flow_step_run.decision_json`) as it was read, so a saved one is this shape.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { type ContractResult } from "./contract.js";
|
|
15
|
+
/** What a teammate may do on one turn. A decision zone: approve, send back, or hand it to a person. A work zone: pick
|
|
16
|
+
* where it goes, ask its person, or say it can't. v94: "use_tool" asks for one tool call. */
|
|
17
|
+
export declare const TURN_ACTIONS: readonly ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"];
|
|
18
|
+
export type TurnAction = (typeof TURN_ACTIONS)[number];
|
|
19
|
+
export declare const teammateTurnSchema: z.ZodObject<{
|
|
20
|
+
action: z.ZodEnum<{
|
|
21
|
+
approve: "approve";
|
|
22
|
+
ask: "ask";
|
|
23
|
+
cant: "cant";
|
|
24
|
+
hand_off: "hand_off";
|
|
25
|
+
route: "route";
|
|
26
|
+
send_back: "send_back";
|
|
27
|
+
use_tool: "use_tool";
|
|
28
|
+
}>;
|
|
29
|
+
answer: z.ZodString;
|
|
30
|
+
text: z.ZodString;
|
|
31
|
+
note: z.ZodString;
|
|
32
|
+
question: z.ZodString;
|
|
33
|
+
options: z.ZodArray<z.ZodString>;
|
|
34
|
+
reason: z.ZodString;
|
|
35
|
+
tool: z.ZodString;
|
|
36
|
+
input: z.ZodString;
|
|
37
|
+
remember: z.ZodString;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type TurnAnswer = z.infer<typeof teammateTurnSchema>;
|
|
40
|
+
/** What Claude's `--json-schema` is for a turn: the turn schema, exactly. */
|
|
41
|
+
export declare const TURN_MODEL_SCHEMA: Readonly<Record<string, unknown>>;
|
|
42
|
+
/** Older and plain-text turns default absent or non-text fields to "", and keep only string options. */
|
|
43
|
+
export declare function readTurnAnswer(value: unknown): ContractResult<TurnAnswer>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A teammate's turn (teammates.ts): one schema for the JSON Schema Claude answers in (`--json-schema`) and the reader
|
|
3
|
+
* that takes the answer back. One flat object with every field (a root union is refused by the API); fields a turn
|
|
4
|
+
* does not use are "" (or [] for options).
|
|
5
|
+
*
|
|
6
|
+
* The text limits (TURN_LIMITS, from TEXT_LIMITS) are deliberately not in the schema: the CLI refuses an answer a few
|
|
7
|
+
* characters over one whole, so they are checked after parsing and an answer over one is asked once to shorten
|
|
8
|
+
* (teammate-work.ts), then kept whole. What the zone allows — which actions, which answers, a note for a send-back, a
|
|
9
|
+
* question for an ask, a tool for a tool call — is checked after parsing too (`readTurn`), by path.
|
|
10
|
+
*
|
|
11
|
+
* A turn is kept as the step's decision (`flow_step_run.decision_json`) as it was read, so a saved one is this shape.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { parseContract, toModelSchema } from "./contract.js";
|
|
15
|
+
/** What a teammate may do on one turn. A decision zone: approve, send back, or hand it to a person. A work zone: pick
|
|
16
|
+
* where it goes, ask its person, or say it can't. v94: "use_tool" asks for one tool call. */
|
|
17
|
+
export const TURN_ACTIONS = ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"];
|
|
18
|
+
export const teammateTurnSchema = z.object({
|
|
19
|
+
action: z.enum(TURN_ACTIONS),
|
|
20
|
+
/** route: one of the zone's answers, exactly as written. */
|
|
21
|
+
answer: z.string(),
|
|
22
|
+
/** What the next zones read, or the draft as rewritten. */
|
|
23
|
+
text: z.string(),
|
|
24
|
+
/** A decision or send-back note. */
|
|
25
|
+
note: z.string(),
|
|
26
|
+
/** ask: one short question, with up to 4 options to tap. */
|
|
27
|
+
question: z.string(),
|
|
28
|
+
options: z.array(z.string()),
|
|
29
|
+
/** One plain sentence saying why, for its manager's log. */
|
|
30
|
+
reason: z.string(),
|
|
31
|
+
/** use_tool: the tool's name, and its input as a JSON object written as text. */
|
|
32
|
+
tool: z.string(),
|
|
33
|
+
input: z.string(),
|
|
34
|
+
/** v95: one short fact worth keeping for later cards ("": none). */
|
|
35
|
+
remember: z.string(),
|
|
36
|
+
});
|
|
37
|
+
/** What Claude's `--json-schema` is for a turn: the turn schema, exactly. */
|
|
38
|
+
export const TURN_MODEL_SCHEMA = toModelSchema(teammateTurnSchema);
|
|
39
|
+
/** Older and plain-text turns default absent or non-text fields to "", and keep only string options. */
|
|
40
|
+
export function readTurnAnswer(value) {
|
|
41
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
42
|
+
const body = { ...value };
|
|
43
|
+
for (const key of Object.keys(teammateTurnSchema.shape)) {
|
|
44
|
+
if (key !== "action" && key !== "options" && typeof body[key] !== "string")
|
|
45
|
+
body[key] = "";
|
|
46
|
+
}
|
|
47
|
+
body["options"] = Array.isArray(body["options"]) ? body["options"].filter(one => typeof one === "string") : [];
|
|
48
|
+
value = body;
|
|
49
|
+
}
|
|
50
|
+
return parseContract(teammateTurnSchema, value);
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Teams button tap (teams-chat.ts `receiveTeams`): an Adaptive Card `Action.Submit` arrives as a message activity
|
|
3
|
+
* whose `value` is the data Toolroll put on the button, `{ so: <one-time token> }`. The activity is Microsoft's (read
|
|
4
|
+
* by `receiveTeams`); only `value.so` is read, as before, and extra submit keys are ignored. A tap from the paired person whose data can't be read
|
|
5
|
+
* is answered with why (`value.so: must be a Toolroll button token`), and nothing is done.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { type ContractResult } from "./contract.js";
|
|
9
|
+
export declare const teamsSubmitSchema: z.ZodObject<{
|
|
10
|
+
so: z.ZodString;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type TeamsSubmit = z.infer<typeof teamsSubmitSchema>;
|
|
13
|
+
/** A submit's one-time token. */
|
|
14
|
+
export declare function readTeamsSubmit(value: unknown): ContractResult<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Teams button tap (teams-chat.ts `receiveTeams`): an Adaptive Card `Action.Submit` arrives as a message activity
|
|
3
|
+
* whose `value` is the data Toolroll put on the button, `{ so: <one-time token> }`. The activity is Microsoft's (read
|
|
4
|
+
* by `receiveTeams`); only `value.so` is read, as before, and extra submit keys are ignored. A tap from the paired person whose data can't be read
|
|
5
|
+
* is answered with why (`value.so: must be a Toolroll button token`), and nothing is done.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { parseContract } from "./contract.js";
|
|
9
|
+
export const teamsSubmitSchema = z.object({ so: z.string().regex(/^[a-f0-9]{32}$/, { error: "must be a Toolroll button token" }) });
|
|
10
|
+
/** A submit's one-time token. */
|
|
11
|
+
export function readTeamsSubmit(value) {
|
|
12
|
+
const read = parseContract(z.object({ value: teamsSubmitSchema }), { value });
|
|
13
|
+
return read.ok ? { ok: true, value: read.value.value.so } : read;
|
|
14
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram (telegram.ts): one schema for an update as Telegram sends it — polled, or pushed to /hooks/telegram and kept
|
|
3
|
+
* in the inbox until applied — and one for the data a Toolroll button carries.
|
|
4
|
+
*
|
|
5
|
+
* The update is Telegram's: only the fields Toolroll reads are described, and any others are ignored. A button's data
|
|
6
|
+
* is Toolroll's: one of the forms it mints, within Telegram's 64 bytes. A tap whose data isn't one of them is
|
|
7
|
+
* answered, after the sender and chat are proved, with the line that says why, and nothing is done.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { type ContractResult } from "./contract.js";
|
|
11
|
+
export declare const telegramMessageSchema: z.ZodObject<{
|
|
12
|
+
message_id: z.ZodInt;
|
|
13
|
+
text: z.ZodOptional<z.ZodString>;
|
|
14
|
+
chat: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
id: z.ZodInt;
|
|
16
|
+
type: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>>;
|
|
18
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
id: z.ZodInt;
|
|
20
|
+
username: z.ZodOptional<z.ZodString>;
|
|
21
|
+
first_name: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
reply_to_message: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
message_id: z.ZodInt;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
forward_origin: z.ZodOptional<z.ZodUnknown>;
|
|
27
|
+
forward_date: z.ZodOptional<z.ZodUnknown>;
|
|
28
|
+
via_bot: z.ZodOptional<z.ZodUnknown>;
|
|
29
|
+
sender_chat: z.ZodOptional<z.ZodUnknown>;
|
|
30
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
export declare const telegramCallbackQuerySchema: z.ZodObject<{
|
|
33
|
+
id: z.ZodString;
|
|
34
|
+
data: z.ZodOptional<z.ZodString>;
|
|
35
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
36
|
+
id: z.ZodInt;
|
|
37
|
+
}, z.core.$strip>>;
|
|
38
|
+
message: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
message_id: z.ZodInt;
|
|
40
|
+
chat: z.ZodOptional<z.ZodObject<{
|
|
41
|
+
id: z.ZodInt;
|
|
42
|
+
}, z.core.$strip>>;
|
|
43
|
+
text: z.ZodOptional<z.ZodString>;
|
|
44
|
+
entities: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export declare const telegramUpdateSchema: z.ZodObject<{
|
|
48
|
+
update_id: z.ZodInt;
|
|
49
|
+
message: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
message_id: z.ZodInt;
|
|
51
|
+
text: z.ZodOptional<z.ZodString>;
|
|
52
|
+
chat: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
id: z.ZodInt;
|
|
54
|
+
type: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
57
|
+
id: z.ZodInt;
|
|
58
|
+
username: z.ZodOptional<z.ZodString>;
|
|
59
|
+
first_name: z.ZodOptional<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
reply_to_message: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
message_id: z.ZodInt;
|
|
63
|
+
}, z.core.$strip>>;
|
|
64
|
+
forward_origin: z.ZodOptional<z.ZodUnknown>;
|
|
65
|
+
forward_date: z.ZodOptional<z.ZodUnknown>;
|
|
66
|
+
via_bot: z.ZodOptional<z.ZodUnknown>;
|
|
67
|
+
sender_chat: z.ZodOptional<z.ZodUnknown>;
|
|
68
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>>;
|
|
70
|
+
callback_query: z.ZodOptional<z.ZodObject<{
|
|
71
|
+
id: z.ZodString;
|
|
72
|
+
data: z.ZodOptional<z.ZodString>;
|
|
73
|
+
from: z.ZodOptional<z.ZodObject<{
|
|
74
|
+
id: z.ZodInt;
|
|
75
|
+
}, z.core.$strip>>;
|
|
76
|
+
message: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
message_id: z.ZodInt;
|
|
78
|
+
chat: z.ZodOptional<z.ZodObject<{
|
|
79
|
+
id: z.ZodInt;
|
|
80
|
+
}, z.core.$strip>>;
|
|
81
|
+
text: z.ZodOptional<z.ZodString>;
|
|
82
|
+
entities: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
export type TelegramUpdate = z.infer<typeof telegramUpdateSchema>;
|
|
87
|
+
export type TelegramMessage = z.infer<typeof telegramMessageSchema>;
|
|
88
|
+
/** One update, as Telegram sent it (refused by path: `callback_query.message.chat.id: must be a number`). */
|
|
89
|
+
export declare function readTelegramUpdate(input: unknown): ContractResult<TelegramUpdate>;
|
|
90
|
+
/** An inbox row's saved update: JSON first, then the schema. */
|
|
91
|
+
export declare function parseTelegramUpdate(raw: string): ContractResult<TelegramUpdate>;
|
|
92
|
+
/**
|
|
93
|
+
* Every form of data a Toolroll button carries: a one-time token (16 random bytes as hex) for a proposal, decision,
|
|
94
|
+
* flow, choice or question button; a decide token (chat-decide.ts); a task from /tasks, or back to the lead; and an
|
|
95
|
+
* option of the lead's question to its owner (by index, or `x` for "Something else").
|
|
96
|
+
*/
|
|
97
|
+
export declare const TELEGRAM_BUTTON_DATA: RegExp;
|
|
98
|
+
export declare const telegramButtonDataSchema: z.ZodString;
|
|
99
|
+
/** A button that calls back (the other kind opens a link). */
|
|
100
|
+
export declare const telegramCallbackButtonSchema: z.ZodObject<{
|
|
101
|
+
text: z.ZodString;
|
|
102
|
+
callback_data: z.ZodString;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
export type TelegramCallbackButton = z.infer<typeof telegramCallbackButtonSchema>;
|
|
105
|
+
/** A tapped button's data: one Toolroll mints, or the path-named reason it isn't (`callback_data: required`). */
|
|
106
|
+
export declare function readTelegramButtonData(data: unknown): ContractResult<string>;
|
|
107
|
+
/**
|
|
108
|
+
* A button as Toolroll sends it. Data Telegram would refuse (over 64 bytes) or that a tap could not be read back is a
|
|
109
|
+
* fault in the code that made it, so it throws before anything is sent.
|
|
110
|
+
*/
|
|
111
|
+
export declare function telegramButton(text: string, data: string): TelegramCallbackButton;
|
|
112
|
+
/** A /tasks pick: the task's ref id, or back to the lead (null); null for anything else. */
|
|
113
|
+
export declare function pickOf(data: string): {
|
|
114
|
+
ref: number | null;
|
|
115
|
+
} | null;
|
|
116
|
+
/** An option of the lead's question to its owner: its turn and index, or `x` for "Something else"; null for anything else. */
|
|
117
|
+
export declare function askOf(data: string): {
|
|
118
|
+
turn: number;
|
|
119
|
+
option: number | "x";
|
|
120
|
+
} | null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram (telegram.ts): one schema for an update as Telegram sends it — polled, or pushed to /hooks/telegram and kept
|
|
3
|
+
* in the inbox until applied — and one for the data a Toolroll button carries.
|
|
4
|
+
*
|
|
5
|
+
* The update is Telegram's: only the fields Toolroll reads are described, and any others are ignored. A button's data
|
|
6
|
+
* is Toolroll's: one of the forms it mints, within Telegram's 64 bytes. A tap whose data isn't one of them is
|
|
7
|
+
* answered, after the sender and chat are proved, with the line that says why, and nothing is done.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { lengthIn, TEXT_LIMITS } from "../text-limits.js";
|
|
11
|
+
import { limited, parseContract } from "./contract.js";
|
|
12
|
+
const chat = z.object({ id: z.int(), type: z.string().optional() });
|
|
13
|
+
const sender = z.object({ id: z.int(), username: z.string().optional(), first_name: z.string().optional() });
|
|
14
|
+
export const telegramMessageSchema = z.object({
|
|
15
|
+
message_id: z.int(),
|
|
16
|
+
text: z.string().optional(),
|
|
17
|
+
chat: chat.optional(),
|
|
18
|
+
from: sender.optional(),
|
|
19
|
+
reply_to_message: z.object({ message_id: z.int() }).optional(),
|
|
20
|
+
/** Presence of any of these disqualifies a note: only direct, initial, plain text counts as authored-and-confirmed
|
|
21
|
+
* by the paired operator. */
|
|
22
|
+
forward_origin: z.unknown().optional(),
|
|
23
|
+
forward_date: z.unknown().optional(),
|
|
24
|
+
via_bot: z.unknown().optional(),
|
|
25
|
+
sender_chat: z.unknown().optional(),
|
|
26
|
+
caption: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
export const telegramCallbackQuerySchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
/** Read by `readTelegramButtonData`, after the sender and chat are proved. */
|
|
31
|
+
data: z.string().optional(),
|
|
32
|
+
from: z.object({ id: z.int() }).optional(),
|
|
33
|
+
message: z.object({ message_id: z.int(), chat: z.object({ id: z.int() }).optional(), text: z.string().optional(), entities: z.array(z.unknown()).optional() }).optional(),
|
|
34
|
+
});
|
|
35
|
+
export const telegramUpdateSchema = z.object({
|
|
36
|
+
update_id: z.int().min(1),
|
|
37
|
+
message: telegramMessageSchema.optional(),
|
|
38
|
+
callback_query: telegramCallbackQuerySchema.optional(),
|
|
39
|
+
});
|
|
40
|
+
/** One update, as Telegram sent it (refused by path: `callback_query.message.chat.id: must be a number`). */
|
|
41
|
+
export function readTelegramUpdate(input) {
|
|
42
|
+
return parseContract(telegramUpdateSchema, input);
|
|
43
|
+
}
|
|
44
|
+
/** An inbox row's saved update: JSON first, then the schema. */
|
|
45
|
+
export function parseTelegramUpdate(raw) {
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { ok: false, issues: [{ path: "payload", kind: "invalid", line: "payload: not JSON" }] };
|
|
52
|
+
}
|
|
53
|
+
return readTelegramUpdate(body);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Every form of data a Toolroll button carries: a one-time token (16 random bytes as hex) for a proposal, decision,
|
|
57
|
+
* flow, choice or question button; a decide token (chat-decide.ts); a task from /tasks, or back to the lead; and an
|
|
58
|
+
* option of the lead's question to its owner (by index, or `x` for "Something else").
|
|
59
|
+
*/
|
|
60
|
+
export const TELEGRAM_BUTTON_DATA = /^(?:[a-f0-9]{32}|d:[a-f0-9]{24}|pick:(?:lead|[1-9][0-9]{0,14})|ask:[1-9][0-9]{0,14}:(?:[0-3]|x))$/;
|
|
61
|
+
export const telegramButtonDataSchema = limited("callback_data", "telegramCallbackDataBytes")
|
|
62
|
+
.regex(TELEGRAM_BUTTON_DATA, { error: "not a Toolroll button" });
|
|
63
|
+
/** A button that calls back (the other kind opens a link). */
|
|
64
|
+
export const telegramCallbackButtonSchema = z.strictObject({ text: z.string(), callback_data: telegramButtonDataSchema });
|
|
65
|
+
/** The byte bound, which counts UTF-8 bytes where the schema counts characters. */
|
|
66
|
+
function withinBytes(data) {
|
|
67
|
+
const limit = TEXT_LIMITS.telegramCallbackDataBytes, bytes = lengthIn(data, "bytes");
|
|
68
|
+
return bytes <= limit ? { ok: true, value: data } : { ok: false, issues: [{ path: "callback_data", kind: "too-long", line: `callback_data: over ${limit} bytes (${bytes})` }] };
|
|
69
|
+
}
|
|
70
|
+
/** A tapped button's data: one Toolroll mints, or the path-named reason it isn't (`callback_data: required`). */
|
|
71
|
+
export function readTelegramButtonData(data) {
|
|
72
|
+
const read = parseContract(z.strictObject({ callback_data: telegramButtonDataSchema }), { callback_data: data });
|
|
73
|
+
return read.ok ? withinBytes(read.value.callback_data) : read;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A button as Toolroll sends it. Data Telegram would refuse (over 64 bytes) or that a tap could not be read back is a
|
|
77
|
+
* fault in the code that made it, so it throws before anything is sent.
|
|
78
|
+
*/
|
|
79
|
+
export function telegramButton(text, data) {
|
|
80
|
+
const read = parseContract(telegramCallbackButtonSchema, { text, callback_data: data });
|
|
81
|
+
const bytes = read.ok ? withinBytes(read.value.callback_data) : read;
|
|
82
|
+
if (!read.ok || !bytes.ok)
|
|
83
|
+
throw new Error(`a Telegram button: ${(read.ok ? bytes.ok ? [] : bytes.issues : read.issues).map(issue => issue.line).join("; ")}`);
|
|
84
|
+
return read.value;
|
|
85
|
+
}
|
|
86
|
+
/** A /tasks pick: the task's ref id, or back to the lead (null); null for anything else. */
|
|
87
|
+
export function pickOf(data) {
|
|
88
|
+
if (data === "pick:lead")
|
|
89
|
+
return { ref: null };
|
|
90
|
+
const ref = /^pick:([1-9][0-9]{0,14})$/.exec(data)?.[1];
|
|
91
|
+
return ref === undefined ? null : { ref: Number(ref) };
|
|
92
|
+
}
|
|
93
|
+
/** An option of the lead's question to its owner: its turn and index, or `x` for "Something else"; null for anything else. */
|
|
94
|
+
export function askOf(data) {
|
|
95
|
+
const tap = /^ask:([1-9][0-9]{0,14}):([0-3]|x)$/.exec(data);
|
|
96
|
+
return tap === null ? null : { turn: Number(tap[1]), option: tap[2] === "x" ? "x" : Number(tap[2]) };
|
|
97
|
+
}
|