@nebutra/support-deflector 0.1.3 → 0.1.5

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @nebutra/support-deflector
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @nebutra/capability-kit@2.0.0
9
+ - @nebutra/errors@2.0.0
10
+ - @nebutra/event-log@2.0.0
11
+ - @nebutra/content-store@0.1.5
12
+ - @nebutra/play-loader@0.1.4
13
+ - @nebutra/generation-context@0.1.4
14
+
3
15
  ## 0.1.3
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@nebutra/support-deflector",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Confidence-gated customer support Play package for founder-owned support automation",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "nebutra": {
9
+ "graph": "labs",
9
10
  "status": "wip",
10
11
  "productionReady": false,
11
12
  "surface": "play-product",
@@ -23,18 +24,22 @@
23
24
  "category": "ai",
24
25
  "summary": "Support Play product that triages tickets into auto-answer, suggest, or escalate decisions"
25
26
  },
26
- "main": "./src/index.ts",
27
- "types": "./src/index.ts",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
28
29
  "exports": {
29
- ".": "./src/index.ts"
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "default": "./dist/index.js"
34
+ }
30
35
  },
31
36
  "dependencies": {
32
- "@nebutra/capability-kit": "0.2.2",
33
- "@nebutra/content-store": "0.1.2",
34
- "@nebutra/errors": "0.1.2",
35
- "@nebutra/generation-context": "0.1.2",
36
- "@nebutra/event-log": "0.1.2",
37
- "@nebutra/play-loader": "0.1.2"
37
+ "@nebutra/capability-kit": "2.0.0",
38
+ "@nebutra/content-store": "0.1.5",
39
+ "@nebutra/errors": "2.0.0",
40
+ "@nebutra/event-log": "2.0.0",
41
+ "@nebutra/generation-context": "0.1.4",
42
+ "@nebutra/play-loader": "0.1.4"
38
43
  },
