@flytedesk/app-kit 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +6 -3
  2. package/dist/bigquery/client.d.ts +18 -0
  3. package/dist/bigquery/client.js +37 -0
  4. package/dist/bigquery/client.js.map +1 -0
  5. package/dist/bigquery/errors.d.ts +26 -0
  6. package/dist/bigquery/errors.js +80 -0
  7. package/dist/bigquery/errors.js.map +1 -0
  8. package/dist/bigquery/extract.d.ts +15 -0
  9. package/dist/bigquery/extract.js +88 -0
  10. package/dist/bigquery/extract.js.map +1 -0
  11. package/dist/bigquery/index.d.ts +47 -0
  12. package/dist/bigquery/index.js +46 -0
  13. package/dist/bigquery/index.js.map +1 -0
  14. package/dist/bigquery/labels.d.ts +16 -0
  15. package/dist/bigquery/labels.js +26 -0
  16. package/dist/bigquery/labels.js.map +1 -0
  17. package/dist/bigquery/query.d.ts +16 -0
  18. package/dist/bigquery/query.js +86 -0
  19. package/dist/bigquery/query.js.map +1 -0
  20. package/dist/bigquery/types.d.ts +155 -0
  21. package/dist/bigquery/types.js +15 -0
  22. package/dist/bigquery/types.js.map +1 -0
  23. package/dist/chat/callback-secret.d.ts +21 -0
  24. package/dist/chat/callback-secret.js +33 -0
  25. package/dist/chat/callback-secret.js.map +1 -0
  26. package/dist/chat/env.d.ts +27 -0
  27. package/dist/chat/env.js +44 -0
  28. package/dist/chat/env.js.map +1 -0
  29. package/dist/chat/index.d.ts +72 -0
  30. package/dist/chat/index.js +70 -0
  31. package/dist/chat/index.js.map +1 -0
  32. package/dist/chat/launcher.d.ts +43 -0
  33. package/dist/chat/launcher.js +103 -0
  34. package/dist/chat/launcher.js.map +1 -0
  35. package/dist/chat/mcp-protocol.d.ts +96 -0
  36. package/dist/chat/mcp-protocol.js +200 -0
  37. package/dist/chat/mcp-protocol.js.map +1 -0
  38. package/dist/chat/mcp-server.d.ts +17 -0
  39. package/dist/chat/mcp-server.js +71 -0
  40. package/dist/chat/mcp-server.js.map +1 -0
  41. package/dist/chat/types.d.ts +91 -0
  42. package/dist/chat/types.js +5 -0
  43. package/dist/chat/types.js.map +1 -0
  44. package/dist/cli/flags-sync.js +2 -5
  45. package/dist/cli/flags-sync.js.map +1 -1
  46. package/dist/cli/profile-sync.js +2 -5
  47. package/dist/cli/profile-sync.js.map +1 -1
  48. package/dist/cli/sync-engine.d.ts +32 -0
  49. package/dist/cli/sync-engine.js +190 -23
  50. package/dist/cli/sync-engine.js.map +1 -1
  51. package/dist/cli/trace-sync.js +2 -5
  52. package/dist/cli/trace-sync.js.map +1 -1
  53. package/package.json +10 -1
  54. package/scripts/pending-release-count.mjs +37 -0
  55. package/scripts/release.sh +126 -0
  56. package/scripts/release.test.ts +205 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Structural (duck-typed) subset of `@google-cloud/bigquery`'s own BigQuery /
