@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.
- package/README.md +6 -3
- package/dist/bigquery/client.d.ts +18 -0
- package/dist/bigquery/client.js +37 -0
- package/dist/bigquery/client.js.map +1 -0
- package/dist/bigquery/errors.d.ts +26 -0
- package/dist/bigquery/errors.js +80 -0
- package/dist/bigquery/errors.js.map +1 -0
- package/dist/bigquery/extract.d.ts +15 -0
- package/dist/bigquery/extract.js +88 -0
- package/dist/bigquery/extract.js.map +1 -0
- package/dist/bigquery/index.d.ts +47 -0
- package/dist/bigquery/index.js +46 -0
- package/dist/bigquery/index.js.map +1 -0
- package/dist/bigquery/labels.d.ts +16 -0
- package/dist/bigquery/labels.js +26 -0
- package/dist/bigquery/labels.js.map +1 -0
- package/dist/bigquery/query.d.ts +16 -0
- package/dist/bigquery/query.js +86 -0
- package/dist/bigquery/query.js.map +1 -0
- package/dist/bigquery/types.d.ts +155 -0
- package/dist/bigquery/types.js +15 -0
- package/dist/bigquery/types.js.map +1 -0
- package/dist/chat/callback-secret.d.ts +21 -0
- package/dist/chat/callback-secret.js +33 -0
- package/dist/chat/callback-secret.js.map +1 -0
- package/dist/chat/env.d.ts +27 -0
- package/dist/chat/env.js +44 -0
- package/dist/chat/env.js.map +1 -0
- package/dist/chat/index.d.ts +72 -0
- package/dist/chat/index.js +70 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/launcher.d.ts +43 -0
- package/dist/chat/launcher.js +103 -0
- package/dist/chat/launcher.js.map +1 -0
- package/dist/chat/mcp-protocol.d.ts +96 -0
- package/dist/chat/mcp-protocol.js +200 -0
- package/dist/chat/mcp-protocol.js.map +1 -0
- package/dist/chat/mcp-server.d.ts +17 -0
- package/dist/chat/mcp-server.js +71 -0
- package/dist/chat/mcp-server.js.map +1 -0
- package/dist/chat/types.d.ts +91 -0
- package/dist/chat/types.js +5 -0
- package/dist/chat/types.js.map +1 -0
- package/dist/cli/flags-sync.js +2 -5
- package/dist/cli/flags-sync.js.map +1 -1
- package/dist/cli/profile-sync.js +2 -5
- package/dist/cli/profile-sync.js.map +1 -1
- package/dist/cli/sync-engine.d.ts +32 -0
- package/dist/cli/sync-engine.js +190 -23
- package/dist/cli/sync-engine.js.map +1 -1
- package/dist/cli/trace-sync.js +2 -5
- package/dist/cli/trace-sync.js.map +1 -1
- package/package.json +10 -1
- package/scripts/pending-release-count.mjs +37 -0
- package/scripts/release.sh +126 -0
- 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;
|
package/dist/chat/env.js
ADDED
|
@@ -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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"}
|