@jsm-mit/sultana-agent-tools-package 0.2.0 → 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/README.md +63 -6
- package/dist/confirmations.d.ts +61 -0
- package/dist/confirmations.d.ts.map +1 -0
- package/dist/confirmations.js +100 -0
- package/dist/create-salon-tools.d.ts +13 -2
- package/dist/create-salon-tools.d.ts.map +1 -1
- package/dist/create-salon-tools.js +10 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/persona.d.ts +4 -1
- package/dist/persona.d.ts.map +1 -1
- package/dist/persona.js +18 -7
- package/dist/tools/promos.d.ts +2 -1
- package/dist/tools/promos.d.ts.map +1 -1
- package/dist/tools/promos.js +79 -39
- package/dist/tools/schedule.d.ts +2 -1
- package/dist/tools/schedule.d.ts.map +1 -1
- package/dist/tools/schedule.js +51 -31
- package/dist/tools/services.d.ts +2 -1
- package/dist/tools/services.d.ts.map +1 -1
- package/dist/tools/services.js +74 -30
- package/dist/tools/write-gate.d.ts +29 -0
- package/dist/tools/write-gate.d.ts.map +1 -0
- package/dist/tools/write-gate.js +37 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,55 @@ const tools = createSalonTools({
|
|
|
23
23
|
`salonId` is captured at construction and is **never a tool argument** — a model holding these
|
|
24
24
|
tools cannot reach another salon's data whatever it sends.
|
|
25
25
|
|
|
26
|
+
### Confirmations across turns
|
|
27
|
+
|
|
28
|
+
A host that runs the owner set in a chat keeps one `ConfirmationLedger` per conversation and builds
|
|
29
|
+
the tools again for every incoming owner message, with that message's turn:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { ConfirmationLedger, createSalonTools, SALON_AGENT_PERSONA_PL } from "@jsm-mit/sultana-agent-tools-package";
|
|
33
|
+
|
|
34
|
+
const ledger = new ConfirmationLedger(); // one per conversation, kept in memory
|
|
35
|
+
|
|
36
|
+
// on every owner message — one agent turn:
|
|
37
|
+
const tools = createSalonTools({ canisterId, identity, salonId, confirmations: ledger.forTurn() });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
With `confirmations`, the two-step protocol is enforced by the tools, not only asked for by the
|
|
41
|
+
persona:
|
|
42
|
+
|
|
43
|
+
1. `confirmed: false` makes no canister write and returns `confirmation_required` with a
|
|
44
|
+
`summary` and an `echo` that now carries a `confirmationId` (`c-` and 8 hex digits).
|
|
45
|
+
2. `confirmed: true` goes through only with that `confirmationId`, the **same arguments** (key
|
|
46
|
+
order does not matter, array order does; `confirmed`, `confirmationId` and
|
|
47
|
+
`confirmationPhrase` are not compared), from a **later turn**, within 15 minutes, and once.
|
|
48
|
+
Anything else is `status: "error"`, `code: "invalid_arguments"`, returned before the tool calls
|
|
49
|
+
the canister at all:
|
|
50
|
+
|
|
51
|
+
| why | summary |
|
|
52
|
+
|---|---|
|
|
53
|
+
| same turn as the preview | Zgoda musi przyjść w osobnej wiadomości właścicielki — najpierw pokaż podsumowanie i poczekaj na odpowiedź. |
|
|
54
|
+
| no id, unknown, used or expired | Brak ważnego podglądu tej zmiany — wywołaj narzędzie z confirmed=false i pokaż właścicielce podsumowanie. |
|
|
55
|
+
| other tool or other arguments | Argumenty różnią się od pokazanego podglądu — pokaż nowy podgląd (confirmed=false). |
|
|
56
|
+
|
|
57
|
+
Every write tool's schema gets a `confirmationId` parameter when a ledger is passed; without one,
|
|
58
|
+
the schemas and the protocol are what they were in 0.2.0 (a model can still confirm in the same
|
|
59
|
+
breath — only the persona stops it). The id is used up right before the write, not at the first
|
|
60
|
+
check: a confirmation refused for a mistyped name can be sent again with the same id once the owner
|
|
61
|
+
has typed it right. Two parallel calls with one id cannot both write: the second is refused right before its write.
|
|
62
|
+
|
|
63
|
+
`ConfirmationLedger` takes `{ ttlMs, now, newId }` for tests; `createSalonToolsFromPort(port,
|
|
64
|
+
{ confirmations })` is the same over any port. `forTurn()` returns `TurnConfirmations` —
|
|
65
|
+
`issue(toolName, args)`, `check(toolName, args, id)` (the verdict, the id kept) and
|
|
66
|
+
`redeem(toolName, args, id)` (the verdict, the id used up) — for a host that wants the protocol
|
|
67
|
+
around tools of its own.
|
|
68
|
+
|
|
69
|
+
`SALON_AGENT_PERSONA_PL` is written for this: every write first with `confirmed=false`, the
|
|
70
|
+
summary shown, the owner's answer awaited in her **next** message, then `confirmed=true` with the
|
|
71
|
+
`confirmationId` and the same arguments; a removal only with the name the owner typed herself; and
|
|
72
|
+
with read tools only, the agent says that changes need the "Salon Assistant" page open in the
|
|
73
|
+
panel.
|
|
74
|
+
|
|
26
75
|
### Read-only set
|
|
27
76
|
|
|
28
77
|
An assistant that runs without the owner's identity takes the read-only set and its own persona:
|
|
@@ -84,12 +133,18 @@ and `find_service_type`.
|
|
|
84
133
|
- **`execute` never throws.** Every call returns `{status: "ok" | "confirmation_required" | "error"}`
|
|
85
134
|
so an agent can read a failure and talk about it.
|
|
86
135
|
- **Confirmation is a schema field, not a prompt rule.** Called with `confirmed: false`, a write
|
|
87
|
-
tool makes no canister
|
|
88
|
-
and service-type labels already looked up — plus the
|
|
89
|
-
be talked out of a rule; a required field cannot.
|
|
136
|
+
tool makes no canister write and returns a sentence built from *resolved* arguments — worker names
|
|
137
|
+
and service-type labels already looked up — plus the arguments to send back in `echo`. A persona
|
|
138
|
+
can be talked out of a rule; a required field cannot. With a `ConfirmationLedger` (above) the
|
|
139
|
+
confirmation must also come from a later turn, with the preview's id.
|
|
90
140
|
- **Irreversible operations need the entity's own name** typed back in `confirmationPhrase`.
|
|
91
|
-
|
|
92
|
-
|
|
141
|
+
`remove_service` and `remove_promo` leave the name OUT of `echo`, and their summary asks the
|
|
142
|
+
model to have the owner write it: a model that just sends the echo back deletes nothing.
|
|
143
|
+
- **A partial edit is merged — at write time.** `updateSalonService` replaces the whole record on
|
|
144
|
+
the canister, so "just change the price" would otherwise unassign the team. The preview summary
|
|
145
|
+
shows the merged record, but `update_service`'s `echo` carries only `salonServiceId` and the
|
|
146
|
+
fields the model sent; the confirmed call reads the record again and merges then, so a change
|
|
147
|
+
made in the panel between preview and confirmation is kept.
|
|
93
148
|
- **Units are fixed at the boundary**: price is whole zloty, duration is minutes rounded up to a
|
|
94
149
|
multiple of 5 (the availability grid), and the tool says when it rounded.
|
|
95
150
|
- **Service-type ids come from the catalogue.** `find_service_type` uses the same matcher as both
|
|
@@ -111,7 +166,9 @@ and `find_service_type`.
|
|
|
111
166
|
|
|
112
167
|
`npm run sandbox -- --fake` is the loop to iterate in: a real model, real tool calls, an in-memory
|
|
113
168
|
salon. Every call is printed with its arguments and its result, so a tool description that misleads
|
|
114
|
-
the model shows up immediately — and a wrong answer costs no cycles and no daily action quota.
|
|
169
|
+
the model shows up immediately — and a wrong answer costs no cycles and no daily action quota. The
|
|
170
|
+
sandbox builds the tools again for every line you type, with a turn of one `ConfirmationLedger`,
|
|
171
|
+
as a host does — so the confirmation id and the next-message rule are in play there too.
|
|
115
172
|
|
|
116
173
|
Drop `--fake` to run the same conversation against a canister with the owner's identity
|
|
117
174
|
(`OWNER1_PEM`). Start with `npm run whoami`: it prints the principal the PEM makes you, the salons
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-step write protocol, enforced in code rather than by the persona.
|
|
3
|
+
*
|
|
4
|
+
* A write tool called with `confirmed: false` issues a confirmation id for exactly those arguments.
|
|
5
|
+
* The write itself goes through only when a LATER turn — the owner's next message — sends the same
|
|
6
|
+
* arguments back with that id. A model that previews and confirms in one breath, confirms
|
|
7
|
+
* something it never showed, or reuses an id, is refused before any canister call.
|
|
8
|
+
*
|
|
9
|
+
* The ledger lives in memory next to the conversation. A host keeps one ledger per conversation and
|
|
10
|
+
* calls `forTurn()` once per incoming owner message, when it builds that turn's tools.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_CONFIRMATION_TTL_MS: number;
|
|
13
|
+
export interface ConfirmationLedgerOptions {
|
|
14
|
+
/** How long a preview can still be confirmed. Defaults to 15 minutes. */
|
|
15
|
+
ttlMs?: number;
|
|
16
|
+
/** Milliseconds since the epoch. For tests. */
|
|
17
|
+
now?: () => number;
|
|
18
|
+
/** Confirmation id generator. For tests. */
|
|
19
|
+
newId?: () => string;
|
|
20
|
+
}
|
|
21
|
+
export type ConfirmationRefusal =
|
|
22
|
+
/** No `confirmationId` was sent. */
|
|
23
|
+
"missing"
|
|
24
|
+
/** The id was never issued, was already used, or was cleared out after expiring. */
|
|
25
|
+
| "unknown"
|
|
26
|
+
/** The id comes from this very turn — the owner has not answered the preview yet. */
|
|
27
|
+
| "same_turn"
|
|
28
|
+
/** The preview is older than the ledger's TTL. */
|
|
29
|
+
| "expired"
|
|
30
|
+
/** The id was issued for another tool or other arguments. */
|
|
31
|
+
| "mismatch";
|
|
32
|
+
export type RedeemResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
reason: ConfirmationRefusal;
|
|
37
|
+
};
|
|
38
|
+
/** One turn's view of the ledger. Ids issued here can only be redeemed from a later turn. */
|
|
39
|
+
export interface TurnConfirmations {
|
|
40
|
+
/** Records a preview of `toolName` with `args` and returns its id (`c-` and 8 hex digits). */
|
|
41
|
+
issue(toolName: string, args: Record<string, unknown>): string;
|
|
42
|
+
/** Whether `redeem` would succeed now — without using the id up. An expired entry is cleared. */
|
|
43
|
+
check(toolName: string, args: Record<string, unknown>, confirmationId: string | undefined): RedeemResult;
|
|
44
|
+
/** The same verdict as `check`; on success the id is used up and cannot confirm anything again. */
|
|
45
|
+
redeem(toolName: string, args: Record<string, unknown>, confirmationId: string | undefined): RedeemResult;
|
|
46
|
+
}
|
|
47
|
+
export declare class ConfirmationLedger {
|
|
48
|
+
private readonly ttlMs;
|
|
49
|
+
private readonly now;
|
|
50
|
+
private readonly newId;
|
|
51
|
+
private readonly pending;
|
|
52
|
+
private turn;
|
|
53
|
+
constructor(options?: ConfirmationLedgerOptions);
|
|
54
|
+
/** Starts a new turn. Call it once per incoming owner message and hand the result to that
|
|
55
|
+
* turn's tools; every call is a later turn than the one before. */
|
|
56
|
+
forTurn(): TurnConfirmations;
|
|
57
|
+
private issue;
|
|
58
|
+
private verdict;
|
|
59
|
+
private isExpired;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=confirmations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirmations.d.ts","sourceRoot":"","sources":["../src/confirmations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,2BAA2B,QAAiB,CAAC;AAK1D,MAAM,WAAW,yBAAyB;IACtC,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,mBAAmB;AAC3B,oCAAoC;AAClC,SAAS;AACX,oFAAoF;GAClF,SAAS;AACX,qFAAqF;GACnF,WAAW;AACb,kDAAkD;GAChD,SAAS;AACX,6DAA6D;GAC3D,UAAU,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAErF,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAC9B,8FAA8F;IAC9F,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAC/D,iGAAiG;IACjG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;IACzG,mGAAmG;IACnG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;CAC7G;AASD,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAClE,OAAO,CAAC,IAAI,CAAK;gBAEL,OAAO,GAAE,yBAA8B;IAMnD;uEACmE;IAC5D,OAAO,IAAI,iBAAiB;IAWnC,OAAO,CAAC,KAAK;IAeb,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,SAAS;CAGpB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-step write protocol, enforced in code rather than by the persona.
|
|
3
|
+
*
|
|
4
|
+
* A write tool called with `confirmed: false` issues a confirmation id for exactly those arguments.
|
|
5
|
+
* The write itself goes through only when a LATER turn — the owner's next message — sends the same
|
|
6
|
+
* arguments back with that id. A model that previews and confirms in one breath, confirms
|
|
7
|
+
* something it never showed, or reuses an id, is refused before any canister call.
|
|
8
|
+
*
|
|
9
|
+
* The ledger lives in memory next to the conversation. A host keeps one ledger per conversation and
|
|
10
|
+
* calls `forTurn()` once per incoming owner message, when it builds that turn's tools.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_CONFIRMATION_TTL_MS = 15 * 60 * 1000;
|
|
13
|
+
/** Argument keys that carry the protocol, not the change — never part of what is compared. */
|
|
14
|
+
const PROTOCOL_KEYS = new Set(["confirmed", "confirmationId", "confirmationPhrase"]);
|
|
15
|
+
export class ConfirmationLedger {
|
|
16
|
+
ttlMs;
|
|
17
|
+
now;
|
|
18
|
+
newId;
|
|
19
|
+
pending = new Map();
|
|
20
|
+
turn = 0;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_CONFIRMATION_TTL_MS;
|
|
23
|
+
this.now = options.now ?? Date.now;
|
|
24
|
+
this.newId = options.newId ?? randomConfirmationId;
|
|
25
|
+
}
|
|
26
|
+
/** Starts a new turn. Call it once per incoming owner message and hand the result to that
|
|
27
|
+
* turn's tools; every call is a later turn than the one before. */
|
|
28
|
+
forTurn() {
|
|
29
|
+
this.turn += 1;
|
|
30
|
+
const turn = this.turn;
|
|
31
|
+
return {
|
|
32
|
+
issue: (toolName, args) => this.issue(turn, toolName, args),
|
|
33
|
+
check: (toolName, args, confirmationId) => this.verdict(turn, toolName, args, confirmationId, false),
|
|
34
|
+
redeem: (toolName, args, confirmationId) => this.verdict(turn, toolName, args, confirmationId, true),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
issue(turn, toolName, args) {
|
|
38
|
+
// Previews nobody confirmed would otherwise pile up for the life of the conversation.
|
|
39
|
+
const now = this.now();
|
|
40
|
+
for (const [id, entry] of this.pending) {
|
|
41
|
+
if (this.isExpired(entry, now))
|
|
42
|
+
this.pending.delete(id);
|
|
43
|
+
}
|
|
44
|
+
// A 32-bit collision only replaces an older preview, whose id then fails as a mismatch —
|
|
45
|
+
// never lets a change through.
|
|
46
|
+
const id = this.newId();
|
|
47
|
+
this.pending.set(id, { toolName, argsKey: confirmationArgsKey(args), turn, issuedAt: now });
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
verdict(turn, toolName, args, confirmationId, redeem) {
|
|
51
|
+
const id = confirmationId?.trim();
|
|
52
|
+
if (!id)
|
|
53
|
+
return { ok: false, reason: "missing" };
|
|
54
|
+
const entry = this.pending.get(id);
|
|
55
|
+
if (!entry)
|
|
56
|
+
return { ok: false, reason: "unknown" };
|
|
57
|
+
if (this.isExpired(entry, this.now())) {
|
|
58
|
+
this.pending.delete(id);
|
|
59
|
+
return { ok: false, reason: "expired" };
|
|
60
|
+
}
|
|
61
|
+
if (entry.turn >= turn)
|
|
62
|
+
return { ok: false, reason: "same_turn" };
|
|
63
|
+
if (entry.toolName !== toolName || entry.argsKey !== confirmationArgsKey(args)) {
|
|
64
|
+
return { ok: false, reason: "mismatch" };
|
|
65
|
+
}
|
|
66
|
+
if (redeem)
|
|
67
|
+
this.pending.delete(id);
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
isExpired(entry, now) {
|
|
71
|
+
return now - entry.issuedAt > this.ttlMs;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Canonical JSON of the arguments without the protocol keys: object keys sorted at every depth,
|
|
75
|
+
* array order kept — `{b, a}` and `{a, b}` are the same change, `[1, 2]` and `[2, 1]` are not. */
|
|
76
|
+
function confirmationArgsKey(args) {
|
|
77
|
+
const change = {};
|
|
78
|
+
for (const [key, value] of Object.entries(args)) {
|
|
79
|
+
if (!PROTOCOL_KEYS.has(key))
|
|
80
|
+
change[key] = value;
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify(sortKeys(change));
|
|
83
|
+
}
|
|
84
|
+
function sortKeys(value) {
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return value.map(sortKeys);
|
|
87
|
+
if (value === null || typeof value !== "object")
|
|
88
|
+
return value;
|
|
89
|
+
const sorted = {};
|
|
90
|
+
for (const key of Object.keys(value).sort()) {
|
|
91
|
+
sorted[key] = sortKeys(value[key]);
|
|
92
|
+
}
|
|
93
|
+
return sorted;
|
|
94
|
+
}
|
|
95
|
+
/** `crypto.getRandomValues` exists in Node and in every browser, secure context or not. */
|
|
96
|
+
function randomConfirmationId() {
|
|
97
|
+
const bytes = new Uint8Array(4);
|
|
98
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
99
|
+
return `c-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
100
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { Identity } from "@icp-sdk/core/agent";
|
|
2
|
+
import type { TurnConfirmations } from "./confirmations.js";
|
|
2
3
|
import { type IcSalonCorePortOptions } from "./ic-salon-core-port.js";
|
|
3
4
|
import type { SalonCorePort } from "./salon-core-port.js";
|
|
4
5
|
import type { AgentTool } from "./types.js";
|
|
5
|
-
export interface
|
|
6
|
+
export interface CreateSalonToolsFromPortOptions {
|
|
7
|
+
/**
|
|
8
|
+
* This turn's confirmations — `ledger.forTurn()` from the conversation's `ConfirmationLedger`,
|
|
9
|
+
* taken once per incoming owner message. With it, every write tool issues a `confirmationId`
|
|
10
|
+
* on `confirmed: false` and refuses `confirmed: true` unless that id comes back, with the same
|
|
11
|
+
* arguments, from a later turn. Omit it and the tools keep the old protocol, where only the
|
|
12
|
+
* persona stops a model from confirming on its own.
|
|
13
|
+
*/
|
|
14
|
+
confirmations?: TurnConfirmations;
|
|
15
|
+
}
|
|
16
|
+
export interface CreateSalonToolsOptions extends Omit<IcSalonCorePortOptions, "language" | "access">, CreateSalonToolsFromPortOptions {
|
|
6
17
|
/** Language of service-type labels. Defaults to Polish. */
|
|
7
18
|
language?: string;
|
|
8
19
|
}
|
|
@@ -12,7 +23,7 @@ export interface CreateSalonToolsOptions extends Omit<IcSalonCorePortOptions, "l
|
|
|
12
23
|
*/
|
|
13
24
|
export declare function createSalonTools(options: CreateSalonToolsOptions): AgentTool[];
|
|
14
25
|
/** The same tool set over any port — the seam tests and the sandbox use. */
|
|
15
|
-
export declare function createSalonToolsFromPort(port: SalonCorePort): AgentTool[];
|
|
26
|
+
export declare function createSalonToolsFromPort(port: SalonCorePort, options?: CreateSalonToolsFromPortOptions): AgentTool[];
|
|
16
27
|
export interface CreateSalonReadToolsOptions {
|
|
17
28
|
canisterId: string;
|
|
18
29
|
salonId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-salon-tools.d.ts","sourceRoot":"","sources":["../src/create-salon-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"create-salon-tools.d.ts","sourceRoot":"","sources":["../src/create-salon-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,+BAA+B;IAC5C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,WAAW,uBACb,SAAQ,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,QAAQ,CAAC,EACvD,+BAA+B;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAK9E;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,+BAAoC,GAAG,SAAS,EAAE,CAQxH;AAED,MAAM,WAAW,2BAA2B;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,SAAS,EAAE,CAGtF;AAED;mEACmE;AACnE,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,EAAE,CAE7E"}
|
|
@@ -7,12 +7,18 @@ import { createServiceTools, findServiceTypeTool, listServicesTool } from "./too
|
|
|
7
7
|
* argument, so a model holding these tools cannot reach another salon's data whatever it sends.
|
|
8
8
|
*/
|
|
9
9
|
export function createSalonTools(options) {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const { confirmations, language, ...portOptions } = options;
|
|
11
|
+
const port = new IcSalonCorePort({ ...portOptions, language: language ?? "pl", access: "owner" });
|
|
12
|
+
return createSalonToolsFromPort(port, { confirmations });
|
|
12
13
|
}
|
|
13
14
|
/** The same tool set over any port — the seam tests and the sandbox use. */
|
|
14
|
-
export function createSalonToolsFromPort(port) {
|
|
15
|
-
|
|
15
|
+
export function createSalonToolsFromPort(port, options = {}) {
|
|
16
|
+
const { confirmations } = options;
|
|
17
|
+
return [
|
|
18
|
+
...createServiceTools(port, confirmations),
|
|
19
|
+
...createScheduleTools(port, confirmations),
|
|
20
|
+
...createPromoTools(port, confirmations),
|
|
21
|
+
];
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* The read-only tool set for ONE salon: `list_services`, `list_promos` and `find_service_type`,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { ConfirmationLedger, DEFAULT_CONFIRMATION_TTL_MS } from "./confirmations.js";
|
|
2
|
+
export type { ConfirmationLedgerOptions, ConfirmationRefusal, RedeemResult, TurnConfirmations, } from "./confirmations.js";
|
|
1
3
|
export { createSalonReadTools, createSalonReadToolsFromPort, createSalonTools, createSalonToolsFromPort, } from "./create-salon-tools.js";
|
|
2
|
-
export type { CreateSalonReadToolsOptions, CreateSalonToolsOptions } from "./create-salon-tools.js";
|
|
4
|
+
export type { CreateSalonReadToolsOptions, CreateSalonToolsFromPortOptions, CreateSalonToolsOptions, } from "./create-salon-tools.js";
|
|
3
5
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
|
4
6
|
export type { IcSalonCorePortOptions } from "./ic-salon-core-port.js";
|
|
5
7
|
export { SALON_AGENT_PERSONA_PL, SALON_READ_PERSONA_PL } from "./persona.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACrF,YAAY,EACR,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACH,oBAAoB,EACpB,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACR,2BAA2B,EAC3B,+BAA+B,EAC/B,uBAAuB,GAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EACR,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EACT,UAAU,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ConfirmationLedger, DEFAULT_CONFIRMATION_TTL_MS } from "./confirmations.js";
|
|
1
2
|
export { createSalonReadTools, createSalonReadToolsFromPort, createSalonTools, createSalonToolsFromPort, } from "./create-salon-tools.js";
|
|
2
3
|
export { IcSalonCorePort } from "./ic-salon-core-port.js";
|
|
3
4
|
export { SALON_AGENT_PERSONA_PL, SALON_READ_PERSONA_PL } from "./persona.js";
|
package/dist/persona.d.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* confirmation protocol, which reads come before which writes), so keeping them next to the tools
|
|
6
6
|
* is the only way they cannot drift apart. An agent host may append its own persona — who the
|
|
7
7
|
* agent is, how it speaks — on top of this.
|
|
8
|
+
*
|
|
9
|
+
* The two-step rule is written for a host that passes a `ConfirmationLedger` turn to the tools:
|
|
10
|
+
* the tools then refuse a confirmation from the same turn, so the persona and the code agree.
|
|
8
11
|
*/
|
|
9
|
-
export declare const SALON_AGENT_PERSONA_PL = "Jeste\u015B asystentem w\u0142a\
|
|
12
|
+
export declare const SALON_AGENT_PERSONA_PL = "Jeste\u015B asystentem w\u0142a\u015Bcicielki salonu w aplikacji Sultana.\nRozmawiasz po polsku, kr\u00F3tko i konkretnie. Wykonujesz zmiany w salonie za pomoc\u0105 narz\u0119dzi.\n\nZasady pracy:\n- Zanim zmienisz albo usuniesz us\u0142ug\u0119, wywo\u0142aj list_services i ustal jej id. Nie zgaduj id.\n- Zanim podasz serviceTypeIds, wywo\u0142aj find_service_type. Nigdy nie wymy\u015Blaj id typu us\u0142ugi.\n- Zanim przypiszesz pracownik\u00F3w, wywo\u0142aj list_workers.\n- Cena to pe\u0142ne z\u0142ote (liczba ca\u0142kowita). Czas trwania to minuty, wielokrotno\u015B\u0107 5.\n \"p\u00F3\u0142torej godziny\" to 90 minut, \"kwadrans\" to 15 minut.\n- Gdy masz narz\u0119dzia zapisuj\u0105ce, list_services i list_promos pokazuj\u0105 te\u017C us\u0142ugi\n i promocje wy\u0142\u0105czone \u2014 nie tylko to, co widz\u0105 klientki.\n- Ka\u017Cda zmiana to dwa kroki w dw\u00F3ch r\u00F3\u017Cnych wiadomo\u015Bciach w\u0142a\u015Bcicielki:\n 1. Wywo\u0142aj narz\u0119dzie zapisuj\u0105ce z confirmed=false. Poka\u017C w\u0142a\u015Bcicielce zdanie z pola summary,\n zapytaj o zgod\u0119 i na tym zako\u0144cz odpowied\u017A.\n 2. Dopiero gdy w swojej NAST\u0118PNEJ wiadomo\u015Bci wyra\u017Anie si\u0119 zgodzi, wywo\u0142aj to samo narz\u0119dzie\n z confirmed=true, z confirmationId z tego podgl\u0105du i z tymi samymi argumentami (pole echo).\n Nigdy nie potwierdzaj w tej samej odpowiedzi, w kt\u00F3rej pokaza\u0142e\u015B podgl\u0105d.\n Gdy w\u0142a\u015Bcicielka chce co\u015B inaczej, zr\u00F3b nowy podgl\u0105d i zn\u00F3w poczekaj na zgod\u0119.\n- Przy usuwaniu popro\u015B w\u0142a\u015Bcicielk\u0119, \u017Ceby sama napisa\u0142a dok\u0142adn\u0105 nazw\u0119 usuwanej rzeczy,\n i wstaw w confirmationPhrase to, co napisa\u0142a. Nigdy nie wpisuj nazwy za ni\u0105.\n- Je\u015Bli masz tylko narz\u0119dzia do odczytu (list_\u2026, find_\u2026), nie mo\u017Cesz niczego zmieni\u0107 \u2014\n powiedz, \u017Ce zmiany wymagaj\u0105 otwartej strony \u201ESalon Assistant\u201D w panelu.\n- Je\u015Bli narz\u0119dzie zwr\u00F3ci status \"error\", przeczytaj summary i powiedz w\u0142a\u015Bcicielce,\n co posz\u0142o nie tak. Nie powtarzaj tego samego wywo\u0142ania w k\u00F3\u0142ko.\n- Je\u015Bli brakuje Ci danych do wykonania polecenia, dopytaj o jedn\u0105 rzecz naraz.\n- Nie obiecuj zmian, kt\u00F3rych nie wykona\u0142e\u015B narz\u0119dziem.";
|
|
10
13
|
/**
|
|
11
14
|
* The operating rules for the read-only set (`createSalonReadTools`), as a system prompt.
|
|
12
15
|
*
|
package/dist/persona.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../src/persona.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"persona.d.ts","sourceRoot":"","sources":["../src/persona.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,i2EAyBoB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,6wCAee,CAAC"}
|
package/dist/persona.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* confirmation protocol, which reads come before which writes), so keeping them next to the tools
|
|
6
6
|
* is the only way they cannot drift apart. An agent host may append its own persona — who the
|
|
7
7
|
* agent is, how it speaks — on top of this.
|
|
8
|
+
*
|
|
9
|
+
* The two-step rule is written for a host that passes a `ConfirmationLedger` turn to the tools:
|
|
10
|
+
* the tools then refuse a confirmation from the same turn, so the persona and the code agree.
|
|
8
11
|
*/
|
|
9
|
-
export const SALON_AGENT_PERSONA_PL = `Jesteś asystentem
|
|
12
|
+
export const SALON_AGENT_PERSONA_PL = `Jesteś asystentem właścicielki salonu w aplikacji Sultana.
|
|
10
13
|
Rozmawiasz po polsku, krótko i konkretnie. Wykonujesz zmiany w salonie za pomocą narzędzi.
|
|
11
14
|
|
|
12
15
|
Zasady pracy:
|
|
@@ -15,12 +18,20 @@ Zasady pracy:
|
|
|
15
18
|
- Zanim przypiszesz pracowników, wywołaj list_workers.
|
|
16
19
|
- Cena to pełne złote (liczba całkowita). Czas trwania to minuty, wielokrotność 5.
|
|
17
20
|
"półtorej godziny" to 90 minut, "kwadrans" to 15 minut.
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
- Gdy masz narzędzia zapisujące, list_services i list_promos pokazują też usługi
|
|
22
|
+
i promocje wyłączone — nie tylko to, co widzą klientki.
|
|
23
|
+
- Każda zmiana to dwa kroki w dwóch różnych wiadomościach właścicielki:
|
|
24
|
+
1. Wywołaj narzędzie zapisujące z confirmed=false. Pokaż właścicielce zdanie z pola summary,
|
|
25
|
+
zapytaj o zgodę i na tym zakończ odpowiedź.
|
|
26
|
+
2. Dopiero gdy w swojej NASTĘPNEJ wiadomości wyraźnie się zgodzi, wywołaj to samo narzędzie
|
|
27
|
+
z confirmed=true, z confirmationId z tego podglądu i z tymi samymi argumentami (pole echo).
|
|
28
|
+
Nigdy nie potwierdzaj w tej samej odpowiedzi, w której pokazałeś podgląd.
|
|
29
|
+
Gdy właścicielka chce coś inaczej, zrób nowy podgląd i znów poczekaj na zgodę.
|
|
30
|
+
- Przy usuwaniu poproś właścicielkę, żeby sama napisała dokładną nazwę usuwanej rzeczy,
|
|
31
|
+
i wstaw w confirmationPhrase to, co napisała. Nigdy nie wpisuj nazwy za nią.
|
|
32
|
+
- Jeśli masz tylko narzędzia do odczytu (list_…, find_…), nie możesz niczego zmienić —
|
|
33
|
+
powiedz, że zmiany wymagają otwartej strony „Salon Assistant” w panelu.
|
|
34
|
+
- Jeśli narzędzie zwróci status "error", przeczytaj summary i powiedz właścicielce,
|
|
24
35
|
co poszło nie tak. Nie powtarzaj tego samego wywołania w kółko.
|
|
25
36
|
- Jeśli brakuje Ci danych do wykonania polecenia, dopytaj o jedną rzecz naraz.
|
|
26
37
|
- Nie obiecuj zmian, których nie wykonałeś narzędziem.`;
|
package/dist/tools/promos.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { TurnConfirmations } from "../confirmations.js";
|
|
1
2
|
import type { SalonAccess, SalonCorePort } from "../salon-core-port.js";
|
|
2
3
|
import { type AgentTool } from "../types.js";
|
|
3
|
-
export declare function createPromoTools(port: SalonCorePort): AgentTool[];
|
|
4
|
+
export declare function createPromoTools(port: SalonCorePort, confirmations?: TurnConfirmations): AgentTool[];
|
|
4
5
|
/** `access` must match the port: `"public"` only for a port read anonymously, because the canister
|
|
5
6
|
* gives the salon's owner the whole library whatever the port calls. */
|
|
6
7
|
export declare function listPromosTool(port: SalonCorePort, access?: SalonAccess): AgentTool;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promos.d.ts","sourceRoot":"","sources":["../../src/tools/promos.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"promos.d.ts","sourceRoot":"","sources":["../../src/tools/promos.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,EAA8B,WAAW,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAsBtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,aAAa,CAAC,EAAE,iBAAiB,GAAG,SAAS,EAAE,CAYpG;AA2CD;wEACwE;AACxE,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,GAAE,WAAqB,GAAG,SAAS,CAmB5F"}
|
package/dist/tools/promos.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { MAX_ACTIVE_PROMOS_PER_SALON, MAX_PROMOS_PER_SALON } from "@jsm-mit/sultana-core-motoko-package";
|
|
2
2
|
import { toToolError } from "../errors.js";
|
|
3
|
-
import { err,
|
|
3
|
+
import { err, ok } from "../types.js";
|
|
4
4
|
import { ArgumentError, phrasesMatch, readBoolean, readOptionalNumber, readOptionalString, readOptionalStringArray, readString, } from "./args.js";
|
|
5
|
+
import { writeGate } from "./write-gate.js";
|
|
5
6
|
const MAX_PROMO_NAME_LENGTH = 60;
|
|
6
7
|
const MAX_TEXT_LINES = 2;
|
|
7
8
|
const MAX_CHIPS = 2;
|
|
@@ -9,17 +10,17 @@ const CONFIRMED_FIELD = {
|
|
|
9
10
|
type: "boolean",
|
|
10
11
|
description: "false dla podglądu: narzędzie nic nie zapisze i odda opis zmiany do zatwierdzenia przez właściciela. true dopiero po potwierdzeniu.",
|
|
11
12
|
};
|
|
12
|
-
export function createPromoTools(port) {
|
|
13
|
+
export function createPromoTools(port, confirmations) {
|
|
13
14
|
return [
|
|
14
15
|
listMediaTool(port),
|
|
15
16
|
listPromosTool(port),
|
|
16
|
-
addPromoTool(port),
|
|
17
|
-
updatePromoTool(port),
|
|
18
|
-
setPromoActiveTool(port),
|
|
19
|
-
removePromoTool(port),
|
|
17
|
+
addPromoTool(port, confirmations),
|
|
18
|
+
updatePromoTool(port, confirmations),
|
|
19
|
+
setPromoActiveTool(port, confirmations),
|
|
20
|
+
removePromoTool(port, confirmations),
|
|
20
21
|
listDiscountCodesTool(port),
|
|
21
|
-
addDiscountCodeTool(port),
|
|
22
|
-
removeDiscountCodeTool(port),
|
|
22
|
+
addDiscountCodeTool(port, confirmations),
|
|
23
|
+
removeDiscountCodeTool(port, confirmations),
|
|
23
24
|
];
|
|
24
25
|
}
|
|
25
26
|
function listMediaTool(port) {
|
|
@@ -81,12 +82,13 @@ export function listPromosTool(port, access = "owner") {
|
|
|
81
82
|
},
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
|
-
function addPromoTool(port) {
|
|
85
|
+
function addPromoTool(port, confirmations) {
|
|
86
|
+
const gate = writeGate("add_promo", confirmations);
|
|
85
87
|
return {
|
|
86
88
|
name: "add_promo",
|
|
87
89
|
progress: "Zakładam promocję…",
|
|
88
90
|
description: "Zakłada nową promocję na materiale z biblioteki. Najpierw wywołaj list_media i wybierz assetId. Nowa promocja jest nieaktywna — włącz ją potem przez set_promo_active.",
|
|
89
|
-
parameters: {
|
|
91
|
+
parameters: gate.parameters({
|
|
90
92
|
type: "object",
|
|
91
93
|
properties: {
|
|
92
94
|
name: { type: "string", description: `Nazwa kampanii, maksymalnie ${MAX_PROMO_NAME_LENGTH} znaków.` },
|
|
@@ -104,13 +106,19 @@ function addPromoTool(port) {
|
|
|
104
106
|
},
|
|
105
107
|
required: ["name", "assetId", "confirmed"],
|
|
106
108
|
additionalProperties: false,
|
|
107
|
-
},
|
|
109
|
+
}),
|
|
108
110
|
execute: async (args) => {
|
|
109
111
|
try {
|
|
112
|
+
const refusal = gate.refusal(args);
|
|
113
|
+
if (refusal)
|
|
114
|
+
return refusal;
|
|
110
115
|
const input = await buildPromoInput(port, args, null);
|
|
111
116
|
if (!readBoolean(args, "confirmed")) {
|
|
112
|
-
return
|
|
117
|
+
return gate.preview(`Założę promocję ${await describePromo(port, input)}.`, args);
|
|
113
118
|
}
|
|
119
|
+
const refused = gate.redeem(args);
|
|
120
|
+
if (refused)
|
|
121
|
+
return refused;
|
|
114
122
|
const promoId = await port.addPromo(input);
|
|
115
123
|
return ok(`Założono promocję „${input.name}”. Jest jeszcze nieaktywna.`, { promoId });
|
|
116
124
|
}
|
|
@@ -122,12 +130,13 @@ function addPromoTool(port) {
|
|
|
122
130
|
},
|
|
123
131
|
};
|
|
124
132
|
}
|
|
125
|
-
function updatePromoTool(port) {
|
|
133
|
+
function updatePromoTool(port, confirmations) {
|
|
134
|
+
const gate = writeGate("update_promo", confirmations);
|
|
126
135
|
return {
|
|
127
136
|
name: "update_promo",
|
|
128
137
|
progress: "Zmieniam promocję…",
|
|
129
138
|
description: "Zmienia istniejącą promocję. Podaj tylko pola, które mają się zmienić — reszta zostanie zachowana. Promocja zachowuje swoje miejsce w kanale, więc poprawka nie restartuje kampanii.",
|
|
130
|
-
parameters: {
|
|
139
|
+
parameters: gate.parameters({
|
|
131
140
|
type: "object",
|
|
132
141
|
properties: {
|
|
133
142
|
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
@@ -142,20 +151,23 @@ function updatePromoTool(port) {
|
|
|
142
151
|
},
|
|
143
152
|
required: ["promoId", "confirmed"],
|
|
144
153
|
additionalProperties: false,
|
|
145
|
-
},
|
|
154
|
+
}),
|
|
146
155
|
execute: async (args) => {
|
|
147
156
|
try {
|
|
157
|
+
const refusal = gate.refusal(args);
|
|
158
|
+
if (refusal)
|
|
159
|
+
return refusal;
|
|
148
160
|
const promoId = readString(args, "promoId");
|
|
149
161
|
const current = await findPromo(port, promoId);
|
|
150
162
|
if (!current)
|
|
151
163
|
return err("not_found", `W tym salonie nie ma promocji o id ${promoId}.`);
|
|
152
164
|
const input = await buildPromoInput(port, args, current);
|
|
153
165
|
if (!readBoolean(args, "confirmed")) {
|
|
154
|
-
return
|
|
155
|
-
...args,
|
|
156
|
-
confirmed: true,
|
|
157
|
-
});
|
|
166
|
+
return gate.preview(`Zmienię promocję „${current.name}” na: ${await describePromo(port, input)}.`, args);
|
|
158
167
|
}
|
|
168
|
+
const refused = gate.redeem(args);
|
|
169
|
+
if (refused)
|
|
170
|
+
return refused;
|
|
159
171
|
await port.updatePromo(promoId, input);
|
|
160
172
|
return ok(`Zmieniono promocję „${input.name}”.`);
|
|
161
173
|
}
|
|
@@ -167,12 +179,13 @@ function updatePromoTool(port) {
|
|
|
167
179
|
},
|
|
168
180
|
};
|
|
169
181
|
}
|
|
170
|
-
function setPromoActiveTool(port) {
|
|
182
|
+
function setPromoActiveTool(port, confirmations) {
|
|
183
|
+
const gate = writeGate("set_promo_active", confirmations);
|
|
171
184
|
return {
|
|
172
185
|
name: "set_promo_active",
|
|
173
186
|
progress: "Zmieniam status promocji…",
|
|
174
187
|
description: `Włącza promocję w kanale albo ją wyłącza. Naraz może być aktywnych najwyżej ${MAX_ACTIVE_PROMOS_PER_SALON} — żeby włączyć kolejną, najpierw wyłącz inną.`,
|
|
175
|
-
parameters: {
|
|
188
|
+
parameters: gate.parameters({
|
|
176
189
|
type: "object",
|
|
177
190
|
properties: {
|
|
178
191
|
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
@@ -181,9 +194,12 @@ function setPromoActiveTool(port) {
|
|
|
181
194
|
},
|
|
182
195
|
required: ["promoId", "active", "confirmed"],
|
|
183
196
|
additionalProperties: false,
|
|
184
|
-
},
|
|
197
|
+
}),
|
|
185
198
|
execute: async (args) => {
|
|
186
199
|
try {
|
|
200
|
+
const refusal = gate.refusal(args);
|
|
201
|
+
if (refusal)
|
|
202
|
+
return refusal;
|
|
187
203
|
const promoId = readString(args, "promoId");
|
|
188
204
|
const current = await findPromo(port, promoId);
|
|
189
205
|
if (!current)
|
|
@@ -191,11 +207,11 @@ function setPromoActiveTool(port) {
|
|
|
191
207
|
const active = readBoolean(args, "active");
|
|
192
208
|
const verb = active ? "włączę" : "wyłączę";
|
|
193
209
|
if (!readBoolean(args, "confirmed")) {
|
|
194
|
-
return
|
|
195
|
-
...args,
|
|
196
|
-
confirmed: true,
|
|
197
|
-
});
|
|
210
|
+
return gate.preview(`${verb[0].toUpperCase()}${verb.slice(1)} promocję „${current.name}”.`, args);
|
|
198
211
|
}
|
|
212
|
+
const refused = gate.redeem(args);
|
|
213
|
+
if (refused)
|
|
214
|
+
return refused;
|
|
199
215
|
await port.setPromoActive(promoId, active);
|
|
200
216
|
return ok(`Promocja „${current.name}” jest teraz ${active ? "aktywna" : "wyłączona"}.`);
|
|
201
217
|
}
|
|
@@ -207,12 +223,13 @@ function setPromoActiveTool(port) {
|
|
|
207
223
|
},
|
|
208
224
|
};
|
|
209
225
|
}
|
|
210
|
-
function removePromoTool(port) {
|
|
226
|
+
function removePromoTool(port, confirmations) {
|
|
227
|
+
const gate = writeGate("remove_promo", confirmations);
|
|
211
228
|
return {
|
|
212
229
|
name: "remove_promo",
|
|
213
230
|
progress: "Kasuję promocję…",
|
|
214
231
|
description: "Kasuje promocję z biblioteki. Nieodwracalne — właściciel musi przepisać jej nazwę w confirmationPhrase. Żeby tylko zdjąć ją z kanału, użyj set_promo_active.",
|
|
215
|
-
parameters: {
|
|
232
|
+
parameters: gate.parameters({
|
|
216
233
|
type: "object",
|
|
217
234
|
properties: {
|
|
218
235
|
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
@@ -221,20 +238,29 @@ function removePromoTool(port) {
|
|
|
221
238
|
},
|
|
222
239
|
required: ["promoId", "confirmed"],
|
|
223
240
|
additionalProperties: false,
|
|
224
|
-
},
|
|
241
|
+
}),
|
|
225
242
|
execute: async (args) => {
|
|
226
243
|
try {
|
|
227
244
|
const promoId = readString(args, "promoId");
|
|
245
|
+
// No confirmationPhrase in the echo: the name is the owner's to type, so a model that
|
|
246
|
+
// simply sends the echo back cannot delete anything.
|
|
247
|
+
const echo = { promoId };
|
|
248
|
+
const refusal = gate.refusal(args, echo);
|
|
249
|
+
if (refusal)
|
|
250
|
+
return refusal;
|
|
228
251
|
const current = await findPromo(port, promoId);
|
|
229
252
|
if (!current)
|
|
230
253
|
return err("not_found", `W tym salonie nie ma promocji o id ${promoId}.`);
|
|
231
254
|
if (!readBoolean(args, "confirmed")) {
|
|
232
|
-
return
|
|
255
|
+
return gate.preview(`Skasuję promocję „${current.name}”. Tego nie da się cofnąć — poproś właścicielkę, żeby sama napisała dokładną nazwę promocji, i wstaw ją w confirmationPhrase. Jeśli chodzi tylko o zdjęcie z kanału, lepiej ją wyłączyć.`, echo);
|
|
233
256
|
}
|
|
234
257
|
const phrase = readOptionalString(args, "confirmationPhrase");
|
|
235
258
|
if (!phrase || !phrasesMatch(phrase, current.name)) {
|
|
236
|
-
return err("invalid_arguments", `Aby skasować promocję,
|
|
259
|
+
return err("invalid_arguments", `Aby skasować promocję, właścicielka musi sama napisać jej dokładną nazwę: „${current.name}”.`);
|
|
237
260
|
}
|
|
261
|
+
const refused = gate.redeem(args, echo);
|
|
262
|
+
if (refused)
|
|
263
|
+
return refused;
|
|
238
264
|
await port.removePromo(promoId);
|
|
239
265
|
return ok(`Skasowano promocję „${current.name}”.`);
|
|
240
266
|
}
|
|
@@ -265,12 +291,13 @@ function listDiscountCodesTool(port) {
|
|
|
265
291
|
},
|
|
266
292
|
};
|
|
267
293
|
}
|
|
268
|
-
function addDiscountCodeTool(port) {
|
|
294
|
+
function addDiscountCodeTool(port, confirmations) {
|
|
295
|
+
const gate = writeGate("add_discount_code", confirmations);
|
|
269
296
|
return {
|
|
270
297
|
name: "add_discount_code",
|
|
271
298
|
progress: "Dodaję kod rabatowy…",
|
|
272
299
|
description: "Dodaje kod rabatowy salonu. Podaj albo percent, albo amountPln — nie oba. Kod nie ma daty ważności ani limitu użyć; działa, dopóki go nie usuniesz.",
|
|
273
|
-
parameters: {
|
|
300
|
+
parameters: gate.parameters({
|
|
274
301
|
type: "object",
|
|
275
302
|
properties: {
|
|
276
303
|
code: { type: "string", description: "Treść kodu, którą wpisze klientka." },
|
|
@@ -281,9 +308,12 @@ function addDiscountCodeTool(port) {
|
|
|
281
308
|
},
|
|
282
309
|
required: ["code", "confirmed"],
|
|
283
310
|
additionalProperties: false,
|
|
284
|
-
},
|
|
311
|
+
}),
|
|
285
312
|
execute: async (args) => {
|
|
286
313
|
try {
|
|
314
|
+
const refusal = gate.refusal(args);
|
|
315
|
+
if (refusal)
|
|
316
|
+
return refusal;
|
|
287
317
|
const code = readString(args, "code");
|
|
288
318
|
const percent = readOptionalNumber(args, "percent");
|
|
289
319
|
const amountPln = readOptionalNumber(args, "amountPln");
|
|
@@ -301,8 +331,11 @@ function addDiscountCodeTool(port) {
|
|
|
301
331
|
const value = percent !== undefined ? `${percent}%` : `${amountPln} zł`;
|
|
302
332
|
const scope = scopeServiceId ? "na jedną usługę" : "na cały salon";
|
|
303
333
|
if (!readBoolean(args, "confirmed")) {
|
|
304
|
-
return
|
|
334
|
+
return gate.preview(`Dodam kod rabatowy „${code}”: ${value}, ${scope}.`, args);
|
|
305
335
|
}
|
|
336
|
+
const refused = gate.redeem(args);
|
|
337
|
+
if (refused)
|
|
338
|
+
return refused;
|
|
306
339
|
await port.addDiscountCode({ code, percent, amountPln, scopeServiceId });
|
|
307
340
|
return ok(`Dodano kod rabatowy „${code}” (${value}, ${scope}).`);
|
|
308
341
|
}
|
|
@@ -314,12 +347,13 @@ function addDiscountCodeTool(port) {
|
|
|
314
347
|
},
|
|
315
348
|
};
|
|
316
349
|
}
|
|
317
|
-
function removeDiscountCodeTool(port) {
|
|
350
|
+
function removeDiscountCodeTool(port, confirmations) {
|
|
351
|
+
const gate = writeGate("remove_discount_code", confirmations);
|
|
318
352
|
return {
|
|
319
353
|
name: "remove_discount_code",
|
|
320
354
|
progress: "Usuwam kod rabatowy…",
|
|
321
355
|
description: "Usuwa kod rabatowy salonu. Kanister odmówi, dopóki jakakolwiek promocja ten kod rozgłasza — wtedy najpierw zmień promocję.",
|
|
322
|
-
parameters: {
|
|
356
|
+
parameters: gate.parameters({
|
|
323
357
|
type: "object",
|
|
324
358
|
properties: {
|
|
325
359
|
code: { type: "string", description: "Treść kodu z list_discount_codes." },
|
|
@@ -327,18 +361,24 @@ function removeDiscountCodeTool(port) {
|
|
|
327
361
|
},
|
|
328
362
|
required: ["code", "confirmed"],
|
|
329
363
|
additionalProperties: false,
|
|
330
|
-
},
|
|
364
|
+
}),
|
|
331
365
|
// No confirmation phrase here, unlike a service or a promo: a removed code can simply be
|
|
332
366
|
// added back with the same text, and nothing else in the salon points at it.
|
|
333
367
|
execute: async (args) => {
|
|
334
368
|
try {
|
|
369
|
+
const refusal = gate.refusal(args);
|
|
370
|
+
if (refusal)
|
|
371
|
+
return refusal;
|
|
335
372
|
const code = readString(args, "code");
|
|
336
373
|
const codes = await port.listDiscountCodes();
|
|
337
374
|
if (!codes.some((entry) => entry.code === code))
|
|
338
375
|
return err("not_found", `Salon nie ma kodu „${code}”.`);
|
|
339
376
|
if (!readBoolean(args, "confirmed")) {
|
|
340
|
-
return
|
|
377
|
+
return gate.preview(`Usunę kod rabatowy „${code}”.`, args);
|
|
341
378
|
}
|
|
379
|
+
const refused = gate.redeem(args);
|
|
380
|
+
if (refused)
|
|
381
|
+
return refused;
|
|
342
382
|
await port.removeDiscountCode(code);
|
|
343
383
|
return ok(`Usunięto kod rabatowy „${code}”.`);
|
|
344
384
|
}
|
package/dist/tools/schedule.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import type { TurnConfirmations } from "../confirmations.js";
|
|
1
2
|
import type { SalonCorePort } from "../salon-core-port.js";
|
|
2
3
|
import { type AgentTool } from "../types.js";
|
|
3
|
-
export declare function createScheduleTools(port: SalonCorePort): AgentTool[];
|
|
4
|
+
export declare function createScheduleTools(port: SalonCorePort, confirmations?: TurnConfirmations): AgentTool[];
|
|
4
5
|
//# sourceMappingURL=schedule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/tools/schedule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/tools/schedule.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAsBtD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,EAAE,aAAa,CAAC,EAAE,iBAAiB,GAAG,SAAS,EAAE,CASvG"}
|
package/dist/tools/schedule.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { formatMinutesToTime, parseTimeRangesToSlots, parseTimeToMinutes } from "@jsm-mit/sultana-core-motoko-package";
|
|
2
2
|
import { toToolError } from "../errors.js";
|
|
3
|
-
import { err,
|
|
3
|
+
import { err, ok } from "../types.js";
|
|
4
4
|
import { ArgumentError, readBoolean, readOptionalString, readString } from "./args.js";
|
|
5
|
+
import { writeGate } from "./write-gate.js";
|
|
5
6
|
/** 0 = Monday, matching the canister's own day index. */
|
|
6
7
|
const DAY_NAMES_PL = ["poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"];
|
|
7
8
|
const CONFIRMED_FIELD = {
|
|
@@ -14,14 +15,14 @@ const RANGES_FIELD = {
|
|
|
14
15
|
type: "string",
|
|
15
16
|
description: 'Zakresy godzin oddzielone przecinkiem, np. "10-18" albo "9:30-13,14-18".',
|
|
16
17
|
};
|
|
17
|
-
export function createScheduleTools(port) {
|
|
18
|
+
export function createScheduleTools(port, confirmations) {
|
|
18
19
|
return [
|
|
19
20
|
getWeeklyHoursTool(port),
|
|
20
|
-
setWeeklyHoursTool(port),
|
|
21
|
+
setWeeklyHoursTool(port, confirmations),
|
|
21
22
|
getDayScheduleTool(port),
|
|
22
|
-
setDayOffTool(port),
|
|
23
|
-
setBusyHoursTool(port),
|
|
24
|
-
clearDailyOverrideTool(port),
|
|
23
|
+
setDayOffTool(port, confirmations),
|
|
24
|
+
setBusyHoursTool(port, confirmations),
|
|
25
|
+
clearDailyOverrideTool(port, confirmations),
|
|
25
26
|
];
|
|
26
27
|
}
|
|
27
28
|
function getWeeklyHoursTool(port) {
|
|
@@ -53,12 +54,13 @@ function getWeeklyHoursTool(port) {
|
|
|
53
54
|
},
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
|
-
function setWeeklyHoursTool(port) {
|
|
57
|
+
function setWeeklyHoursTool(port, confirmations) {
|
|
58
|
+
const gate = writeGate("set_weekly_hours", confirmations);
|
|
57
59
|
return {
|
|
58
60
|
name: "set_weekly_hours",
|
|
59
61
|
progress: "Ustawiam grafik…",
|
|
60
62
|
description: "Ustawia godziny PRACY pracownika w wybranych dniach tygodnia. Zastępuje dotychczasowe godziny w tych dniach. Pusty zakres oznacza dzień niepracujący.",
|
|
61
|
-
parameters: {
|
|
63
|
+
parameters: gate.parameters({
|
|
62
64
|
type: "object",
|
|
63
65
|
properties: {
|
|
64
66
|
workerId: WORKER_FIELD,
|
|
@@ -72,20 +74,23 @@ function setWeeklyHoursTool(port) {
|
|
|
72
74
|
},
|
|
73
75
|
required: ["workerId", "days", "ranges", "confirmed"],
|
|
74
76
|
additionalProperties: false,
|
|
75
|
-
},
|
|
77
|
+
}),
|
|
76
78
|
execute: async (args) => {
|
|
77
79
|
try {
|
|
80
|
+
const refusal = gate.refusal(args);
|
|
81
|
+
if (refusal)
|
|
82
|
+
return refusal;
|
|
78
83
|
const worker = await resolveWorker(port, readString(args, "workerId"));
|
|
79
84
|
const days = readDays(args);
|
|
80
85
|
const parsed = normalizeRanges(readOptionalString(args, "ranges") ?? "");
|
|
81
86
|
const dayNames = days.map((day) => DAY_NAMES_PL[day]).join(", ");
|
|
82
87
|
const hours = parsed.ranges.length === 0 ? "dzień wolny" : formatRanges(parsed.ranges).join(", ");
|
|
83
88
|
if (!readBoolean(args, "confirmed")) {
|
|
84
|
-
return
|
|
85
|
-
...args,
|
|
86
|
-
confirmed: true,
|
|
87
|
-
});
|
|
89
|
+
return gate.preview(`Ustawię ${worker.name}: ${dayNames} — ${hours}.${noteSuffix(parsed.notes)}`, args);
|
|
88
90
|
}
|
|
91
|
+
const refused = gate.redeem(args);
|
|
92
|
+
if (refused)
|
|
93
|
+
return refused;
|
|
89
94
|
for (const day of days) {
|
|
90
95
|
await port.setWeeklyHours(worker.id, day, parsed.ranges);
|
|
91
96
|
}
|
|
@@ -127,24 +132,31 @@ function getDayScheduleTool(port) {
|
|
|
127
132
|
},
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
|
-
function setDayOffTool(port) {
|
|
135
|
+
function setDayOffTool(port, confirmations) {
|
|
136
|
+
const gate = writeGate("set_day_off", confirmations);
|
|
131
137
|
return {
|
|
132
138
|
name: "set_day_off",
|
|
133
139
|
progress: "Ustawiam dzień wolny…",
|
|
134
140
|
description: "Oznacza cały wskazany dzień jako wolny dla pracownika.",
|
|
135
|
-
parameters: {
|
|
141
|
+
parameters: gate.parameters({
|
|
136
142
|
type: "object",
|
|
137
143
|
properties: { workerId: WORKER_FIELD, date: DATE_FIELD, confirmed: CONFIRMED_FIELD },
|
|
138
144
|
required: ["workerId", "date", "confirmed"],
|
|
139
145
|
additionalProperties: false,
|
|
140
|
-
},
|
|
146
|
+
}),
|
|
141
147
|
execute: async (args) => {
|
|
142
148
|
try {
|
|
149
|
+
const refusal = gate.refusal(args);
|
|
150
|
+
if (refusal)
|
|
151
|
+
return refusal;
|
|
143
152
|
const worker = await resolveWorker(port, readString(args, "workerId"));
|
|
144
153
|
const date = readDate(args);
|
|
145
154
|
if (!readBoolean(args, "confirmed")) {
|
|
146
|
-
return
|
|
155
|
+
return gate.preview(`Ustawię ${worker.name} dzień wolny: ${date}.`, args);
|
|
147
156
|
}
|
|
157
|
+
const refused = gate.redeem(args);
|
|
158
|
+
if (refused)
|
|
159
|
+
return refused;
|
|
148
160
|
await port.setDayOff(worker.id, date);
|
|
149
161
|
return ok(`${worker.name} ma wolne ${date}.`);
|
|
150
162
|
}
|
|
@@ -156,19 +168,23 @@ function setDayOffTool(port) {
|
|
|
156
168
|
},
|
|
157
169
|
};
|
|
158
170
|
}
|
|
159
|
-
function setBusyHoursTool(port) {
|
|
171
|
+
function setBusyHoursTool(port, confirmations) {
|
|
172
|
+
const gate = writeGate("set_busy_hours", confirmations);
|
|
160
173
|
return {
|
|
161
174
|
name: "set_busy_hours",
|
|
162
175
|
progress: "Zapisuję godziny zajęte…",
|
|
163
176
|
description: "Ustawia godziny ZAJĘTE w konkretnym dniu — nadpisuje grafik tygodniowy tylko tego dnia. Używaj do jednorazowych nieobecności („we wtorek jestem zajęta od 12 do 15”).",
|
|
164
|
-
parameters: {
|
|
177
|
+
parameters: gate.parameters({
|
|
165
178
|
type: "object",
|
|
166
179
|
properties: { workerId: WORKER_FIELD, date: DATE_FIELD, ranges: RANGES_FIELD, confirmed: CONFIRMED_FIELD },
|
|
167
180
|
required: ["workerId", "date", "ranges", "confirmed"],
|
|
168
181
|
additionalProperties: false,
|
|
169
|
-
},
|
|
182
|
+
}),
|
|
170
183
|
execute: async (args) => {
|
|
171
184
|
try {
|
|
185
|
+
const refusal = gate.refusal(args);
|
|
186
|
+
if (refusal)
|
|
187
|
+
return refusal;
|
|
172
188
|
const worker = await resolveWorker(port, readString(args, "workerId"));
|
|
173
189
|
const date = readDate(args);
|
|
174
190
|
const parsed = normalizeRanges(readString(args, "ranges"));
|
|
@@ -177,11 +193,11 @@ function setBusyHoursTool(port) {
|
|
|
177
193
|
}
|
|
178
194
|
const hours = formatRanges(parsed.ranges).join(", ");
|
|
179
195
|
if (!readBoolean(args, "confirmed")) {
|
|
180
|
-
return
|
|
181
|
-
...args,
|
|
182
|
-
confirmed: true,
|
|
183
|
-
});
|
|
196
|
+
return gate.preview(`Zaznaczę ${worker.name} jako zajętą ${date}: ${hours}.${noteSuffix(parsed.notes)}`, args);
|
|
184
197
|
}
|
|
198
|
+
const refused = gate.redeem(args);
|
|
199
|
+
if (refused)
|
|
200
|
+
return refused;
|
|
185
201
|
await port.setDailyBusy(worker.id, date, parsed.ranges);
|
|
186
202
|
return ok(`${worker.name}, ${date}: zajęte ${hours}.${noteSuffix(parsed.notes)}`);
|
|
187
203
|
}
|
|
@@ -193,27 +209,31 @@ function setBusyHoursTool(port) {
|
|
|
193
209
|
},
|
|
194
210
|
};
|
|
195
211
|
}
|
|
196
|
-
function clearDailyOverrideTool(port) {
|
|
212
|
+
function clearDailyOverrideTool(port, confirmations) {
|
|
213
|
+
const gate = writeGate("clear_daily_override", confirmations);
|
|
197
214
|
return {
|
|
198
215
|
name: "clear_daily_override",
|
|
199
216
|
progress: "Przywracam grafik tygodniowy…",
|
|
200
217
|
description: "Usuwa nadpisanie dla wskazanego dnia — od tej chwili obowiązuje zwykły grafik tygodniowy.",
|
|
201
|
-
parameters: {
|
|
218
|
+
parameters: gate.parameters({
|
|
202
219
|
type: "object",
|
|
203
220
|
properties: { workerId: WORKER_FIELD, date: DATE_FIELD, confirmed: CONFIRMED_FIELD },
|
|
204
221
|
required: ["workerId", "date", "confirmed"],
|
|
205
222
|
additionalProperties: false,
|
|
206
|
-
},
|
|
223
|
+
}),
|
|
207
224
|
execute: async (args) => {
|
|
208
225
|
try {
|
|
226
|
+
const refusal = gate.refusal(args);
|
|
227
|
+
if (refusal)
|
|
228
|
+
return refusal;
|
|
209
229
|
const worker = await resolveWorker(port, readString(args, "workerId"));
|
|
210
230
|
const date = readDate(args);
|
|
211
231
|
if (!readBoolean(args, "confirmed")) {
|
|
212
|
-
return
|
|
213
|
-
...args,
|
|
214
|
-
confirmed: true,
|
|
215
|
-
});
|
|
232
|
+
return gate.preview(`Usunę nadpisanie dnia ${date} dla ${worker.name} — wróci grafik tygodniowy.`, args);
|
|
216
233
|
}
|
|
234
|
+
const refused = gate.redeem(args);
|
|
235
|
+
if (refused)
|
|
236
|
+
return refused;
|
|
217
237
|
await port.clearDailyOverride(worker.id, date);
|
|
218
238
|
return ok(`${worker.name}, ${date}: wrócił grafik tygodniowy.`);
|
|
219
239
|
}
|
package/dist/tools/services.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { TurnConfirmations } from "../confirmations.js";
|
|
1
2
|
import type { SalonAccess, SalonCorePort } from "../salon-core-port.js";
|
|
2
3
|
import { type AgentTool } from "../types.js";
|
|
3
|
-
export declare function createServiceTools(port: SalonCorePort): AgentTool[];
|
|
4
|
+
export declare function createServiceTools(port: SalonCorePort, confirmations?: TurnConfirmations): AgentTool[];
|
|
4
5
|
/** `access` must match the port: `"public"` only for a port that reads the visitor's view. */
|
|
5
6
|
export declare function listServicesTool(port: SalonCorePort, access?: SalonAccess): AgentTool;
|
|
6
7
|
export declare function findServiceTypeTool(port: SalonCorePort): AgentTool;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/tools/services.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/tools/services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAkC,MAAM,uBAAuB,CAAC;AACxG,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAwBtD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,aAAa,CAAC,EAAE,iBAAiB,GAAG,SAAS,EAAE,CAStG;AAoBD,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,GAAE,WAAqB,GAAG,SAAS,CAmB9F;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAiClE"}
|
package/dist/tools/services.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { toToolError } from "../errors.js";
|
|
2
|
-
import { err,
|
|
2
|
+
import { err, ok } from "../types.js";
|
|
3
3
|
import { ArgumentError, normalizeDuration, normalizePrice, phrasesMatch, readBoolean, readNumber, readOptionalNumber, readOptionalString, readOptionalStringArray, readString, } from "./args.js";
|
|
4
|
+
import { writeGate } from "./write-gate.js";
|
|
4
5
|
const CONFIRMED_FIELD = {
|
|
5
6
|
type: "boolean",
|
|
6
7
|
description: "false dla podglądu: narzędzie nic nie zapisze i odda opis zmiany do zatwierdzenia przez właściciela. true dopiero po tym, jak właściciel potwierdzi.",
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
/** The fields of a service a model may set — what `update_service` echoes back when it was sent. */
|
|
10
|
+
const SERVICE_FIELDS = ["name", "pricePln", "durationMinutes", "serviceTypeIds", "workerIds", "active"];
|
|
11
|
+
export function createServiceTools(port, confirmations) {
|
|
9
12
|
return [
|
|
10
13
|
listServicesTool(port),
|
|
11
14
|
findServiceTypeTool(port),
|
|
12
15
|
listWorkersTool(port),
|
|
13
|
-
addServiceTool(port),
|
|
14
|
-
updateServiceTool(port),
|
|
15
|
-
removeServiceTool(port),
|
|
16
|
+
addServiceTool(port, confirmations),
|
|
17
|
+
updateServiceTool(port, confirmations),
|
|
18
|
+
removeServiceTool(port, confirmations),
|
|
16
19
|
];
|
|
17
20
|
}
|
|
18
21
|
/** What the model reads about the list depends on whose view the port returns: a public read has
|
|
@@ -102,12 +105,13 @@ function listWorkersTool(port) {
|
|
|
102
105
|
},
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
|
-
function addServiceTool(port) {
|
|
108
|
+
function addServiceTool(port, confirmations) {
|
|
109
|
+
const gate = writeGate("add_service", confirmations);
|
|
106
110
|
return {
|
|
107
111
|
name: "add_service",
|
|
108
112
|
progress: "Dodaję usługę…",
|
|
109
113
|
description: "Dodaje nową usługę do katalogu salonu. Najpierw ustal typ usługi przez find_service_type i pracowników przez list_workers.",
|
|
110
|
-
parameters: {
|
|
114
|
+
parameters: gate.parameters({
|
|
111
115
|
type: "object",
|
|
112
116
|
properties: {
|
|
113
117
|
name: { type: "string", description: "Nazwa usługi widoczna dla klientek." },
|
|
@@ -128,17 +132,23 @@ function addServiceTool(port) {
|
|
|
128
132
|
},
|
|
129
133
|
required: ["name", "pricePln", "durationMinutes", "confirmed"],
|
|
130
134
|
additionalProperties: false,
|
|
131
|
-
},
|
|
135
|
+
}),
|
|
132
136
|
execute: async (args) => {
|
|
133
137
|
try {
|
|
134
|
-
|
|
138
|
+
// The echo is the draft laid over the model's own arguments, and a draft read from an
|
|
139
|
+
// echo is that echo again — resending either one confirms the same change.
|
|
140
|
+
const draft = readDraft(args, null);
|
|
141
|
+
const echo = { ...args, ...draft.input };
|
|
142
|
+
const refusal = gate.refusal(args, echo);
|
|
143
|
+
if (refusal)
|
|
144
|
+
return refusal;
|
|
145
|
+
await assertDraftReferences(port, draft.input);
|
|
135
146
|
if (!readBoolean(args, "confirmed")) {
|
|
136
|
-
return
|
|
137
|
-
...args,
|
|
138
|
-
...draft.input,
|
|
139
|
-
confirmed: true,
|
|
140
|
-
});
|
|
147
|
+
return gate.preview(`Dodam usługę: ${await describe(port, draft.input)}.${noteSuffix(draft.notes)}`, echo);
|
|
141
148
|
}
|
|
149
|
+
const refused = gate.redeem(args, echo);
|
|
150
|
+
if (refused)
|
|
151
|
+
return refused;
|
|
142
152
|
const serviceId = await port.addService(draft.input);
|
|
143
153
|
return ok(`Dodano usługę „${draft.input.name}”.${noteSuffix(draft.notes)}`, { serviceId });
|
|
144
154
|
}
|
|
@@ -150,12 +160,13 @@ function addServiceTool(port) {
|
|
|
150
160
|
},
|
|
151
161
|
};
|
|
152
162
|
}
|
|
153
|
-
function updateServiceTool(port) {
|
|
163
|
+
function updateServiceTool(port, confirmations) {
|
|
164
|
+
const gate = writeGate("update_service", confirmations);
|
|
154
165
|
return {
|
|
155
166
|
name: "update_service",
|
|
156
167
|
progress: "Zmieniam usługę…",
|
|
157
168
|
description: "Zmienia istniejącą usługę. Podaj tylko te pola, które mają się zmienić — reszta zostanie zachowana. Id weź z list_services.",
|
|
158
|
-
parameters: {
|
|
169
|
+
parameters: gate.parameters({
|
|
159
170
|
type: "object",
|
|
160
171
|
properties: {
|
|
161
172
|
salonServiceId: { type: "string", description: "Id usługi z list_services." },
|
|
@@ -169,17 +180,27 @@ function updateServiceTool(port) {
|
|
|
169
180
|
},
|
|
170
181
|
required: ["salonServiceId", "confirmed"],
|
|
171
182
|
additionalProperties: false,
|
|
172
|
-
},
|
|
183
|
+
}),
|
|
173
184
|
execute: async (args) => {
|
|
174
185
|
try {
|
|
175
186
|
const serviceId = readString(args, "salonServiceId");
|
|
187
|
+
const echo = requestedServiceChange(serviceId, args);
|
|
188
|
+
const refusal = gate.refusal(args, echo);
|
|
189
|
+
if (refusal)
|
|
190
|
+
return refusal;
|
|
176
191
|
const current = await findService(port, serviceId);
|
|
177
192
|
if (!current)
|
|
178
193
|
return err("not_found", `W tym salonie nie ma usługi o id ${serviceId}.`);
|
|
179
|
-
|
|
194
|
+
// The summary shows the merged record the owner will end up with; the write merges
|
|
195
|
+
// again onto the record as it is by then.
|
|
196
|
+
const draft = readDraft(args, current);
|
|
197
|
+
await assertDraftReferences(port, draft.input);
|
|
180
198
|
if (!readBoolean(args, "confirmed")) {
|
|
181
|
-
return
|
|
199
|
+
return gate.preview(`Zmienię usługę „${current.name}” na: ${await describe(port, draft.input)}.${noteSuffix(draft.notes)}`, echo);
|
|
182
200
|
}
|
|
201
|
+
const refused = gate.redeem(args, echo);
|
|
202
|
+
if (refused)
|
|
203
|
+
return refused;
|
|
183
204
|
await port.updateService(serviceId, draft.input);
|
|
184
205
|
return ok(`Zmieniono usługę „${draft.input.name}”.${noteSuffix(draft.notes)}`);
|
|
185
206
|
}
|
|
@@ -191,12 +212,13 @@ function updateServiceTool(port) {
|
|
|
191
212
|
},
|
|
192
213
|
};
|
|
193
214
|
}
|
|
194
|
-
function removeServiceTool(port) {
|
|
215
|
+
function removeServiceTool(port, confirmations) {
|
|
216
|
+
const gate = writeGate("remove_service", confirmations);
|
|
195
217
|
return {
|
|
196
218
|
name: "remove_service",
|
|
197
219
|
progress: "Usuwam usługę…",
|
|
198
220
|
description: "Usuwa usługę z katalogu. Operacja jest nieodwracalna — właściciel musi przepisać dokładną nazwę usługi w polu confirmationPhrase.",
|
|
199
|
-
parameters: {
|
|
221
|
+
parameters: gate.parameters({
|
|
200
222
|
type: "object",
|
|
201
223
|
properties: {
|
|
202
224
|
salonServiceId: { type: "string", description: "Id usługi z list_services." },
|
|
@@ -205,20 +227,29 @@ function removeServiceTool(port) {
|
|
|
205
227
|
},
|
|
206
228
|
required: ["salonServiceId", "confirmed"],
|
|
207
229
|
additionalProperties: false,
|
|
208
|
-
},
|
|
230
|
+
}),
|
|
209
231
|
execute: async (args) => {
|
|
210
232
|
try {
|
|
211
233
|
const serviceId = readString(args, "salonServiceId");
|
|
234
|
+
// No confirmationPhrase in the echo: the name is the owner's to type, so a model that
|
|
235
|
+
// simply sends the echo back cannot delete anything.
|
|
236
|
+
const echo = { salonServiceId: serviceId };
|
|
237
|
+
const refusal = gate.refusal(args, echo);
|
|
238
|
+
if (refusal)
|
|
239
|
+
return refusal;
|
|
212
240
|
const current = await findService(port, serviceId);
|
|
213
241
|
if (!current)
|
|
214
242
|
return err("not_found", `W tym salonie nie ma usługi o id ${serviceId}.`);
|
|
215
243
|
if (!readBoolean(args, "confirmed")) {
|
|
216
|
-
return
|
|
244
|
+
return gate.preview(`Usunę usługę „${current.name}” (${current.pricePln} zł, ${current.durationMinutes} min). Tego nie da się cofnąć — poproś właścicielkę, żeby sama napisała dokładną nazwę usługi, i wstaw ją w confirmationPhrase.`, echo);
|
|
217
245
|
}
|
|
218
246
|
const phrase = readOptionalString(args, "confirmationPhrase");
|
|
219
247
|
if (!phrase || !phrasesMatch(phrase, current.name)) {
|
|
220
|
-
return err("invalid_arguments", `Aby usunąć usługę,
|
|
248
|
+
return err("invalid_arguments", `Aby usunąć usługę, właścicielka musi sama napisać jej dokładną nazwę: „${current.name}”.`);
|
|
221
249
|
}
|
|
250
|
+
const refused = gate.redeem(args, echo);
|
|
251
|
+
if (refused)
|
|
252
|
+
return refused;
|
|
222
253
|
await port.removeService(serviceId);
|
|
223
254
|
return ok(`Usunięto usługę „${current.name}”.`);
|
|
224
255
|
}
|
|
@@ -230,12 +261,23 @@ function removeServiceTool(port) {
|
|
|
230
261
|
},
|
|
231
262
|
};
|
|
232
263
|
}
|
|
264
|
+
/** `update_service`'s echo: the id and only the fields the model sent, as it sent them — never the
|
|
265
|
+
* merged record, which would pin fields the owner did not mention to what they were at preview. */
|
|
266
|
+
function requestedServiceChange(serviceId, args) {
|
|
267
|
+
const change = { salonServiceId: serviceId };
|
|
268
|
+
for (const field of SERVICE_FIELDS) {
|
|
269
|
+
if (args[field] !== undefined && args[field] !== null)
|
|
270
|
+
change[field] = args[field];
|
|
271
|
+
}
|
|
272
|
+
return change;
|
|
273
|
+
}
|
|
233
274
|
/**
|
|
234
|
-
* Builds the full record the canister needs. `current` is null for a new
|
|
235
|
-
* record for an edit — **this merge is the whole point**:
|
|
236
|
-
* field, so a "just the price" call that forgot `workerIds`
|
|
275
|
+
* Builds the full record the canister needs, without calling it. `current` is null for a new
|
|
276
|
+
* service and the existing record for an edit — **this merge is the whole point**:
|
|
277
|
+
* `updateSalonService` replaces every field, so a "just the price" call that forgot `workerIds`
|
|
278
|
+
* would unassign the entire team.
|
|
237
279
|
*/
|
|
238
|
-
|
|
280
|
+
function readDraft(args, current) {
|
|
239
281
|
const notes = [];
|
|
240
282
|
const name = current ? (readOptionalString(args, "name") ?? current.name) : readString(args, "name");
|
|
241
283
|
const rawPrice = readOptionalNumber(args, "pricePln");
|
|
@@ -249,10 +291,12 @@ async function buildDraft(port, args, current) {
|
|
|
249
291
|
const serviceTypeIds = readOptionalStringArray(args, "serviceTypeIds") ?? current?.serviceTypeIds ?? [];
|
|
250
292
|
const workerIds = readOptionalStringArray(args, "workerIds") ?? current?.workerIds ?? [];
|
|
251
293
|
const active = args.active === undefined || args.active === null ? (current?.active ?? true) : readBoolean(args, "active");
|
|
252
|
-
await assertKnownServiceTypes(port, serviceTypeIds);
|
|
253
|
-
await assertKnownWorkers(port, workerIds);
|
|
254
294
|
return { input: { name, pricePln: price, durationMinutes: duration, active, serviceTypeIds, workerIds }, notes };
|
|
255
295
|
}
|
|
296
|
+
async function assertDraftReferences(port, input) {
|
|
297
|
+
await assertKnownServiceTypes(port, input.serviceTypeIds);
|
|
298
|
+
await assertKnownWorkers(port, input.workerIds);
|
|
299
|
+
}
|
|
256
300
|
function normalizeOrNote(normalized, notes) {
|
|
257
301
|
if (normalized.note)
|
|
258
302
|
notes.push(normalized.note);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TurnConfirmations } from "../confirmations.js";
|
|
2
|
+
import { type ToolParametersSchema, type ToolResult } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The confirmation protocol of ONE write tool, in one place for all of them.
|
|
5
|
+
*
|
|
6
|
+
* `echo` is the change as the tool describes it back to the model — the arguments to send again
|
|
7
|
+
* with `confirmed: true`; it defaults to `args` itself. A tool must build it from `args` the same
|
|
8
|
+
* way on both calls, and building it from an echo must give that echo back, so a model that resends
|
|
9
|
+
* its own arguments and a model that resends the echo confirm the same change.
|
|
10
|
+
*
|
|
11
|
+
* Without a ledger the gate lets every `confirmed: true` through, which is the protocol as it was
|
|
12
|
+
* before the ledger: the rule then lives in the persona only.
|
|
13
|
+
*/
|
|
14
|
+
export interface WriteGate {
|
|
15
|
+
/** The tool's schema, with `confirmationId` added when the tool set has a ledger. */
|
|
16
|
+
parameters(schema: ToolParametersSchema): ToolParametersSchema;
|
|
17
|
+
/** The `confirmed: false` answer: `echo` goes back with `confirmed: true` and, with a ledger,
|
|
18
|
+
* the id of this preview. */
|
|
19
|
+
preview(summary: string, echo: Record<string, unknown>): ToolResult;
|
|
20
|
+
/** Run first, before any canister call. For a `confirmed: true` call whose id would not redeem,
|
|
21
|
+
* the refusal to return; otherwise `null`. Does not use the id up, so a write that then fails
|
|
22
|
+
* on its arguments (a mistyped name, say) can be retried with the same id. */
|
|
23
|
+
refusal(args: Record<string, unknown>, echo?: Record<string, unknown>): ToolResult | null;
|
|
24
|
+
/** Run right before the write: uses the id up. `null` lets the write go ahead; a refusal here
|
|
25
|
+
* means a parallel call used the id first, or the preview expired in the meantime. */
|
|
26
|
+
redeem(args: Record<string, unknown>, echo?: Record<string, unknown>): ToolResult | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function writeGate(toolName: string, confirmations: TurnConfirmations | undefined): WriteGate;
|
|
29
|
+
//# sourceMappingURL=write-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-gate.d.ts","sourceRoot":"","sources":["../../src/tools/write-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqC,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAChG,OAAO,EAA0B,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAkBjG;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IACtB,qFAAqF;IACrF,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAC/D;iCAC6B;IAC7B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAAC;IACpE;;kFAE8E;IAC9E,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,GAAG,IAAI,CAAC;IAC1F;0FACsF;IACtF,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,GAAG,IAAI,CAAC;CAC5F;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAAG,SAAS,CA6BnG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { err, needsConfirmation } from "../types.js";
|
|
2
|
+
import { readBoolean, readOptionalString } from "./args.js";
|
|
3
|
+
const CONFIRMATION_ID_FIELD = {
|
|
4
|
+
type: "string",
|
|
5
|
+
description: "Id z podglądu (confirmed=false). Wymagane przy confirmed=true.",
|
|
6
|
+
};
|
|
7
|
+
const NO_PREVIEW = "Brak ważnego podglądu tej zmiany — wywołaj narzędzie z confirmed=false i pokaż właścicielce podsumowanie.";
|
|
8
|
+
const REFUSALS = {
|
|
9
|
+
missing: NO_PREVIEW,
|
|
10
|
+
unknown: NO_PREVIEW,
|
|
11
|
+
expired: NO_PREVIEW,
|
|
12
|
+
same_turn: "Zgoda musi przyjść w osobnej wiadomości właścicielki — najpierw pokaż podsumowanie i poczekaj na odpowiedź.",
|
|
13
|
+
mismatch: "Argumenty różnią się od pokazanego podglądu — pokaż nowy podgląd (confirmed=false).",
|
|
14
|
+
};
|
|
15
|
+
export function writeGate(toolName, confirmations) {
|
|
16
|
+
const decide = (args, verdict) => {
|
|
17
|
+
if (!confirmations || !readBoolean(args, "confirmed"))
|
|
18
|
+
return null;
|
|
19
|
+
const result = verdict(confirmations, readOptionalString(args, "confirmationId"));
|
|
20
|
+
return result.ok ? null : err("invalid_arguments", REFUSALS[result.reason]);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
parameters: (schema) => {
|
|
24
|
+
if (!confirmations)
|
|
25
|
+
return schema;
|
|
26
|
+
return { ...schema, properties: { ...schema.properties, confirmationId: CONFIRMATION_ID_FIELD } };
|
|
27
|
+
},
|
|
28
|
+
preview: (summary, echo) => {
|
|
29
|
+
if (!confirmations)
|
|
30
|
+
return needsConfirmation(summary, { ...echo, confirmed: true });
|
|
31
|
+
const confirmationId = confirmations.issue(toolName, echo);
|
|
32
|
+
return needsConfirmation(summary, { ...echo, confirmed: true, confirmationId });
|
|
33
|
+
},
|
|
34
|
+
refusal: (args, echo = args) => decide(args, (turn, confirmationId) => turn.check(toolName, echo, confirmationId)),
|
|
35
|
+
redeem: (args, echo = args) => decide(args, (turn, confirmationId) => turn.redeem(toolName, echo, confirmationId)),
|
|
36
|
+
};
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsm-mit/sultana-agent-tools-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent tool layer for a Sultana salon: framework-agnostic tools (services, promos, schedule) over the Sultana core canister wrapper, plus a read-only set that needs no identity.",
|
|
5
5
|
"homepage": "https://github.com/JSM-Sultana/sultana-agent-tools-package#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@icp-sdk/core": "^6.1.0",
|
|
39
|
-
"@jsm-mit/sultana-core-motoko-package": "^0.
|
|
39
|
+
"@jsm-mit/sultana-core-motoko-package": "^0.17.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@jsm-mit/utils-package": "^0.5.0",
|