39
44
  "devDependencies": {
40
45
  "@types/node": "^25.9.1",
@@ -55,6 +60,12 @@
55
60
  "publishConfig": {
56
61
  "access": "public"
57
62
  },
63
+ "files": [
64
+ "dist",
65
+ "README.md",
66
+ "LICENSE",
67
+ "CHANGELOG.md"
68
+ ],
58
69
  "scripts": {
59
70
  "build": "tsup",
60
71
  "test": "vitest run",
@@ -1,24 +0,0 @@
1
- {"at":"2026-05-18T14:42:47.511Z","type":"reindex","tenantId":"tenant_a","files":0}
2
- {"at":"2026-05-18T14:42:47.514Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
3
- {"at":"2026-05-18T14:42:47.522Z","type":"reindex","tenantId":"tenant_a","files":0}
4
- {"at":"2026-05-18T14:43:18.065Z","type":"reindex","tenantId":"tenant_a","files":0}
5
- {"at":"2026-05-18T14:43:18.069Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
6
- {"at":"2026-05-18T14:43:18.077Z","type":"reindex","tenantId":"tenant_a","files":0}
7
- {"at":"2026-05-18T14:46:58.860Z","type":"reindex","tenantId":"tenant_a","files":0}
8
- {"at":"2026-05-18T14:46:58.868Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
9
- {"at":"2026-05-18T14:46:58.885Z","type":"reindex","tenantId":"tenant_a","files":0}
10
- {"at":"2026-05-18T14:49:15.261Z","type":"reindex","tenantId":"tenant_a","files":0}
11
- {"at":"2026-05-18T14:49:15.269Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
12
- {"at":"2026-05-18T14:49:15.284Z","type":"reindex","tenantId":"tenant_a","files":0}
13
- {"at":"2026-05-18T14:52:55.141Z","type":"reindex","tenantId":"tenant_a","files":0}
14
- {"at":"2026-05-18T14:52:55.144Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
15
- {"at":"2026-05-18T14:52:55.151Z","type":"reindex","tenantId":"tenant_a","files":0}
16
- {"at":"2026-05-19T05:25:19.177Z","type":"reindex","tenantId":"tenant_a","files":0}
17
- {"at":"2026-05-19T05:25:19.181Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
18
- {"at":"2026-05-19T05:25:19.187Z","type":"reindex","tenantId":"tenant_a","files":0}
19
- {"at":"2026-05-30T21:07:17.661Z","type":"reindex","tenantId":"tenant_a","files":0}
20
- {"at":"2026-05-30T21:07:17.669Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
21
- {"at":"2026-05-30T21:07:17.689Z","type":"reindex","tenantId":"tenant_a","files":0}
22
- {"at":"2026-07-01T01:40:46.757Z","type":"reindex","tenantId":"tenant_a","files":0}
23
- {"at":"2026-07-01T01:40:46.791Z","type":"write","tenantId":"tenant_a","path":"support/tickets/ticket_1.json"}
24
- {"at":"2026-07-01T01:40:46.902Z","type":"reindex","tenantId":"tenant_a","files":0}
@@ -1,8 +0,0 @@
1
- {"traceId":"support_ticket_ticket_1_mpbbcrbu","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"0159324ba9e04ef756841e6cdbfabd1dac01c7c671399606a429d5b7763ec7a3","tenantId":"tenant_a","at":"2026-05-18T14:42:47.515Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
2
- {"traceId":"support_ticket_ticket_1_mpbbdewl","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"6cc0b872a13bf1d3a629c79cd34fc6065ee246820ff8ad9b9ed750437d0ebbd0","tenantId":"tenant_a","at":"2026-05-18T14:43:18.070Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
3
- {"traceId":"support_ticket_ticket_1_mpbbi59w","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"260d0399f35022be282b386901cc1a9234f644ea5b08cf41d72b23265321a159","tenantId":"tenant_a","at":"2026-05-18T14:46:58.871Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
4
- {"traceId":"support_ticket_ticket_1_mpbbl2it","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"6028e0dea1613330851b76045afdee123a40cc4aaf6967f879c2ec6a8d73f7da","tenantId":"tenant_a","at":"2026-05-18T14:49:15.271Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
5
- {"traceId":"support_ticket_ticket_1_mpbbps6g","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"a4538cd355acc38f2bea8c4157e5084282330344282c32260cca963227039de9","tenantId":"tenant_a","at":"2026-05-18T14:52:55.145Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
6
- {"traceId":"support_ticket_ticket_1_mpc6vp3h","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"ac23d8981d7f37cc896019911e6256e082e039d4f42d48184f08c2b3036f0c13","tenantId":"tenant_a","at":"2026-05-19T05:25:19.182Z","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
7
- {"at":"2026-05-30T21:07:17.673Z","traceId":"support_ticket_ticket_1_mpsudgdi","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"a1fb106d15db970f8d27d3c644fd96d17ec6d2c96de6cbd8b5d023f2a3c3be93","tenantId":"tenant_a","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
8
- {"at":"2026-07-01T01:40:46.794Z","traceId":"support_ticket_ticket_1_mr1eskef","kind":"content_write","affected":["support/tickets/ticket_1.json"],"parent":null,"snapshot":{"support/tickets/ticket_1.json":"{\n \"ticket\": {\n \"id\": \"ticket_1\",\n \"tenantId\": \"tenant_a\",\n \"customer\": {\n \"id\": \"customer_1\",\n \"email\": \"a@example.com\",\n \"plan\": \"free\"\n },\n \"subject\": \"How do refunds work?\",\n \"body\": \"Can I get a refund if I cancel this week?\"\n },\n \"decision\": {\n \"play\": \"ticket_triage\",\n \"action\": \"auto-answer\",\n \"confidence\": 0.9,\n \"classification\": {\n \"category\": \"billing\",\n \"sentiment\": \"neutral\",\n \"highValue\": false,\n \"complaint\": false\n },\n \"reply\": {\n \"subject\": \"Re: How do refunds work?\",\n \"body\": \"Refunds are available within 14 days. If you want, reply here and we will help with the next step.\",\n \"citations\": [\n \"kb_refund\"\n ]\n }\n }\n}\n"},"id":"51bb8ca6110ee8ca731e729ea92f1e3f9c15e46a7cec12ed9379b25659f1a216","tenantId":"tenant_a","summary":"content_write touched 1 path(s)","objectHashes":["e79f9ed0f7be6b3229b5eff19a9f5e242fb2575fe56c38dfdda06bf56d882706"]}
@@ -1,8 +0,0 @@
1
- {"at":"2026-05-18T14:42:47.516Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"0159324ba9e04ef756841e6cdbfabd1dac01c7c671399606a429d5b7763ec7a3"}
2
- {"at":"2026-05-18T14:43:18.070Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"6cc0b872a13bf1d3a629c79cd34fc6065ee246820ff8ad9b9ed750437d0ebbd0"}
3
- {"at":"2026-05-18T14:46:58.872Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"260d0399f35022be282b386901cc1a9234f644ea5b08cf41d72b23265321a159"}
4
- {"at":"2026-05-18T14:49:15.274Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"6028e0dea1613330851b76045afdee123a40cc4aaf6967f879c2ec6a8d73f7da"}
5
- {"at":"2026-05-18T14:52:55.146Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"a4538cd355acc38f2bea8c4157e5084282330344282c32260cca963227039de9"}
6
- {"at":"2026-05-19T05:25:19.182Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"ac23d8981d7f37cc896019911e6256e082e039d4f42d48184f08c2b3036f0c13"}
7
- {"at":"2026-05-30T21:07:17.675Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"a1fb106d15db970f8d27d3c644fd96d17ec6d2c96de6cbd8b5d023f2a3c3be93"}
8
- {"at":"2026-07-01T01:40:46.796Z","type":"ticket_decision","tenantId":"tenant_a","ticketId":"ticket_1","action":"auto-answer","eventId":"51bb8ca6110ee8ca731e729ea92f1e3f9c15e46a7cec12ed9379b25659f1a216"}
@@ -1,22 +0,0 @@
1
-
2
- > @nebutra/support-deflector@0.1.3 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/support-deflector
3
- > tsup
4
-
5
- CLI Building entry: src/cli.ts, src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/support-deflector/tsup.config.ts
9
- CLI Target: es2022
10
- CLI Cleaning output folder
11
- ESM Build start
12
- ESM dist/cli.js 1.65 KB
13
- ESM dist/index.js 248.00 B
14
- ESM dist/chunk-EBB2GRYR.js 5.97 KB
15
- ESM dist/index.js.map 71.00 B
16
- ESM dist/chunk-EBB2GRYR.js.map 13.22 KB
17
- ESM dist/cli.js.map 2.80 KB
18
- ESM ⚡️ Build success in 44ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 1559ms
21
- DTS dist/cli.d.ts 13.00 B
22
- DTS dist/index.d.ts 3.08 KB
@@ -1,16 +0,0 @@
1
-
2
- > @nebutra/support-deflector@0.1.2 test /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/support-deflector
3
- > vitest run
4
-
5
-
6
- RUN v4.1.4 /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/support-deflector
7
-
8
- (node:57826) ExperimentalWarning: SQLite is an experimental feature and might change at any time
9
- (Use `node --trace-warnings ...` to show where the warning was created)
10
- ✓ src/index.test.ts (5 tests) 191ms
11
-
12
- Test Files 1 passed (1)
13
- Tests 5 passed (5)
14
- Start at 09:40:45
15
- Duration 1.31s (transform 373ms, setup 0ms, import 518ms, tests 191ms, environment 0ms)
16
-
@@ -1,4 +0,0 @@
1
-
2
- > @nebutra/support-deflector@0.1.3 typecheck /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/support-deflector
3
- > tsc --noEmit
4
-
@@ -1,15 +0,0 @@
1
- import { classifyTicket } from "../src/index";
2
-
3
- process.stdout.write(
4
- `${JSON.stringify(
5
- classifyTicket({
6
- id: "ticket_1",
7
- tenantId: "local",
8
- customer: { id: "customer_1", email: "a@example.com", plan: "enterprise" },
9
- subject: "Production bug",
10
- body: "I am angry because this broke production.",
11
- }),
12
- null,
13
- 2,
14
- )}\n`,
15
- );
@@ -1,25 +0,0 @@
1
- import { decideTicket } from "../src/index";
2
-
3
- process.stdout.write(
4
- `${JSON.stringify(
5
- decideTicket(
6
- {
7
- id: "ticket_1",
8
- tenantId: "local",
9
- customer: { id: "customer_1", email: "a@example.com", plan: "free" },
10
- subject: "Refund",
11
- body: "How do refunds work?",
12
- },
13
- [
14
- {
15
- id: "kb_refund",
16
- title: "Refund policy",
17
- body: "Refunds are available within 14 days.",
18
- },
19
- ],
20
- { autoReplyThreshold: 0.82 },
21
- ),
22
- null,
23
- 2,
24
- )}\n`,
25
- );
@@ -1,33 +0,0 @@
1
- import { SupportDeflector } from "../src/index";
2
-
3
- const support = await SupportDeflector.open(".nebutra/support-deflector-example", {
4
- tenantId: "local",
5
- });
6
-
7
- try {
8
- process.stdout.write(
9
- `${JSON.stringify(
10
- await support.handleTicket({
11
- ticket: {
12
- id: "ticket_demo",
13
- tenantId: "local",
14
- customer: { id: "customer_1", email: "a@example.com", plan: "free" },
15
- subject: "How do refunds work?",
16
- body: "Can I get a refund if I cancel this week?",
17
- },
18
- articles: [
19
- {
20
- id: "kb_refund",
21
- title: "Refund policy",
22
- body: "Refunds are available within 14 days. Contact support with your account email.",
23
- },
24
- ],
25
- policy: { autoReplyThreshold: 0.82 },
26
- }),
27
- null,
28
- 2,
29
- )}\n`,
30
- );
31
- } finally {
32
- await support.close();
33
- }
@@ -1,36 +0,0 @@
1
- ---
2
- name: ticket_triage
3
- kind: play
4
- version: 1.0.0
5
- description: Triage a support ticket into auto-answer, suggest-answer, or escalate
6
- inputs:
7
- ticket: { type: object }
8
- support_policy: { type: object }
9
- outputs:
10
- decision: { type: file, path: support/tickets/ticket.json, mime: application/json }
11
- budget:
12
- duration_s: 30
13
- cost_usd: 0.2
14
- required_skills:
15
- - knowledge_base.search
16
- - content_store.write
17
- - event_log.commit
18
- sub_agents:
19
- - role: support_triager
20
- allowed_skills: [knowledge_base.search, content_store.write]
21
- - role: escalation_summarizer
22
- allowed_skills: [content_store.write]
23
- depends_on_plays: []
24
- ---
25
-
26
- ## What this play does
27
-
28
- Classifies a new customer-support ticket, finds relevant knowledge, then returns
29
- a confidence-gated decision.
30
-
31
- ## Rules
32
-
33
- - Escalate angry, complaint, or high-value customer tickets.
34
- - Auto-answer only when confidence exceeds policy threshold.
35
- - Keep the founder in control of medium-confidence replies.
36
- - Redact personally sensitive data before trace-store emission.
package/src/cli.ts DELETED
@@ -1,50 +0,0 @@
1
- import { readSupportDeflectorDebug, SupportDeflector } from "./index";
2
-
3
- const command = process.argv[2] ?? "doctor";
4
- const root = process.env.SUPPORT_DEFLECTOR_ROOT ?? ".nebutra/support-deflector";
5
- const tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
6
-
7
- if (command === "doctor") {
8
- const support = await SupportDeflector.open(root, { tenantId });
9
- try {
10
- process.stdout.write(`${JSON.stringify(await support.doctor(), null, 2)}\n`);
11
- } finally {
12
- await support.close();
13
- }
14
- } else if (command === "quickstart") {
15
- const support = await SupportDeflector.open(root, { tenantId });
16
- try {
17
- process.stdout.write(
18
- `${JSON.stringify(
19
- await support.handleTicket({
20
- ticket: {
21
- id: "ticket_demo",
22
- tenantId,
23
- customer: { id: "customer_demo", email: "demo@example.com", plan: "free" },
24
- subject: "How do refunds work?",
25
- body: "Can I get a refund if I cancel this week?",
26
- },
27
- articles: [
28
- {
29
- id: "kb_refund",
30
- title: "Refund policy",
31
- body: "Refunds are available within 14 days. Contact support with your account email.",
32
- },
33
- ],
34
- policy: { autoReplyThreshold: 0.82 },
35
- }),
36
- null,
37
- 2,
38
- )}\n`,
39
- );
40
- } finally {
41
- await support.close();
42
- }
43
- } else if (command === "debug") {
44
- process.stdout.write(
45
- `${JSON.stringify({ capability: "support-deflector", entries: await readSupportDeflectorDebug() }, null, 2)}\n`,
46
- );
47
- } else {
48
- process.stderr.write(`Unknown support-deflector command: ${command}\n`);
49
- process.exitCode = 1;
50
- }
package/src/index.test.ts DELETED
@@ -1,106 +0,0 @@
1
- import { mkdtemp, readFile, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { parsePlayMarkdown } from "@nebutra/play-loader";
5
- import { afterEach, describe, expect, it } from "vitest";
6
- import { classifyTicket, decideTicket, readSupportDeflectorDebug, SupportDeflector } from "./index";
7
-
8
- let root: string | undefined;
9
- let support: SupportDeflector | undefined;
10
-
11
- const ticket = {
12
- id: "ticket_1",
13
- tenantId: "tenant_a",
14
- customer: { id: "customer_1", email: "a@example.com", plan: "free" },
15
- subject: "How do refunds work?",
16
- body: "Can I get a refund if I cancel this week?",
17
- } as const;
18
-
19
- const articles = [
20
- {
21
- id: "kb_refund",
22
- title: "Refund policy",
23
- body: "Refunds are available within 14 days. Contact support and include your account email.",
24
- },
25
- ];
26
-
27
- afterEach(async () => {
28
- if (support) await support.close();
29
- if (root) await rm(root, { recursive: true, force: true });
30
- root = undefined;
31
- support = undefined;
32
- });
33
-
34
- async function open(): Promise<SupportDeflector> {
35
- root = await mkdtemp(join(tmpdir(), "support-deflector-"));
36
- support = await SupportDeflector.open(root, { tenantId: "tenant_a" });
37
- return support;
38
- }
39
-
40
- describe("support-deflector", () => {
41
- it("classifies customer tickets conservatively", () => {
42
- expect(classifyTicket(ticket)).toMatchObject({ category: "billing", sentiment: "neutral" });
43
- expect(
44
- classifyTicket({
45
- ...ticket,
46
- body: "I am angry and this bug broke production for our enterprise team",
47
- }),
48
- ).toMatchObject({ category: "bug", sentiment: "angry", highValue: true });
49
- });
50
-
51
- it("uses confidence gates to decide auto-answer, suggest, or escalate", () => {
52
- const auto = decideTicket(ticket, articles, { autoReplyThreshold: 0.82 });
53
- const escalate = decideTicket(
54
- {
55
- ...ticket,
56
- body: "I am angry and this broke production",
57
- customer: { ...ticket.customer, plan: "enterprise" },
58
- },
59
- articles,
60
- { autoReplyThreshold: 0.82 },
61
- );
62
-
63
- expect(auto.action).toBe("auto-answer");
64
- expect(auto.reply?.body).toContain("Refunds are available within 14 days");
65
- expect(escalate.action).toBe("escalate");
66
- });
67
-
68
- it("runs ticket_triage and records the decision", async () => {
69
- const runtime = await open();
70
-
71
- const decision = await runtime.handleTicket({
72
- ticket,
73
- articles,
74
- policy: { autoReplyThreshold: 0.82 },
75
- });
76
-
77
- expect(decision.play).toBe("ticket_triage");
78
- expect(decision.action).toBe("auto-answer");
79
- expect(decision.eventId).toEqual(expect.any(String));
80
- await expect(readSupportDeflectorDebug(root)).resolves.toEqual(expect.any(Array));
81
- });
82
-
83
- it("requires tenant context before persistent operations", async () => {
84
- const runtime = await open();
85
-
86
- await expect(
87
- runtime.handleTicket({
88
- ticket: { ...ticket, tenantId: "" },
89
- articles,
90
- policy: { autoReplyThreshold: 0.82 },
91
- }),
92
- ).rejects.toMatchObject({
93
- capability: "support-deflector",
94
- suggestion: expect.stringContaining("tenantId"),
95
- });
96
- });
97
-
98
- it("keeps ticket triage as SKILL.md instead of a new workflow format", async () => {
99
- const skill = await readFile(join(process.cwd(), "plays", "ticket_triage", "SKILL.md"), "utf8");
100
- const play = parsePlayMarkdown(skill);
101
-
102
- expect(play.meta).toMatchObject({ name: "ticket_triage", kind: "play" });
103
- expect(play.requiredSkills).toContain("knowledge_base.search");
104
- expect(play.subAgents.map((agent) => agent.role)).toContain("support_triager");
105
- });
106
- });
package/src/index.ts DELETED
@@ -1,271 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { requireCapabilityTenant } from "@nebutra/capability-kit";
4
- import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
5
- import { ContentStore } from "@nebutra/content-store";
6
- import { CapabilityError } from "@nebutra/errors";
7
- import { EventLog } from "@nebutra/event-log";
8
- import { assetId } from "@nebutra/generation-context";
9
-
10
- export type TicketCategory = "billing" | "bug" | "how_to" | "sales" | "other";
11
- export type TicketSentiment = "neutral" | "angry" | "positive";
12
- export type SupportAction = "auto-answer" | "suggest-answer" | "escalate";
13
-
14
- export interface SupportTicket {
15
- readonly id: string;
16
- readonly tenantId?: string;
17
- readonly customer: {
18
- readonly id: string;
19
- readonly email: string;
20
- readonly plan?: string;
21
- };
22
- readonly subject: string;
23
- readonly body: string;
24
- }
25
-
26
- export interface KnowledgeArticle {
27
- readonly id: string;
28
- readonly title: string;
29
- readonly body: string;
30
- }
31
-
32
- export interface TicketClassification {
33
- readonly category: TicketCategory;
34
- readonly sentiment: TicketSentiment;
35
- readonly highValue: boolean;
36
- readonly complaint: boolean;
37
- }
38
-
39
- export interface SupportPolicy {
40
- readonly autoReplyThreshold: number;
41
- readonly escalateOnComplaint?: boolean;
42
- readonly escalateOnHighValueCustomer?: boolean;
43
- }
44
-
45
- export interface SupportReply {
46
- readonly subject: string;
47
- readonly body: string;
48
- readonly citations: readonly string[];
49
- }
50
-
51
- export interface SupportDecision {
52
- readonly play: "ticket_triage";
53
- readonly action: SupportAction;
54
- readonly confidence: number;
55
- readonly classification: TicketClassification;
56
- readonly reply?: SupportReply;
57
- readonly escalationSummary?: string;
58
- readonly eventId?: string;
59
- }
60
-
61
- export interface HandleTicketInput {
62
- readonly ticket: SupportTicket;
63
- readonly articles: readonly KnowledgeArticle[];
64
- readonly policy: SupportPolicy;
65
- }
66
-
67
- export interface SupportDoctorReport {
68
- readonly capability: "support-deflector";
69
- readonly ok: boolean;
70
- readonly checkedAt: string;
71
- readonly plays: readonly string[];
72
- readonly mode: "confidence-gated";
73
- readonly channels: readonly { readonly provider: string; readonly ok: boolean }[];
74
- }
75
-
76
- export interface SupportDeflectorOptions {
77
- readonly tenantId?: string;
78
- readonly root?: string;
79
- readonly debugRoot?: string;
80
- readonly contentStore?: ContentStore;
81
- readonly eventLog?: EventLog;
82
- }
83
-
84
- function includesAny(text: string, terms: readonly string[]): boolean {
85
- return terms.some((term) => text.includes(term));
86
- }
87
-
88
- export function classifyTicket(ticket: SupportTicket): TicketClassification {
89
- const text = `${ticket.subject} ${ticket.body}`.toLowerCase();
90
- const category: TicketCategory = includesAny(text, ["bug", "broke", "error", "production"])
91
- ? "bug"
92
- : includesAny(text, ["refund", "invoice", "billing", "cancel"])
93
- ? "billing"
94
- : includesAny(text, ["how", "setup", "configure"])
95
- ? "how_to"
96
- : includesAny(text, ["pricing", "demo", "sales"])
97
- ? "sales"
98
- : "other";
99
- const angry = includesAny(text, ["angry", "furious", "broken", "terrible", "lawsuit"]);
100
- const positive = includesAny(text, ["thanks", "great", "love"]);
101
- const highValue = ticket.customer.plan === "enterprise" || includesAny(text, ["enterprise"]);
102
- return {
103
- category,
104
- sentiment: angry ? "angry" : positive ? "positive" : "neutral",
105
- highValue,
106
- complaint: angry || includesAny(text, ["complaint", "refund now", "unacceptable"]),
107
- };
108
- }
109
-
110
- function matchArticle(
111
- ticket: SupportTicket,
112
- articles: readonly KnowledgeArticle[],
113
- ): KnowledgeArticle | undefined {
114
- const text = `${ticket.subject} ${ticket.body}`.toLowerCase();
115
- return (
116
- articles.find((article) =>
117
- article.title
118
- .toLowerCase()
119
- .split(/\s+/)
120
- .filter((term) => term.length > 3)
121
- .some((term) => text.includes(term)),
122
- ) ??
123
- articles.find((article) =>
124
- article.body
125
- .toLowerCase()
126
- .split(/\W+/)
127
- .filter((term) => term.length > 6)
128
- .some((term) => text.includes(term)),
129
- )
130
- );
131
- }
132
-
133
- function synthesizeReply(ticket: SupportTicket, article: KnowledgeArticle): SupportReply {
134
- const firstSentence = article.body.split(".")[0]?.trim() ?? article.body;
135
- return {
136
- subject: `Re: ${ticket.subject}`,
137
- body: `${firstSentence}. If you want, reply here and we will help with the next step.`,
138
- citations: [article.id],
139
- };
140
- }
141
-
142
- export function decideTicket(
143
- ticket: SupportTicket,
144
- articles: readonly KnowledgeArticle[],
145
- policy: SupportPolicy,
146
- ): SupportDecision {
147
- const classification = classifyTicket(ticket);
148
- const article = matchArticle(ticket, articles);
149
- const confidence = article ? (classification.category === "other" ? 0.72 : 0.9) : 0.35;
150
- const shouldEscalate =
151
- (policy.escalateOnComplaint ?? true) && classification.complaint
152
- ? true
153
- : (policy.escalateOnHighValueCustomer ?? true) && classification.highValue;
154
- if (shouldEscalate) {
155
- return {
156
- play: "ticket_triage",
157
- action: "escalate",
158
- confidence,
159
- classification,
160
- ...(article ? { reply: synthesizeReply(ticket, article) } : {}),
161
- escalationSummary: `${ticket.customer.email} needs founder attention for ${classification.category}.`,
162
- };
163
- }
164
- if (article && confidence >= policy.autoReplyThreshold) {
165
- return {
166
- play: "ticket_triage",
167
- action: "auto-answer",
168
- confidence,
169
- classification,
170
- reply: synthesizeReply(ticket, article),
171
- };
172
- }
173
- return {
174
- play: "ticket_triage",
175
- action: "suggest-answer",
176
- confidence,
177
- classification,
178
- ...(article ? { reply: synthesizeReply(ticket, article) } : {}),
179
- };
180
- }
181
-
182
- export class SupportDeflector {
183
- readonly #tenantId: string | undefined;
184
- readonly #debugRoot: string;
185
- readonly #contentStore: ContentStore;
186
- readonly #eventLog: EventLog;
187
-
188
- private constructor(
189
- options: SupportDeflectorOptions & { contentStore: ContentStore; eventLog: EventLog },
190
- ) {
191
- this.#tenantId = options.tenantId;
192
- this.#debugRoot = options.debugRoot ?? process.cwd();
193
- this.#contentStore = options.contentStore;
194
- this.#eventLog = options.eventLog;
195
- }
196
-
197
- static async open(
198
- root = ".nebutra/support-deflector",
199
- options: Omit<SupportDeflectorOptions, "root" | "contentStore" | "eventLog"> = {},
200
- ): Promise<SupportDeflector> {
201
- const tenantId = options.tenantId ?? "local";
202
- await mkdir(root, { recursive: true });
203
- const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
204
- const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
205
- return new SupportDeflector({ ...options, tenantId, root, contentStore, eventLog });
206
- }
207
-
208
- async handleTicket(input: HandleTicketInput): Promise<SupportDecision> {
209
- const tenantId = requireCapabilityTenant({
210
- explicit: input.ticket.tenantId,
211
- fallback: this.#tenantId,
212
- onMissing: () =>
213
- new CapabilityError("support-deflector", "Support Deflector requires tenant context", {
214
- suggestion: "Pass tenantId with the ticket or construct SupportDeflector with tenantId.",
215
- statusCode: 400,
216
- }),
217
- });
218
- const decision = decideTicket(input.ticket, input.articles, input.policy);
219
- const artifactPath = `support/tickets/${input.ticket.id}.json`;
220
- const content = `${JSON.stringify({ ticket: input.ticket, decision }, null, 2)}\n`;
221
- await this.#contentStore.write(artifactPath, content);
222
- const eventId = await this.#eventLog.commit({
223
- traceId: assetId("support_ticket", input.ticket.id),
224
- kind: "content_write",
225
- affected: [artifactPath],
226
- parent: null,
227
- snapshot: { [artifactPath]: content },
228
- });
229
- await this.#debug({
230
- type: "ticket_decision",
231
- tenantId,
232
- ticketId: input.ticket.id,
233
- action: decision.action,
234
- eventId,
235
- });
236
- return { ...decision, eventId };
237
- }
238
-
239
- async doctor(): Promise<SupportDoctorReport> {
240
- return {
241
- capability: "support-deflector",
242
- ok: true,
243
- checkedAt: new Date().toISOString(),
244
- plays: ["ticket_triage"],
245
- mode: "confidence-gated",
246
- channels: [
247
- { provider: "local-ticket", ok: true },
248
- { provider: "chatwoot-bridge", ok: false },
249
- { provider: "email-bridge", ok: false },
250
- ],
251
- };
252
- }
253
-
254
- async close(): Promise<void> {
255
- await this.#contentStore.close();
256
- }
257
-
258
- async #debug(entry: Record<string, unknown>): Promise<void> {
259
- await mkdir(dirname(join(this.#debugRoot, ".nebutra", "debug", "support-deflector.jsonl")), {
260
- recursive: true,
261
- });
262
- await appendCapabilityDebug("support-deflector", entry, { root: this.#debugRoot });
263
- }
264
- }
265
-
266
- export async function readSupportDeflectorDebug(
267
- root = process.cwd(),
268
- limit = 20,
269
- ): Promise<unknown[]> {
270
- return readCapabilityDebug("support-deflector", { root, limit });
271
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": ".",
6
- "types": ["node", "vitest"]
7
- },
8
- "include": ["src/**/*.ts", "examples/**/*.ts"]
9
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts", "src/cli.ts"],
5
- format: ["esm"],
6
- sourcemap: true,
7
- dts: true,
8
- clean: true,
9
- target: "es2022",
10
- external: [/^@nebutra\//],
11
- });