3
+ * Job / Dataset / Table classes that this module actually calls — mirrors
4
+ * src/trace/types.ts's TracePrismaClient boundary: deliberately NOT a
5
+ * dependency on `@google-cloud/bigquery` (this package has no hard dependency
6
+ * on any GCP SDK), so a real `new BigQuery()` instance satisfies BigQueryLike
7
+ * directly (TypeScript structural typing — its Job/Dataset/Table classes carry
8
+ * every member below), and a test fake can implement just this surface without
9
+ * mocking the SDK's own internals. AK-11: extracted from media-planner's
10
+ * `apps/api/src/services/inventoryWarehouse.ts`, generalizing the
11
+ * `@google-cloud/bigquery` calls it made; inventory-specific SQL stays behind
12
+ * in media-planner.
13
+ */
14
+ /** One structured error entry, same shape the Google APIs client library
15
+ * attaches to a rejected job promise's `.errors` array. */
16
+ export interface BigQueryApiErrorDetail {
17
+ reason?: string;
18
+ message?: string;
19
+ location?: string;
20
+ }
21
+ export interface BigQueryJobStatus {
22
+ state?: string;
23
+ errorResult?: BigQueryApiErrorDetail | null;
24
+ errors?: BigQueryApiErrorDetail[];
25
+ }
26
+ export interface BigQueryJobStatistics {
27
+ query?: {
28
+ totalBytesProcessed?: string | number;
29
+ totalBytesBilled?: string | number;
30
+ cacheHit?: boolean;
31
+ };
32
+ }
33
+ export interface BigQueryJobMetadata {
34
+ id?: string;
35
+ status?: BigQueryJobStatus;
36
+ statistics?: BigQueryJobStatistics;
37
+ configuration?: {
38
+ labels?: Record<string, string>;
39
+ dryRun?: boolean;
40
+ };
41
+ }
42
+ export interface BigQueryQueryResultsOptions {
43
+ maxResults?: number;
44
+ pageToken?: string;
45
+ }
46
+ /** The next-page cursor `Job#getQueryResults` hands back as its second tuple
47
+ * element — `null` once there are no more pages. */
48
+ export type BigQueryNextQuery = {
49
+ pageToken?: string;
50
+ } | null;
51
+ export interface BigQueryJobLike {
52
+ id: string;
53
+ getMetadata(): Promise<[BigQueryJobMetadata, unknown]>;
54
+ getQueryResults<T = Record<string, unknown>>(options?: BigQueryQueryResultsOptions): Promise<[T[], BigQueryNextQuery, unknown]>;
55
+ }
56
+ export interface BigQueryCreateQueryJobOptions {
57
+ query: string;
58
+ /** Positional (`?`) or named (`@name`) query parameters. */
59
+ params?: unknown[] | Record<string, unknown>;
60
+ /** Only needed when the SDK can't infer a param's BigQuery type. */
61
+ types?: unknown[] | Record<string, string>;
62
+ labels?: Record<string, string>;
63
+ dryRun?: boolean;
64
+ location?: string;
65
+ maxResults?: number;
66
+ jobId?: string;
67
+ }
68
+ export interface BigQueryExtractOptions {
69
+ format?: "CSV" | "NEWLINE_DELIMITED_JSON" | "AVRO" | "PARQUET";
70
+ gzip?: boolean;
71
+ labels?: Record<string, string>;
72
+ }
73
+ /** Structural subset of `@google-cloud/storage`'s File — only what a
74
+ * destination for `Table#createExtractJob` needs to carry. */
75
+ export interface GcsFileLike {
76
+ readonly name: string;
77
+ readonly bucket: {
78
+ readonly name: string;
79
+ };
80
+ }
81
+ export interface BigQueryTableLike {
82
+ createExtractJob(destination: GcsFileLike, options?: BigQueryExtractOptions): Promise<[BigQueryJobLike, BigQueryJobMetadata]>;
83
+ }
84
+ export interface BigQueryDatasetLike {
85
+ table(id: string): BigQueryTableLike;
86
+ }
87
+ export interface BigQueryLike {
88
+ createQueryJob(options: BigQueryCreateQueryJobOptions): Promise<[BigQueryJobLike, BigQueryJobMetadata]>;
89
+ dataset(id: string): BigQueryDatasetLike;
90
+ /** Looks up an already-submitted job by id — used to poll extract-job status
91
+ * independently of the handle returned at submission time. */
92
+ job(id: string): BigQueryJobLike;
93
+ }
94
+ export interface RunQueryOptions {
95
+ /** The parameterised SQL. Always use `params`/`types` for user-supplied
96
+ * values — never string-interpolate them into `sql`. */
97
+ sql: string;
98
+ params?: unknown[] | Record<string, unknown>;
99
+ types?: unknown[] | Record<string, string>;
100
+ labels?: Record<string, string>;
101
+ location?: string;
102
+ /** Page size. Omit to let BigQuery pick a default. */
103
+ pageSize?: number;
104
+ /** Resume a prior page — from `QueryPage.nextPageToken`. */
105
+ pageToken?: string;
106
+ }
107
+ export interface QueryPage<T = Record<string, unknown>> {
108
+ rows: T[];
109
+ jobId: string;
110
+ /** Present when another page is available. */
111
+ nextPageToken?: string;
112
+ }
113
+ export interface EstimateQueryBytesOptions {
114
+ sql: string;
115
+ params?: unknown[] | Record<string, unknown>;
116
+ types?: unknown[] | Record<string, string>;
117
+ location?: string;
118
+ /** When given, `estimateQueryBytes` throws `DryRunBudgetExceededError`
119
+ * instead of returning once the estimate exceeds this many bytes. */
120
+ maxBytesBilled?: number;
121
+ }
122
+ export interface DryRunEstimate {
123
+ totalBytesProcessed: number;
124
+ cacheHit: boolean;
125
+ }
126
+ export interface ExtractTableToGCSInput {
127
+ datasetId: string;
128
+ tableId: string;
129
+ destination: GcsFileLike;
130
+ format?: BigQueryExtractOptions["format"];
131
+ gzip?: boolean;
132
+ labels?: Record<string, string>;
133
+ }
134
+ export interface ExtractJobHandle {
135
+ jobId: string;
136
+ }
137
+ export type ExtractJobState = "PENDING" | "RUNNING" | "DONE" | "UNKNOWN";
138
+ export interface ExtractJobStatus {
139
+ jobId: string;
140
+ state: ExtractJobState;
141
+ done: boolean;
142
+ errors?: BigQueryApiErrorDetail[];
143
+ }
144
+ export interface WaitForExtractJobOptions {
145
+ pollIntervalMs?: number;
146
+ timeoutMs?: number;
147
+ }
148
+ /** Who ran the query/extract, and for what — merged into every job's
149
+ * `labels` so usage can be attributed after the fact in BigQuery's own job
150
+ * history / billing export. */
151
+ export interface JobLabelInput {
152
+ actor: string;
153
+ purpose: string;
154
+ extra?: Record<string, string>;
155
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Structural (duck-typed) subset of `@google-cloud/bigquery`'s own BigQuery /
3
+ * Job / Dataset / Table classes that this module actually calls — mirrors
4
+ * src/trace/types.ts's TracePrismaClient boundary: deliberately NOT a
5
+ * dependency on `@google-cloud/bigquery` (this package has no hard dependency
6
+ * on any GCP SDK), so a real `new BigQuery()` instance satisfies BigQueryLike
7
+ * directly (TypeScript structural typing — its Job/Dataset/Table classes carry
8
+ * every member below), and a test fake can implement just this surface without
9
+ * mocking the SDK's own internals. AK-11: extracted from media-planner's
10
+ * `apps/api/src/services/inventoryWarehouse.ts`, generalizing the
11
+ * `@google-cloud/bigquery` calls it made; inventory-specific SQL stays behind
12
+ * in media-planner.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/bigquery/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * One constant-time shared-secret bearer guard for the dispatched agent's callback
3
+ * leg — extracted from media-planner's `apps/api/src/lib/callbackSecret.ts` (there,
4
+ * shared between the danxbot chat callback and an unrelated internal-task route). No
5
+ * behavior changed in the port: same compare, same preHandler contract.
6
+ */
7
+ import type { FastifyReply, FastifyRequest } from "fastify";
8
+ /**
9
+ * Constant-time compare of a presented secret against the expected one.
10
+ * `timingSafeEqual` throws on a length mismatch, so lengths are compared first —
11
+ * the one thing this deliberately leaks, which is fine: the length of a secret is
12
+ * not the secret. An empty expected secret can never match a non-empty (or equally
13
+ * empty) presented one, so an unconfigured environment fails closed.
14
+ */
15
+ export declare function callbackSecretMatches(expected: string, presented: string): boolean;
16
+ /**
17
+ * preHandler factory: requires `Authorization: Bearer <expectedSecret>`. These
18
+ * routes carry no user session — the caller is a service (the dispatched agent's MCP
19
+ * server) — so this shared secret is the only thing guarding them.
20
+ */
21
+ export declare function requireCallbackSecret(expectedSecret: string): (request: FastifyRequest, reply: FastifyReply, done: () => void) => void;
@@ -0,0 +1,33 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ const BEARER_PREFIX = "Bearer ";
3
+ /**
4
+ * Constant-time compare of a presented secret against the expected one.
5
+ * `timingSafeEqual` throws on a length mismatch, so lengths are compared first —
6
+ * the one thing this deliberately leaks, which is fine: the length of a secret is
7
+ * not the secret. An empty expected secret can never match a non-empty (or equally
8
+ * empty) presented one, so an unconfigured environment fails closed.
9
+ */
10
+ export function callbackSecretMatches(expected, presented) {
11
+ const e = Buffer.from(expected, "utf8");
12
+ const a = Buffer.from(presented, "utf8");
13
+ if (e.length === 0 || e.length !== a.length)
14
+ return false;
15
+ return timingSafeEqual(e, a);
16
+ }
17
+ /**
18
+ * preHandler factory: requires `Authorization: Bearer <expectedSecret>`. These
19
+ * routes carry no user session — the caller is a service (the dispatched agent's MCP
20
+ * server) — so this shared secret is the only thing guarding them.
21
+ */
22
+ export function requireCallbackSecret(expectedSecret) {
23
+ return function callbackSecretPreHandler(request, reply, done) {
24
+ const header = request.headers.authorization;
25
+ if (!header?.startsWith(BEARER_PREFIX) ||
26
+ !callbackSecretMatches(expectedSecret, header.slice(BEARER_PREFIX.length))) {
27
+ reply.code(401).send({ error: "Invalid callback credentials", code: "unauthenticated" });
28
+ return;
29
+ }
30
+ done();
31
+ };
32
+ }
33
+ //# sourceMappingURL=callback-secret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback-secret.js","sourceRoot":"","sources":["../../src/chat/callback-secret.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,aAAa,GAAG,SAAS,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,SAAiB;IACvE,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,cAAsB;IAEtB,OAAO,SAAS,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI;QAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7C,IACE,CAAC,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC;YAClC,CAAC,qBAAqB,CAAC,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,EAC1E,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;YACzF,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The env contract every app wiring @flytedesk/app-kit/chat shares — extracted from
3
+ * media-planner's `apps/api/src/env.ts` (the `DANXBOT_*` slice of it, lines 35-66).
4
+ * Merge these keys into your app's own env schema/parsing; this module only owns
5
+ * their names and defaults, not how the rest of your app reads env.
6
+ */
7
+ import type { ChatEnv } from "./types.js";
8
+ export declare class ChatEnvError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /**
12
+ * Parse the `DANXBOT_*` chat env vars out of a process-env-shaped source.
13
+ *
14
+ * `DANXBOT_API_TOKEN` and `DANXBOT_CALLBACK_SECRET` default to `""` ON PURPOSE — an
15
+ * empty value is a valid state (danxbot isn't configured for this environment yet,
16
+ * e.g. production before rollout), and every call site (the launcher's DanxbotError
17
+ * path, `requireCallbackSecret`'s fail-closed compare) already turns a rejected or
18
+ * failed request into a normal error rather than crashing. Requiring either at boot
19
+ * would crash the entire consuming app in any environment without a real danxbot
20
+ * deployment, not just degrade the chat feature.
21
+ *
22
+ * `DANXBOT_BOARD_ID` and `DANXBOT_CALLBACK_API_URL` have no universal default —
23
+ * each is inherently per-app (which board a chat turn dispatches onto; the URL the
24
+ * dispatched agent's MCP server can reach this app's callback routes through, e.g.
25
+ * `host.docker.internal:<port>` for a local worker) — so both are required.
26
+ */
27
+ export declare function parseChatEnv(source: Record<string, string | undefined>): ChatEnv;
@@ -0,0 +1,44 @@
1
+ export class ChatEnvError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ChatEnvError";
5
+ }
6
+ }
7
+ /**
8
+ * Parse the `DANXBOT_*` chat env vars out of a process-env-shaped source.
9
+ *
10
+ * `DANXBOT_API_TOKEN` and `DANXBOT_CALLBACK_SECRET` default to `""` ON PURPOSE — an
11
+ * empty value is a valid state (danxbot isn't configured for this environment yet,
12
+ * e.g. production before rollout), and every call site (the launcher's DanxbotError
13
+ * path, `requireCallbackSecret`'s fail-closed compare) already turns a rejected or
14
+ * failed request into a normal error rather than crashing. Requiring either at boot
15
+ * would crash the entire consuming app in any environment without a real danxbot
16
+ * deployment, not just degrade the chat feature.
17
+ *
18
+ * `DANXBOT_BOARD_ID` and `DANXBOT_CALLBACK_API_URL` have no universal default —
19
+ * each is inherently per-app (which board a chat turn dispatches onto; the URL the
20
+ * dispatched agent's MCP server can reach this app's callback routes through, e.g.
21
+ * `host.docker.internal:<port>` for a local worker) — so both are required.
22
+ */
23
+ export function parseChatEnv(source) {
24
+ const boardId = source.DANXBOT_BOARD_ID;
25
+ if (!boardId) {
26
+ throw new ChatEnvError("DANXBOT_BOARD_ID is required (the danxbot board this app dispatches chat turns onto)");
27
+ }
28
+ const callbackApiUrl = source.DANXBOT_CALLBACK_API_URL;
29
+ if (!callbackApiUrl) {
30
+ throw new ChatEnvError("DANXBOT_CALLBACK_API_URL is required (the URL the dispatched agent's MCP server reaches this app through)");
31
+ }
32
+ return {
33
+ // Deliberately `||`, not `??`, unlike the token/secret below: an empty string is
34
+ // never a valid URL (unlike an empty token/secret, which is a real "not
35
+ // configured yet" sentinel — see their own comments), so an explicitly empty
36
+ // DANXBOT_API_URL should fall back to the default exactly like an unset one.
37
+ danxbotApiUrl: source.DANXBOT_API_URL || "http://localhost:5555",
38
+ danxbotBoardId: boardId,
39
+ danxbotApiToken: source.DANXBOT_API_TOKEN ?? "",
40
+ danxbotCallbackApiUrl: callbackApiUrl,
41
+ danxbotCallbackSecret: source.DANXBOT_CALLBACK_SECRET ?? "",
42
+ };
43
+ }
44
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/chat/env.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0C;IACrE,MAAM,OAAO,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,YAAY,CAAC,sFAAsF,CAAC,CAAC;IACjH,CAAC;IACD,MAAM,cAAc,GAAG,MAAM,CAAC,wBAAwB,CAAC;IACvD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,YAAY,CACpB,2GAA2G,CAC5G,CAAC;IACJ,CAAC;IACD,OAAO;QACL,iFAAiF;QACjF,wEAAwE;QACxE,6EAA6E;QAC7E,6EAA6E;QAC7E,aAAa,EAAE,MAAM,CAAC,eAAe,IAAI,uBAAuB;QAChE,cAAc,EAAE,OAAO;QACvB,eAAe,EAAE,MAAM,CAAC,iBAAiB,IAAI,EAAE;QAC/C,qBAAqB,EAAE,cAAc;QACrC,qBAAqB,EAAE,MAAM,CAAC,uBAAuB,IAAI,EAAE;KAC5D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The danxbot agent-chat bridge — DEC-51 (AK-12). Extracted and generalized from
3
+ * media-planner's in-app assistant (`apps/api/src/lib/danxbot.ts` + `services/
4
+ * chat.ts` + `routes/chat.ts` + `packages/chat-mcp`), so any flytedesk app can give
5
+ * its own chat sidebar a real danxbot agent without re-solving dispatch launch, the
6
+ * reply protocol, or the callback secret from scratch.
7
+ *
8
+ * Four pieces, usable independently or together:
9
+ *
10
+ * - `createDanxbotLauncher` — dispatch (and resume) a danxbot chat turn.
11
+ * - `createChatMcpProtocol` + `createReplyTool` — the MCP server the dispatched
12
+ * agent talks to the user through, with an extension point for app-registered
13
+ * tools (see AppRegisteredTool).
14
+ * - `createChatMcpStdioServer` — the newline-delimited JSON-RPC stdio transport for
15
+ * the above, built on Node builtins only (no runtime dependency) so it can run
16
+ * from an unbuilt clone the way danxbot's dispatched agents expect.
17
+ * - `callbackSecretMatches` / `requireCallbackSecret` — the constant-time guard for
18
+ * the HTTP routes the dispatched agent's MCP server calls back into.
19
+ * - `parseChatEnv` — the `DANXBOT_*` env var contract all of the above share.
20
+ *
21
+ * Usage — the app's own Fastify API process (launch + callback routes):
22
+ *
23
+ * import { createDanxbotLauncher, requireCallbackSecret, parseChatEnv } from "@flytedesk/app-kit/chat";
24
+ *
25
+ * const chatEnv = parseChatEnv(process.env);
26
+ * const launcher = createDanxbotLauncher({
27
+ * apiUrl: chatEnv.danxbotApiUrl,
28
+ * apiToken: chatEnv.danxbotApiToken,
29
+ * board: chatEnv.danxbotBoardId,
30
+ * profile: "board-chat",
31
+ * });
32
+ *
33
+ * const { jobId } = await launcher.dispatchChatTurn({
34
+ * task: prompt,
35
+ * overlay: { TURN_ID: turn.id, API_URL: chatEnv.danxbotCallbackApiUrl, CALLBACK_SECRET: chatEnv.danxbotCallbackSecret },
36
+ * });
37
+ *
38
+ * fastify.post(
39
+ * "/chat/internal/reply",
40
+ * { preHandler: requireCallbackSecret(chatEnv.danxbotCallbackSecret) },
41
+ * async (request) => recordReply(request.body),
42
+ * );
43
+ *
44
+ * Usage — the MCP server script the dispatched agent's clean room actually runs
45
+ * (typically `dist/chat/mcp/entry.js`, wired through a `.danxbot/config/mcp-servers/
46
+ * *.yml`):
47
+ *
48
+ * import {
49
+ * createChatMcpProtocol,
50
+ * createChatMcpStdioServer,
51
+ * createReplyTool,
52
+ * } from "@flytedesk/app-kit/chat";
53
+ *
54
+ * const protocol = createChatMcpProtocol({
55
+ * serverName: "sms-app-chat",
56
+ * tools: [
57
+ * createReplyTool({ description: "...", deliver: postReplyToApi }),
58
+ * // The extension point: register whatever app-specific tools the agent
59
+ * // needs — each is just { name, description, inputSchema, handler }.
60
+ * { name: "audience_validate", description: "...", inputSchema: {...}, handler: validateAudience },
61
+ * ],
62
+ * });
63
+ * createChatMcpStdioServer({ protocol }).start();
64
+ */
65
+ export { createDanxbotLauncher, DanxbotError, isFailedJobStatus, isTerminalJobStatus } from "./launcher.js";
66
+ export { callbackSecretMatches, requireCallbackSecret } from "./callback-secret.js";
67
+ export { createChatMcpProtocol, createReplyTool, parseReplyArguments, FALLBACK_PROTOCOL_VERSION, JSON_RPC, } from "./mcp-protocol.js";
68
+ export { createChatMcpStdioServer } from "./mcp-server.js";
69
+ export { parseChatEnv, ChatEnvError } from "./env.js";
70
+ export type { AppRegisteredTool, ChatEnv, ChatToolCallResult, ChatToolContent, DanxbotDispatchResult, DanxbotJobStatus, DanxbotLauncher, DanxbotLauncherOptions, } from "./types.js";
71
+ export type { ChatMcpProtocol, ChatMcpProtocolOptions, ChatReplyPayload, ParseReplyArgumentsOptions, ReplyDeliverResult, ReplyToolOptions, } from "./mcp-protocol.js";
72
+ export type { ChatMcpStdioServer, ChatMcpStdioServerOptions } from "./mcp-server.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The danxbot agent-chat bridge — DEC-51 (AK-12). Extracted and generalized from
3
+ * media-planner's in-app assistant (`apps/api/src/lib/danxbot.ts` + `services/
4
+ * chat.ts` + `routes/chat.ts` + `packages/chat-mcp`), so any flytedesk app can give
5
+ * its own chat sidebar a real danxbot agent without re-solving dispatch launch, the
6
+ * reply protocol, or the callback secret from scratch.
7
+ *
8
+ * Four pieces, usable independently or together:
9
+ *
10
+ * - `createDanxbotLauncher` — dispatch (and resume) a danxbot chat turn.
11
+ * - `createChatMcpProtocol` + `createReplyTool` — the MCP server the dispatched
12
+ * agent talks to the user through, with an extension point for app-registered
13
+ * tools (see AppRegisteredTool).
14
+ * - `createChatMcpStdioServer` — the newline-delimited JSON-RPC stdio transport for
15
+ * the above, built on Node builtins only (no runtime dependency) so it can run
16
+ * from an unbuilt clone the way danxbot's dispatched agents expect.
17
+ * - `callbackSecretMatches` / `requireCallbackSecret` — the constant-time guard for
18
+ * the HTTP routes the dispatched agent's MCP server calls back into.
19
+ * - `parseChatEnv` — the `DANXBOT_*` env var contract all of the above share.
20
+ *
21
+ * Usage — the app's own Fastify API process (launch + callback routes):
22
+ *
23
+ * import { createDanxbotLauncher, requireCallbackSecret, parseChatEnv } from "@flytedesk/app-kit/chat";
24
+ *
25
+ * const chatEnv = parseChatEnv(process.env);
26
+ * const launcher = createDanxbotLauncher({
27
+ * apiUrl: chatEnv.danxbotApiUrl,
28
+ * apiToken: chatEnv.danxbotApiToken,
29
+ * board: chatEnv.danxbotBoardId,
30
+ * profile: "board-chat",
31
+ * });
32
+ *
33
+ * const { jobId } = await launcher.dispatchChatTurn({
34
+ * task: prompt,
35
+ * overlay: { TURN_ID: turn.id, API_URL: chatEnv.danxbotCallbackApiUrl, CALLBACK_SECRET: chatEnv.danxbotCallbackSecret },
36
+ * });
37
+ *
38
+ * fastify.post(
39
+ * "/chat/internal/reply",
40
+ * { preHandler: requireCallbackSecret(chatEnv.danxbotCallbackSecret) },
41
+ * async (request) => recordReply(request.body),
42
+ * );
43
+ *
44
+ * Usage — the MCP server script the dispatched agent's clean room actually runs
45
+ * (typically `dist/chat/mcp/entry.js`, wired through a `.danxbot/config/mcp-servers/
46
+ * *.yml`):
47
+ *
48
+ * import {
49
+ * createChatMcpProtocol,
50
+ * createChatMcpStdioServer,
51
+ * createReplyTool,
52
+ * } from "@flytedesk/app-kit/chat";
53
+ *
54
+ * const protocol = createChatMcpProtocol({
55
+ * serverName: "sms-app-chat",
56
+ * tools: [
57
+ * createReplyTool({ description: "...", deliver: postReplyToApi }),
58
+ * // The extension point: register whatever app-specific tools the agent
59
+ * // needs — each is just { name, description, inputSchema, handler }.
60
+ * { name: "audience_validate", description: "...", inputSchema: {...}, handler: validateAudience },
61
+ * ],
62
+ * });
63
+ * createChatMcpStdioServer({ protocol }).start();
64
+ */
65
+ export { createDanxbotLauncher, DanxbotError, isFailedJobStatus, isTerminalJobStatus } from "./launcher.js";
66
+ export { callbackSecretMatches, requireCallbackSecret } from "./callback-secret.js";
67
+ export { createChatMcpProtocol, createReplyTool, parseReplyArguments, FALLBACK_PROTOCOL_VERSION, JSON_RPC, } from "./mcp-protocol.js";
68
+ export { createChatMcpStdioServer } from "./mcp-server.js";
69
+ export { parseChatEnv, ChatEnvError } from "./env.js";
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC5G,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A danxbot dispatch client for a per-turn chat bridge — the ONLY place a consuming
3
+ * app should speak danxbot's `/api/launch` / `/api/resume` / `/api/status` contract.
4
+ *
5
+ * Extracted from media-planner's `apps/api/src/lib/danxbot.ts`, generalized so
6
+ * `board`/`profile` are options instead of one app's hardcoded values. The wire
7
+ * contract itself did not change:
8
+ *
9
+ * POST /api/launch {board, profile, task, overlay?}
10
+ * -> 200 {"job_id":"e4d603a3-…","status":"launched"}
11
+ * POST /api/resume {board, profile, task, job_id, overlay?}
12
+ * -> 200 {"job_id":"2aaac2e1-…","parent_job_id":"e4d603a3-…","status":"launched"}
13
+ * GET /api/status/:jobId
14
+ * -> 200 {"job_id":…,"status":"running"|"completed"|…,"summary":…,"elapsed_seconds":…,
15
+ * "input_tokens":…,"output_tokens":…,"cache_read_input_tokens":…,
16
+ * "cache_creation_input_tokens":…}
17
+ *
18
+ * `resume` is what makes a thread a thread: passing the previous turn's `job_id`
19
+ * resumes the underlying Claude session (`claude --resume`), so a later turn
20
+ * remembers an earlier one. `overlay` is danxbot's per-dispatch substitution map:
21
+ * its values are substituted into the dispatched agent's clean-room MCP-server env
22
+ * — see mcp-protocol.ts / mcp-server.ts for the reply side of that channel.
23
+ */
24
+ import type { DanxbotLauncher, DanxbotLauncherOptions } from "./types.js";
25
+ /**
26
+ * danxbot rejected the call. Carries the HTTP status so a caller can tell a
27
+ * misconfiguration (4xx — bad board, unknown profile, revoked token) apart from a
28
+ * worker that is down or wedged (5xx / network), which are different operator
29
+ * problems even though both surface to the user as "the assistant is unavailable".
30
+ */
31
+ export declare class DanxbotError extends Error {
32
+ readonly httpStatus: number | null;
33
+ constructor(message: string, httpStatus: number | null);
34
+ }
35
+ /**
36
+ * Create a launcher bound to one board/profile pair. One instance per consumer app
37
+ * (or per board, for an app dispatching onto more than one) — cheap, holds no
38
+ * mutable state of its own.
39
+ */
40
+ export declare function createDanxbotLauncher(options: DanxbotLauncherOptions): DanxbotLauncher;
41
+ export declare function isTerminalJobStatus(status: string): boolean;
42
+ /** A terminal status that is terminal because something went WRONG. */
43
+ export declare function isFailedJobStatus(status: string): boolean;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * danxbot rejected the call. Carries the HTTP status so a caller can tell a
3
+ * misconfiguration (4xx — bad board, unknown profile, revoked token) apart from a
4
+ * worker that is down or wedged (5xx / network), which are different operator
5
+ * problems even though both surface to the user as "the assistant is unavailable".
6
+ */
7
+ export class DanxbotError extends Error {
8
+ httpStatus;
9
+ constructor(message, httpStatus) {
10
+ super(message);
11
+ this.httpStatus = httpStatus;
12
+ this.name = "DanxbotError";
13
+ }
14
+ }
15
+ async function danxbotFetch(options, path, init) {
16
+ let response;
17
+ try {
18
+ response = await fetch(`${options.apiUrl}${path}`, {
19
+ method: init.method,
20
+ headers: {
21
+ Authorization: `Bearer ${options.apiToken}`,
22
+ ...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
23
+ },
24
+ body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
25
+ });
26
+ }
27
+ catch (cause) {
28
+ // The worker is unreachable. Deliberately NOT swallowed into a placeholder
29
+ // result: a chat that silently never answers is worse than one that says it is
30
+ // down.
31
+ throw new DanxbotError(`danxbot at ${options.apiUrl} is unreachable: ${cause.message}`, null);
32
+ }
33
+ const text = await response.text();
34
+ if (!response.ok) {
35
+ throw new DanxbotError(`danxbot ${init.method} ${path} returned ${response.status}: ${text}`, response.status);
36
+ }
37
+ try {
38
+ return JSON.parse(text);
39
+ }
40
+ catch {
41
+ throw new DanxbotError(`danxbot ${init.method} ${path} returned unparseable body: ${text}`, response.status);
42
+ }
43
+ }
44
+ /**
45
+ * Create a launcher bound to one board/profile pair. One instance per consumer app
46
+ * (or per board, for an app dispatching onto more than one) — cheap, holds no
47
+ * mutable state of its own.
48
+ */
49
+ export function createDanxbotLauncher(options) {
50
+ return {
51
+ /**
52
+ * Dispatch a chat turn. `parentJobId` selects the endpoint: absent starts a
53
+ * fresh Claude session (`/api/launch`), present continues the one that job
54
+ * belongs to (`/api/resume`). Both carry the same `board`/`profile`/`task`/
55
+ * `overlay` shape — the only difference on the wire is the extra `job_id`.
56
+ */
57
+ async dispatchChatTurn(input) {
58
+ const body = {
59
+ board: options.board,
60
+ profile: options.profile,
61
+ task: input.task,
62
+ overlay: input.overlay,
63
+ ...(input.parentJobId ? { job_id: input.parentJobId } : {}),
64
+ };
65
+ const result = await danxbotFetch(options, input.parentJobId ? "/api/resume" : "/api/launch", { method: "POST", body });
66
+ return { jobId: result.job_id, parentJobId: result.parent_job_id };
67
+ },
68
+ async getJobStatus(jobId) {
69
+ const result = await danxbotFetch(options, `/api/status/${encodeURIComponent(jobId)}`, { method: "GET" });
70
+ return {
71
+ jobId: result.job_id,
72
+ status: result.status,
73
+ summary: result.summary ?? "",
74
+ elapsedSeconds: result.elapsed_seconds ?? 0,
75
+ inputTokens: result.input_tokens ?? 0,
76
+ outputTokens: result.output_tokens ?? 0,
77
+ cacheReadInputTokens: result.cache_read_input_tokens ?? 0,
78
+ cacheCreationInputTokens: result.cache_creation_input_tokens ?? 0,
79
+ };
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * danxbot job statuses that mean "this job will never produce anything more".
85
+ * Anything else (including an unknown one — danxbot owns this vocabulary and may
86
+ * grow it) is treated as still in flight, so a status danxbot adds later can never
87
+ * make a consumer declare a live turn dead.
88
+ */
89
+ const TERMINAL_JOB_STATUSES = new Set([
90
+ "completed",
91
+ "failed",
92
+ "critical_failure",
93
+ "cancelled",
94
+ "aborted",
95
+ ]);
96
+ export function isTerminalJobStatus(status) {
97
+ return TERMINAL_JOB_STATUSES.has(status);
98
+ }
99
+ /** A terminal status that is terminal because something went WRONG. */
100
+ export function isFailedJobStatus(status) {
101
+ return isTerminalJobStatus(status) && status !== "completed";
102
+ }
103
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.js","sourceRoot":"","sources":["../../src/chat/launcher.ts"],"names":[],"mappings":"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"}