@kietdev/taiga-mcp 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +285 -2
- package/dist/config.js +59 -0
- package/dist/config.js.map +1 -0
- package/dist/format.js +104 -0
- package/dist/format.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/remote/app.js +238 -0
- package/dist/remote/app.js.map +1 -0
- package/dist/remote/config.js +52 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/guard.js +120 -0
- package/dist/remote/guard.js.map +1 -0
- package/dist/remote/pages.js +472 -0
- package/dist/remote/pages.js.map +1 -0
- package/dist/remote/provider.js +211 -0
- package/dist/remote/provider.js.map +1 -0
- package/dist/remote/seal.js +50 -0
- package/dist/remote/seal.js.map +1 -0
- package/dist/remote/taiga-auth.js +70 -0
- package/dist/remote/taiga-auth.js.map +1 -0
- package/dist/remote.js +23 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/taiga/client.js +1274 -0
- package/dist/taiga/client.js.map +1 -0
- package/dist/taiga/files.js +331 -0
- package/dist/taiga/files.js.map +1 -0
- package/dist/taiga/http.js +237 -0
- package/dist/taiga/http.js.map +1 -0
- package/dist/taiga/types.js +6 -0
- package/dist/taiga/types.js.map +1 -0
- package/dist/tools/add-comment.js +24 -0
- package/dist/tools/add-comment.js.map +1 -0
- package/dist/tools/create-item.js +63 -0
- package/dist/tools/create-item.js.map +1 -0
- package/dist/tools/file-view.js +300 -0
- package/dist/tools/file-view.js.map +1 -0
- package/dist/tools/get-attachment.js +76 -0
- package/dist/tools/get-attachment.js.map +1 -0
- package/dist/tools/get-item.js +120 -0
- package/dist/tools/get-item.js.map +1 -0
- package/dist/tools/get-project.js +47 -0
- package/dist/tools/get-project.js.map +1 -0
- package/dist/tools/get-sprint.js +56 -0
- package/dist/tools/get-sprint.js.map +1 -0
- package/dist/tools/list-activity.js +102 -0
- package/dist/tools/list-activity.js.map +1 -0
- package/dist/tools/list-epics.js +58 -0
- package/dist/tools/list-epics.js.map +1 -0
- package/dist/tools/list-issues.js +78 -0
- package/dist/tools/list-issues.js.map +1 -0
- package/dist/tools/list-projects.js +22 -0
- package/dist/tools/list-projects.js.map +1 -0
- package/dist/tools/list-sprints.js +46 -0
- package/dist/tools/list-sprints.js.map +1 -0
- package/dist/tools/list-tasks.js +84 -0
- package/dist/tools/list-tasks.js.map +1 -0
- package/dist/tools/list-user-stories.js +85 -0
- package/dist/tools/list-user-stories.js.map +1 -0
- package/dist/tools/search.js +83 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shared.js +90 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/update-item.js +74 -0
- package/dist/tools/update-item.js.map +1 -0
- package/dist/tools/wiki.js +62 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/tools/written.js +18 -0
- package/dist/tools/written.js.map +1 -0
- package/package.json +78 -4
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** A failed call to Taiga, worded so that the model (and the person reading its answer) can act on it. */
|
|
2
|
+
export class TaigaError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
name = 'TaigaError';
|
|
5
|
+
constructor(message,
|
|
6
|
+
/** HTTP status, when Taiga answered at all. */
|
|
7
|
+
status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const TIMEOUT_MS = 15_000;
|
|
13
|
+
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
14
|
+
const megabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
15
|
+
/** `size` is what was seen so far, so it may be less than the whole file. */
|
|
16
|
+
const tooLarge = (size, max) => `The file is at least ${megabytes(size)}; this server reads files up to ${megabytes(max)}. Open it in Taiga instead.`;
|
|
17
|
+
/**
|
|
18
|
+
* The only place that talks to Taiga. It signs in on first use, keeps the tokens in memory,
|
|
19
|
+
* and renews them once when Taiga answers 401. It sends GET requests, plus the two POSTs of the
|
|
20
|
+
* sign-in itself; POST and PATCH to create and change items only when the configuration allows
|
|
21
|
+
* writes. It has no way to send a DELETE.
|
|
22
|
+
*/
|
|
23
|
+
export class TaigaHttp {
|
|
24
|
+
#config;
|
|
25
|
+
#fetch;
|
|
26
|
+
#token;
|
|
27
|
+
#refreshToken;
|
|
28
|
+
/** A sign-in in flight, shared by the calls that are waiting for it. */
|
|
29
|
+
#signingIn;
|
|
30
|
+
constructor(config, fetchImpl = fetch) {
|
|
31
|
+
this.#config = config;
|
|
32
|
+
this.#fetch = fetchImpl;
|
|
33
|
+
this.#token = config.token;
|
|
34
|
+
this.#refreshToken = config.refreshToken;
|
|
35
|
+
}
|
|
36
|
+
/** One object, or a list that is known to be short (`everything` asks Taiga for all of it at once). */
|
|
37
|
+
async get(path, query = {}, options = {}) {
|
|
38
|
+
// Taiga pages lists by 30 unless told otherwise.
|
|
39
|
+
const headers = options.everything ? { 'x-disable-pagination': 'True' } : {};
|
|
40
|
+
const response = await this.#request('GET', path, query, headers);
|
|
41
|
+
return (await response.json());
|
|
42
|
+
}
|
|
43
|
+
/** Whether this connection may create and change items. */
|
|
44
|
+
get writable() {
|
|
45
|
+
return this.#config.write === true;
|
|
46
|
+
}
|
|
47
|
+
/** Create (POST) or change (PATCH) something, and read back what Taiga made of it. */
|
|
48
|
+
async write(method, path, body) {
|
|
49
|
+
// A second lock behind the tools: a read-only connection never sends a write, whatever calls it.
|
|
50
|
+
if (!this.writable) {
|
|
51
|
+
throw new TaigaError('This connection is read-only: changing Taiga is not switched on.', 403);
|
|
52
|
+
}
|
|
53
|
+
const response = await this.#request(method, path, {}, {}, body);
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `limit` items of a list that may be long: the first ones, or those of a later `page`.
|
|
58
|
+
* Taiga reports the full count in a header.
|
|
59
|
+
*/
|
|
60
|
+
async page(path, query, limit, page = 1) {
|
|
61
|
+
const paging = page > 1 ? { page_size: limit, page } : { page_size: limit };
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await this.#request('GET', path, { ...query, ...paging }, {});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Taiga answers 404 for a page after the last one.
|
|
68
|
+
if (page > 1 && error instanceof TaigaError && error.status === 404) {
|
|
69
|
+
throw new TaigaError(`The list has no page ${page} at ${limit} items a page. Call again with \`page\`: 1 to see how many items there are.`, 404);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const items = (await response.json());
|
|
74
|
+
const count = Number(response.headers.get('x-pagination-count') ?? Number.NaN);
|
|
75
|
+
return { items, total: Number.isFinite(count) ? Math.max(count, items.length) : items.length };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A file Taiga stores, by the address Taiga gave for it. Those addresses carry their own
|
|
79
|
+
* signature, so no token is sent: the file server may be another host. Stops at `maxBytes`.
|
|
80
|
+
*/
|
|
81
|
+
async download(url, maxBytes) {
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await this.#fetch(new URL(url), { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
88
|
+
throw new TaigaError(`Could not download the file (${reason}).`);
|
|
89
|
+
}
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new TaigaError(response.status === 403 || response.status === 401
|
|
92
|
+
? 'Taiga refused the file. Its link may have expired: read the item again with get_item and retry.'
|
|
93
|
+
: `Taiga answered ${response.status} for the file.`, response.status);
|
|
94
|
+
}
|
|
95
|
+
const declared = Number(response.headers.get('content-length') ?? Number.NaN);
|
|
96
|
+
if (declared > maxBytes) {
|
|
97
|
+
await response.body?.cancel();
|
|
98
|
+
throw new TaigaError(tooLarge(declared, maxBytes), 413);
|
|
99
|
+
}
|
|
100
|
+
// The declared length may be missing or wrong: count while reading.
|
|
101
|
+
const chunks = [];
|
|
102
|
+
let size = 0;
|
|
103
|
+
const reader = response.body?.getReader();
|
|
104
|
+
while (reader !== undefined) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done)
|
|
107
|
+
break;
|
|
108
|
+
size += value.byteLength;
|
|
109
|
+
if (size > maxBytes) {
|
|
110
|
+
await reader.cancel();
|
|
111
|
+
throw new TaigaError(tooLarge(size, maxBytes), 413);
|
|
112
|
+
}
|
|
113
|
+
chunks.push(value);
|
|
114
|
+
}
|
|
115
|
+
const bytes = new Uint8Array(size);
|
|
116
|
+
let at = 0;
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
bytes.set(chunk, at);
|
|
119
|
+
at += chunk.byteLength;
|
|
120
|
+
}
|
|
121
|
+
return { bytes, type: response.headers.get('content-type') ?? undefined };
|
|
122
|
+
}
|
|
123
|
+
/* ------------------------------ requests ------------------------------ */
|
|
124
|
+
async #request(method, path, query, headers, body) {
|
|
125
|
+
let response = await this.#send(path, query, { ...headers, ...this.#bearer(await this.#currentToken()) }, body, method);
|
|
126
|
+
if (response.status === 401) {
|
|
127
|
+
response = await this.#send(path, query, { ...headers, ...this.#bearer(await this.#renewToken()) }, body, method);
|
|
128
|
+
}
|
|
129
|
+
if (!response.ok)
|
|
130
|
+
throw await this.#failure(response, path, method);
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
#bearer(token) {
|
|
134
|
+
return { Authorization: `Bearer ${token}` };
|
|
135
|
+
}
|
|
136
|
+
async #send(path, query, headers, body,
|
|
137
|
+
/** GET without a body; the sign-in sends its body with POST. */
|
|
138
|
+
method = body === undefined ? 'GET' : 'POST') {
|
|
139
|
+
const url = new URL(`${this.#config.baseUrl}/api/v1${path}`);
|
|
140
|
+
for (const [key, value] of Object.entries(query)) {
|
|
141
|
+
if (value !== undefined)
|
|
142
|
+
url.searchParams.set(key, String(value));
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return await this.#fetch(url, {
|
|
146
|
+
method,
|
|
147
|
+
headers: {
|
|
148
|
+
Accept: 'application/json',
|
|
149
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
150
|
+
...headers,
|
|
151
|
+
},
|
|
152
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
153
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
158
|
+
if (method !== 'GET' && !path.startsWith('/auth')) {
|
|
159
|
+
// The request may have arrived even though no answer came back: do not invite a duplicate.
|
|
160
|
+
throw new TaigaError(`No answer from Taiga at ${this.#config.baseUrl} (${reason}). The change may or may not have been made: ` +
|
|
161
|
+
'look at the item (or the list) before trying again.');
|
|
162
|
+
}
|
|
163
|
+
throw new TaigaError(`Could not reach Taiga at ${this.#config.baseUrl} (${reason}). Check TAIGA_URL.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async #failure(response, path, method) {
|
|
167
|
+
const { status } = response;
|
|
168
|
+
const verb = method === 'GET' ? 'read' : 'change';
|
|
169
|
+
if (status === 401) {
|
|
170
|
+
return new TaigaError('Taiga did not accept the sign-in. Check the credentials in the environment.', 401);
|
|
171
|
+
}
|
|
172
|
+
if (status === 403)
|
|
173
|
+
return new TaigaError(`The signed-in user may not ${verb} ${path} in Taiga.`, 403);
|
|
174
|
+
if (status === 404)
|
|
175
|
+
return new TaigaError(`Taiga has nothing at ${path}.`, 404);
|
|
176
|
+
if (status === 429)
|
|
177
|
+
return new TaigaError('Taiga is rate-limiting requests. Wait a moment and try again.', 429);
|
|
178
|
+
// Taiga explains most client errors in `_error_message`, and a rejected change field by field.
|
|
179
|
+
const detail = await response
|
|
180
|
+
.json()
|
|
181
|
+
.then(explain)
|
|
182
|
+
.catch(() => '');
|
|
183
|
+
return new TaigaError(`Taiga answered ${status} for ${path}${detail ? `: ${detail}` : ''}.`, status);
|
|
184
|
+
}
|
|
185
|
+
/* ------------------------------- sign-in ------------------------------- */
|
|
186
|
+
async #currentToken() {
|
|
187
|
+
return this.#token ?? this.#signIn();
|
|
188
|
+
}
|
|
189
|
+
/** After a 401: try the refresh token, then the username and password. */
|
|
190
|
+
async #renewToken() {
|
|
191
|
+
const refresh = this.#refreshToken;
|
|
192
|
+
this.#token = undefined;
|
|
193
|
+
if (refresh !== undefined) {
|
|
194
|
+
const response = await this.#send('/auth/refresh', {}, {}, { refresh });
|
|
195
|
+
if (response.ok)
|
|
196
|
+
return this.#remember((await response.json()));
|
|
197
|
+
this.#refreshToken = undefined;
|
|
198
|
+
}
|
|
199
|
+
return this.#signIn();
|
|
200
|
+
}
|
|
201
|
+
#signIn() {
|
|
202
|
+
this.#signingIn ??= this.#signInOnce().finally(() => {
|
|
203
|
+
this.#signingIn = undefined;
|
|
204
|
+
});
|
|
205
|
+
return this.#signingIn;
|
|
206
|
+
}
|
|
207
|
+
async #signInOnce() {
|
|
208
|
+
const { credentials } = this.#config;
|
|
209
|
+
if (credentials === undefined) {
|
|
210
|
+
throw new TaigaError(this.#config.expiredHint ??
|
|
211
|
+
'The Taiga token has expired and cannot be renewed. Set a new TAIGA_TOKEN, or set TAIGA_USERNAME and TAIGA_PASSWORD so the server can sign in by itself.', 401);
|
|
212
|
+
}
|
|
213
|
+
const response = await this.#send('/auth', {}, {}, { type: 'normal', ...credentials });
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
// Deliberately generic: the response body may repeat what was sent.
|
|
216
|
+
throw new TaigaError(`Taiga sign-in failed (${response.status}). Check TAIGA_USERNAME and TAIGA_PASSWORD.`, response.status);
|
|
217
|
+
}
|
|
218
|
+
return this.#remember((await response.json()));
|
|
219
|
+
}
|
|
220
|
+
#remember(auth) {
|
|
221
|
+
this.#token = auth.auth_token;
|
|
222
|
+
this.#refreshToken = auth.refresh;
|
|
223
|
+
return auth.auth_token;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Taiga's reason for refusing a request, in one line: its message, or each field and what is wrong. */
|
|
227
|
+
function explain(data) {
|
|
228
|
+
if (typeof data !== 'object' || data === null)
|
|
229
|
+
return '';
|
|
230
|
+
if ('_error_message' in data)
|
|
231
|
+
return String(data._error_message);
|
|
232
|
+
return Object.entries(data)
|
|
233
|
+
.filter(([key]) => !key.startsWith('_'))
|
|
234
|
+
.map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(' ') : String(value)}`)
|
|
235
|
+
.join('; ');
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/taiga/http.ts"],"names":[],"mappings":"AAGA,0GAA0G;AAC1G,MAAM,OAAO,UAAW,SAAQ,KAAK;IAKxB;IAJO,IAAI,GAAG,YAAY,CAAC;IACtC,YACE,OAAe;IACf,+CAA+C;IACtC,MAAe;QAExB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,WAAM,GAAN,MAAM,CAAS;IAG1B,CAAC;CACF;AAUD,MAAM,UAAU,GAAG,MAAM,CAAC;AAC1B,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,MAAM,SAAS,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAChF,6EAA6E;AAC7E,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,GAAW,EAAE,EAAE,CAC7C,wBAAwB,SAAS,CAAC,IAAI,CAAC,mCAAmC,SAAS,CAAC,GAAG,CAAC,6BAA6B,CAAC;AAKxH;;;;;GAKG;AACH,MAAM,OAAO,SAAS;IACX,OAAO,CAAc;IACrB,MAAM,CAAe;IAC9B,MAAM,CAAqB;IAC3B,aAAa,CAAqB;IAClC,wEAAwE;IACxE,UAAU,CAA8B;IAExC,YAAY,MAAmB,EAAE,YAA0B,KAAK;QAC9D,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,YAAY,CAAC;IAC3C,CAAC;IAED,uGAAuG;IACvG,KAAK,CAAC,GAAG,CAAI,IAAY,EAAE,QAAe,EAAE,EAAE,UAAoC,EAAE;QAClF,iDAAiD;QACjD,MAAM,OAAO,GAA2B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrG,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAClE,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IACtC,CAAC;IAED,2DAA2D;IAC3D,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC;IACrC,CAAC;IAED,sFAAsF;IACtF,KAAK,CAAC,KAAK,CAAI,MAAmB,EAAE,IAAY,EAAE,IAAuC;QACvF,iGAAiG;QACjG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,IAAI,UAAU,CAAC,kEAAkE,EAAE,GAAG,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IACtC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,KAAY,EAAE,KAAa,EAAE,IAAI,GAAG,CAAC;QAC/D,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAC5E,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,mDAAmD;YACnD,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,UAAU,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACpE,MAAM,IAAI,UAAU,CAClB,wBAAwB,IAAI,OAAO,KAAK,6EAA6E,EACrH,GAAG,CACJ,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAQ,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;QAC/E,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IACjG,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ,CAAC,GAAW,EAAE,QAAgB;QAC1C,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,IAAI,UAAU,CAAC,gCAAgC,MAAM,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,UAAU,CAClB,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;gBAChD,CAAC,CAAC,iGAAiG;gBACnG,CAAC,CAAC,kBAAkB,QAAQ,CAAC,MAAM,gBAAgB,EACrD,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9E,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAAC;YACxB,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;YAC9B,MAAM,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC;QAC1D,CAAC;QACD,oEAAoE;QACpE,MAAM,MAAM,GAAiB,EAAE,CAAC;QAChC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;QAC1C,OAAO,MAAM,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC;YACzB,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACpB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;gBACtB,MAAM,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC;YACtD,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACrB,EAAE,IAAI,KAAK,CAAC,UAAU,CAAC;QACzB,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,SAAS,EAAE,CAAC;IAC5E,CAAC;IAED,4EAA4E;IAE5E,KAAK,CAAC,QAAQ,CACZ,MAA2B,EAC3B,IAAY,EACZ,KAAY,EACZ,OAA+B,EAC/B,IAAc;QAEd,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B,IAAI,EACJ,KAAK,EACL,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC,EAAE,EAC3D,IAAI,EACJ,MAAM,CACP,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CACzB,IAAI,EACJ,KAAK,EACL,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,EACzD,IAAI,EACJ,MAAM,CACP,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACpE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,KAAa;QACnB,OAAO,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,KAAK,CACT,IAAY,EACZ,KAAY,EACZ,OAA+B,EAC/B,IAAc;IACd,gEAAgE;IAChE,SAA8B,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;QAEjE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,UAAU,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,KAAK,KAAK,SAAS;gBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC5B,MAAM;gBACN,OAAO,EAAE;oBACP,MAAM,EAAE,kBAAkB;oBAC1B,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;oBACrE,GAAG,OAAO;iBACX;gBACD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7D,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;aACxC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,IAAI,MAAM,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBAClD,2FAA2F;gBAC3F,MAAM,IAAI,UAAU,CAClB,2BAA2B,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,MAAM,+CAA+C;oBACvG,qDAAqD,CACxD,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,UAAU,CAAC,4BAA4B,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,MAAM,qBAAqB,CAAC,CAAC;QACzG,CAAC;IACH,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,QAAkB,EAAE,IAAY,EAAE,MAA2B;QAC1E,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;QAClD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,IAAI,UAAU,CACnB,6EAA6E,EAC7E,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,UAAU,CAAC,8BAA8B,IAAI,IAAI,IAAI,YAAY,EAAE,GAAG,CAAC,CAAC;QACvG,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,UAAU,CAAC,wBAAwB,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;QAChF,IAAI,MAAM,KAAK,GAAG;YAChB,OAAO,IAAI,UAAU,CAAC,+DAA+D,EAAE,GAAG,CAAC,CAAC;QAC9F,+FAA+F;QAC/F,MAAM,MAAM,GAAG,MAAM,QAAQ;aAC1B,IAAI,EAAE;aACN,IAAI,CAAC,OAAO,CAAC;aACb,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnB,OAAO,IAAI,UAAU,CAAC,kBAAkB,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACvG,CAAC;IAED,6EAA6E;IAE7E,KAAK,CAAC,aAAa;QACjB,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;IACvC,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,WAAW;QACf,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YACxE,IAAI,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAC,CAAC;YAC3E,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAED,OAAO;QACL,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAClD,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACrC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,IAAI,UAAU,CAClB,IAAI,CAAC,OAAO,CAAC,WAAW;gBACtB,yJAAyJ,EAC3J,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,oEAAoE;YACpE,MAAM,IAAI,UAAU,CAClB,yBAAyB,QAAQ,CAAC,MAAM,6CAA6C,EACrF,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAC,CAAC;IAC5D,CAAC;IAED,SAAS,CAAC,IAAa;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC;QAClC,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;CACF;AAED,wGAAwG;AACxG,SAAS,OAAO,CAAC,IAAa;IAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACzD,IAAI,gBAAgB,IAAI,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;SAC1F,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/taiga/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { answer } from '../format.js';
|
|
3
|
+
import { ADDS, ONLY_WHEN_ASKED, project, ref } from './shared.js';
|
|
4
|
+
import { written } from './written.js';
|
|
5
|
+
export function registerAddComment(server, taiga) {
|
|
6
|
+
server.registerTool('add_comment', {
|
|
7
|
+
title: 'Comment on a story, issue, epic or task',
|
|
8
|
+
description: 'Post a comment on one user story, issue, epic or task of a Taiga project, by its reference ' +
|
|
9
|
+
'number, as the signed-in user. Project members are notified as Taiga does for any comment. ' +
|
|
10
|
+
`${ONLY_WHEN_ASKED} Unless the user dictated the text word for word, show it to them first. ` +
|
|
11
|
+
'Comments cannot be edited or removed from here.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
project,
|
|
14
|
+
ref,
|
|
15
|
+
text: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(10_000)
|
|
19
|
+
.describe('The comment exactly as it should appear in Taiga. Markdown works.'),
|
|
20
|
+
},
|
|
21
|
+
annotations: ADDS,
|
|
22
|
+
}, answer(async (args) => written('Commented on', await taiga.addComment(args.project, args.ref, args.text))));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=add-comment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-comment.js","sourceRoot":"","sources":["../../src/tools/add-comment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,KAAkB;IACtE,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,yCAAyC;QAChD,WAAW,EACT,6FAA6F;YAC7F,6FAA6F;YAC7F,GAAG,eAAe,2EAA2E;YAC7F,iDAAiD;QACnD,WAAW,EAAE;YACX,OAAO;YACP,GAAG;YACH,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,MAAM,CAAC;iBACX,QAAQ,CAAC,mEAAmE,CAAC;SACjF;QACD,WAAW,EAAE,IAAI;KAClB,EACD,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACpB,OAAO,CAAC,cAAc,EAAE,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CACnF,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { answer } from '../format.js';
|
|
3
|
+
import { ADDS, assignee, ONLY_WHEN_ASKED, project, subject, tags } from './shared.js';
|
|
4
|
+
import { written } from './written.js';
|
|
5
|
+
const name = (what) => z.string().min(1).max(200).optional().describe(what);
|
|
6
|
+
export function registerCreateItem(server, taiga) {
|
|
7
|
+
server.registerTool('create_item', {
|
|
8
|
+
title: 'Create a user story, task or issue',
|
|
9
|
+
description: 'Create a user story, a task or an issue in a Taiga project, as the signed-in user. ' +
|
|
10
|
+
`${ONLY_WHEN_ASKED} Every name (status, person, sprint, story, epic, type…) is checked before ` +
|
|
11
|
+
'anything is created: a wrong one is answered with the valid names, and nothing is created. ' +
|
|
12
|
+
'The answer gives the new reference number. Call it once per item: if an answer is lost, look ' +
|
|
13
|
+
'for the item with search before creating it again.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
project,
|
|
16
|
+
kind: z
|
|
17
|
+
.enum(['story', 'task', 'issue'])
|
|
18
|
+
.describe('"story" for a user story, "task" for a task (usually of a story), "issue" for a bug or question.'),
|
|
19
|
+
subject: subject.describe('Title of the new item, short and specific.'),
|
|
20
|
+
description: z.string().max(50_000).optional().describe('Longer text for the item. Markdown works.'),
|
|
21
|
+
status: name('Status name as the project defines it for this kind of item (get_project lists them). Taiga’s default when left out.'),
|
|
22
|
+
assigned_to: assignee('Who will do it: a member’s full name or username (part of it is enough when it fits one member; accents may be left out), or "me". Nobody when left out.'),
|
|
23
|
+
tags: tags('Tags to put on the item, e.g. ["backend"].'),
|
|
24
|
+
sprint: name('Sprint name, "current" for the one running today, or "backlog" for none. Left out: a task joins the sprint of its story, a story or issue has none.'),
|
|
25
|
+
story: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('For a task: reference number of the user story it belongs to.'),
|
|
31
|
+
epic: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('For a user story: reference number of an epic to add it to.'),
|
|
37
|
+
due_date: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD')
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Due date as YYYY-MM-DD.'),
|
|
42
|
+
type: name('For an issue: its type, e.g. "Bug". get_project lists the types.'),
|
|
43
|
+
severity: name('For an issue: its severity, e.g. "Critical".'),
|
|
44
|
+
priority: name('For an issue: its priority, e.g. "High".'),
|
|
45
|
+
},
|
|
46
|
+
annotations: ADDS,
|
|
47
|
+
}, answer(async (args) => written('Created', await taiga.createItem(args.project, {
|
|
48
|
+
kind: args.kind,
|
|
49
|
+
subject: args.subject,
|
|
50
|
+
description: args.description,
|
|
51
|
+
status: args.status,
|
|
52
|
+
assignedTo: args.assigned_to,
|
|
53
|
+
tags: args.tags,
|
|
54
|
+
sprint: args.sprint,
|
|
55
|
+
story: args.story,
|
|
56
|
+
epic: args.epic,
|
|
57
|
+
dueDate: args.due_date,
|
|
58
|
+
type: args.type,
|
|
59
|
+
severity: args.severity,
|
|
60
|
+
priority: args.priority,
|
|
61
|
+
}))));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=create-item.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-item.js","sourceRoot":"","sources":["../../src/tools/create-item.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { fold, plural } from '../format.js';
|
|
2
|
+
import { TaigaError } from '../taiga/http.js';
|
|
3
|
+
export const BUDGET = 20_000;
|
|
4
|
+
const LINE_MAX = 2_000;
|
|
5
|
+
const PREVIEW_ROWS = 10;
|
|
6
|
+
const PREVIEW_LINES = 60;
|
|
7
|
+
const OUTLINE_PAGES = 100;
|
|
8
|
+
const FIRST_PAGE_MAX = 3_000;
|
|
9
|
+
const clip = (line) => (line.length <= LINE_MAX ? line : `${line.slice(0, LINE_MAX)}… [line cut]`);
|
|
10
|
+
/** "120-300", "120", "120-": the numbers asked for, both ends included. */
|
|
11
|
+
export function parseRange(text, what) {
|
|
12
|
+
const match = /^\s*(\d+)\s*(?:[-–]\s*(\d*)\s*)?$/.exec(text);
|
|
13
|
+
if (match === null)
|
|
14
|
+
throw new TaigaError(`\`${what}\` takes numbers like "120-300", "5" or "40-"; not "${text}".`);
|
|
15
|
+
const from = Number(match[1]);
|
|
16
|
+
const to = match[2] === undefined ? from : match[2] === '' ? Number.POSITIVE_INFINITY : Number(match[2]);
|
|
17
|
+
if (from < 1 || to < from)
|
|
18
|
+
throw new TaigaError(`\`${what}\` "${text}" is not a range: it must count up from 1.`);
|
|
19
|
+
return { from, to };
|
|
20
|
+
}
|
|
21
|
+
/** Lines added one by one until the budget is spent; says where it stopped. */
|
|
22
|
+
class Out {
|
|
23
|
+
lines = [];
|
|
24
|
+
#size = 0;
|
|
25
|
+
full = false;
|
|
26
|
+
add(line) {
|
|
27
|
+
const text = clip(line);
|
|
28
|
+
if (this.#size + text.length + 1 > BUDGET) {
|
|
29
|
+
this.full = true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
this.lines.push(text);
|
|
33
|
+
this.#size += text.length + 1;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this.#size;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const matcher = (find) => {
|
|
41
|
+
const words = fold(find).split(/\s+/).filter(Boolean);
|
|
42
|
+
return (text) => {
|
|
43
|
+
const folded = fold(text);
|
|
44
|
+
return words.every((word) => folded.includes(word));
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export function view(content, selection) {
|
|
48
|
+
if (selection.sheet !== undefined && content.kind !== 'sheets') {
|
|
49
|
+
throw new TaigaError('`sheet` is for spreadsheets and CSV files. This file has no sheets.');
|
|
50
|
+
}
|
|
51
|
+
if (selection.pages !== undefined && content.kind !== 'pages') {
|
|
52
|
+
throw new TaigaError(`\`pages\` is for PDFs and presentations. ${content.kind === 'sheets' ? 'Use `sheet` and `rows`' : 'Use `rows` for lines'} here.`);
|
|
53
|
+
}
|
|
54
|
+
if (content.kind === 'sheets')
|
|
55
|
+
return viewSheets(content, selection);
|
|
56
|
+
if (content.kind === 'pages')
|
|
57
|
+
return viewPages(content, selection);
|
|
58
|
+
return viewLines(content, selection);
|
|
59
|
+
}
|
|
60
|
+
/* ---------------------------------- sheets ---------------------------------- */
|
|
61
|
+
const rowText = (row) => `${row.n}: ${row.cells.join(' | ')}`;
|
|
62
|
+
function pickSheets(sheets, wanted) {
|
|
63
|
+
if (wanted === undefined)
|
|
64
|
+
return sheets;
|
|
65
|
+
const byNumber = /^\d+$/.test(wanted.trim()) ? sheets[Number(wanted) - 1] : undefined;
|
|
66
|
+
const byName = sheets.find((sheet) => fold(sheet.name) === fold(wanted));
|
|
67
|
+
const found = byName ?? byNumber;
|
|
68
|
+
if (found === undefined) {
|
|
69
|
+
throw new TaigaError(`No sheet "${wanted}". Sheets: ${sheets.map((sheet, i) => `${i + 1}. "${sheet.name}"`).join(', ')}.`);
|
|
70
|
+
}
|
|
71
|
+
return [found];
|
|
72
|
+
}
|
|
73
|
+
function columns(sheet) {
|
|
74
|
+
return sheet.rows.reduce((max, row) => Math.max(max, row.cells.length), 0);
|
|
75
|
+
}
|
|
76
|
+
function viewSheets(content, selection) {
|
|
77
|
+
const sheets = pickSheets(content.sheets, selection.sheet);
|
|
78
|
+
const out = new Out();
|
|
79
|
+
const head = `Read as: ${content.from}. Each line is "row number: cells split by |".`;
|
|
80
|
+
if (selection.find !== undefined) {
|
|
81
|
+
const fits = matcher(selection.find);
|
|
82
|
+
let found = 0;
|
|
83
|
+
let shown = 0;
|
|
84
|
+
for (const sheet of sheets) {
|
|
85
|
+
for (const row of sheet.rows) {
|
|
86
|
+
if (!fits(row.cells.join(' ')))
|
|
87
|
+
continue;
|
|
88
|
+
found++;
|
|
89
|
+
if (!out.full && out.add(`${sheets.length > 1 ? `"${sheet.name}" ` : ''}${rowText(row)}`))
|
|
90
|
+
shown++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
`${head}\nRows containing "${selection.find}": ${found}${shown < found ? `, the first ${shown} shown` : ''}.`,
|
|
95
|
+
out.lines.join('\n'),
|
|
96
|
+
shown < found ? 'Narrow `find` with more words, or pick a `sheet`.' : undefined,
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join('\n\n');
|
|
100
|
+
}
|
|
101
|
+
if (selection.rows !== undefined) {
|
|
102
|
+
if (sheets.length > 1) {
|
|
103
|
+
throw new TaigaError(`The file has ${sheets.length} sheets: name one with \`sheet\` (${sheets.map((s) => `"${s.name}"`).join(', ')}).`);
|
|
104
|
+
}
|
|
105
|
+
const [sheet] = sheets;
|
|
106
|
+
const { from, to } = parseRange(selection.rows, 'rows');
|
|
107
|
+
const header = sheet.rows[0];
|
|
108
|
+
const wanted = sheet.rows.filter((row) => row.n >= from && row.n <= to);
|
|
109
|
+
if (header !== undefined && header.n < from)
|
|
110
|
+
out.add(`(header) ${rowText(header)}`);
|
|
111
|
+
let last;
|
|
112
|
+
for (const row of wanted) {
|
|
113
|
+
if (!out.add(rowText(row)))
|
|
114
|
+
break;
|
|
115
|
+
last = row.n;
|
|
116
|
+
}
|
|
117
|
+
const lastInFile = sheet.rows.at(-1)?.n ?? 0;
|
|
118
|
+
const end = Number.isFinite(to) ? to : lastInFile;
|
|
119
|
+
// Rows asked for that did not fit in this answer.
|
|
120
|
+
const next = wanted.find((row) => last === undefined || row.n > last);
|
|
121
|
+
const more = next === undefined ? undefined : `Continue with \`rows\`: "${next.n}-${end}".`;
|
|
122
|
+
return [
|
|
123
|
+
`${head}\nSheet "${sheet.name}", rows ${from} to ${end}: ${wanted.length} with data${next === undefined ? '' : `, up to row ${last ?? from} shown`}.`,
|
|
124
|
+
out.lines.join('\n') || '(no rows with data here)',
|
|
125
|
+
more,
|
|
126
|
+
cutNote(sheet),
|
|
127
|
+
]
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.join('\n\n');
|
|
130
|
+
}
|
|
131
|
+
// Everything, when it fits.
|
|
132
|
+
const whole = sheets.flatMap((sheet) => [`=== Sheet "${sheet.name}" ===`, ...sheet.rows.map(rowText), '']);
|
|
133
|
+
if (whole.every((line) => out.add(line)) &&
|
|
134
|
+
sheets.every((sheet) => sheet.rows.length === sheet.totalRows)) {
|
|
135
|
+
return `${head}\n\n${out.lines.join('\n').trim()}`;
|
|
136
|
+
}
|
|
137
|
+
// An overview, and how to read the rest.
|
|
138
|
+
const overview = sheets.map((sheet) => {
|
|
139
|
+
const preview = sheet.rows.slice(0, PREVIEW_ROWS).map(rowText);
|
|
140
|
+
return [
|
|
141
|
+
`=== Sheet "${sheet.name}": ${plural(sheet.totalRows, 'row')} with data, up to ${plural(columns(sheet), 'column')} ===`,
|
|
142
|
+
preview.length === 0 ? '(empty)' : preview.join('\n'),
|
|
143
|
+
sheet.rows.length > PREVIEW_ROWS ? `… ${sheet.totalRows - PREVIEW_ROWS} more rows` : undefined,
|
|
144
|
+
cutNote(sheet),
|
|
145
|
+
]
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join('\n');
|
|
148
|
+
});
|
|
149
|
+
return [
|
|
150
|
+
`${head}\nToo large to show at once; this is an overview with the first ${PREVIEW_ROWS} rows of each sheet.`,
|
|
151
|
+
clipBudget(overview.join('\n\n')),
|
|
152
|
+
'To read more: `sheet` with `rows` (e.g. "11-300"), or `find` with words to get only the rows that contain them.',
|
|
153
|
+
].join('\n\n');
|
|
154
|
+
}
|
|
155
|
+
const cutNote = (sheet) => sheet.rows.length < sheet.totalRows
|
|
156
|
+
? `Only the first ${sheet.rows.length} of ${sheet.totalRows} rows of "${sheet.name}" can be read; use \`find\` to reach the rest.`
|
|
157
|
+
: undefined;
|
|
158
|
+
const clipBudget = (text) => text.length <= BUDGET ? text : `${text.slice(0, BUDGET)}… [overview cut; pick a \`sheet\`]`;
|
|
159
|
+
/* ---------------------------------- pages ---------------------------------- */
|
|
160
|
+
function viewPages(content, selection) {
|
|
161
|
+
const { pages, unit, totalPages } = content;
|
|
162
|
+
const Unit = unit === 'page' ? 'Page' : 'Slide';
|
|
163
|
+
const out = new Out();
|
|
164
|
+
const head = `Read as: ${content.from}.`;
|
|
165
|
+
const unread = pages.length < totalPages ? `Pages after ${pages.length} cannot be read by this server.` : undefined;
|
|
166
|
+
if (selection.find !== undefined) {
|
|
167
|
+
const fits = matcher(selection.find);
|
|
168
|
+
let found = 0;
|
|
169
|
+
let shown = 0;
|
|
170
|
+
pages.forEach((page, i) => {
|
|
171
|
+
for (const line of page.split('\n')) {
|
|
172
|
+
if (!fits(line))
|
|
173
|
+
continue;
|
|
174
|
+
found++;
|
|
175
|
+
if (!out.full && out.add(`${Unit} ${i + 1}: ${line.trim()}`))
|
|
176
|
+
shown++;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return [
|
|
180
|
+
`${head}\nLines containing "${selection.find}": ${found}${shown < found ? `, the first ${shown} shown` : ''}.`,
|
|
181
|
+
out.lines.join('\n'),
|
|
182
|
+
found > 0 ? `Read a whole ${unit} with \`pages\`.` : undefined,
|
|
183
|
+
unread,
|
|
184
|
+
]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join('\n\n');
|
|
187
|
+
}
|
|
188
|
+
const render = (from, to) => {
|
|
189
|
+
let last = from - 1;
|
|
190
|
+
for (let n = from; n <= Math.min(to, pages.length); n++) {
|
|
191
|
+
const block = [
|
|
192
|
+
`--- ${Unit} ${n} ---`,
|
|
193
|
+
...(pages[n - 1] || `(no text on this ${unit}${unit === 'page' ? '; it may be a scan' : ''})`).split('\n'),
|
|
194
|
+
];
|
|
195
|
+
// A page is shown whole or not at all, unless it alone is larger than the budget.
|
|
196
|
+
const fits = block.reduce((sum, line) => sum + clip(line).length + 1, 0) + out.size <= BUDGET;
|
|
197
|
+
if (!fits && last >= from)
|
|
198
|
+
break;
|
|
199
|
+
for (const line of block)
|
|
200
|
+
if (!out.add(line))
|
|
201
|
+
break;
|
|
202
|
+
last = n;
|
|
203
|
+
if (out.full)
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
return last;
|
|
207
|
+
};
|
|
208
|
+
if (selection.pages !== undefined) {
|
|
209
|
+
const { from, to } = parseRange(selection.pages, 'pages');
|
|
210
|
+
if (from > pages.length) {
|
|
211
|
+
throw new TaigaError(`The file has ${totalPages} ${unit}s${unread ? `, of which ${pages.length} can be read` : ''}.`);
|
|
212
|
+
}
|
|
213
|
+
const last = render(from, to);
|
|
214
|
+
const end = Math.min(to, pages.length);
|
|
215
|
+
return [
|
|
216
|
+
`${head}\n${Unit}s ${from} to ${last}${last < end ? ` of the ${from}-${end} asked for` : ''}.`,
|
|
217
|
+
out.lines.join('\n'),
|
|
218
|
+
last < end ? `Continue with \`pages\`: "${last + 1}-${end}".` : undefined,
|
|
219
|
+
unread,
|
|
220
|
+
]
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.join('\n\n');
|
|
223
|
+
}
|
|
224
|
+
const total = pages.reduce((sum, page) => sum + page.length + 20, 0);
|
|
225
|
+
if (total <= BUDGET && pages.length === totalPages) {
|
|
226
|
+
render(1, pages.length);
|
|
227
|
+
return `${head}\n\n${out.lines.join('\n')}`;
|
|
228
|
+
}
|
|
229
|
+
const outline = pages
|
|
230
|
+
.slice(0, OUTLINE_PAGES)
|
|
231
|
+
.map((page, i) => `${Unit} ${i + 1}: ${(page.split('\n').find((line) => line.trim()) ?? '(no text)').slice(0, 80)}`);
|
|
232
|
+
const first = pages[0] ?? '';
|
|
233
|
+
return [
|
|
234
|
+
`${head}\nToo large to show at once; this is the first line of each ${unit}${pages.length > OUTLINE_PAGES ? ` (the first ${OUTLINE_PAGES})` : ''}, then ${Unit.toLowerCase()} 1.`,
|
|
235
|
+
outline.join('\n'),
|
|
236
|
+
`--- ${Unit} 1 ---\n${first.length > FIRST_PAGE_MAX ? `${first.slice(0, FIRST_PAGE_MAX)}… [${unit} cut]` : first || '(no text)'}`,
|
|
237
|
+
`To read more: \`pages\` (e.g. "2-6"), or \`find\` with words to get only the lines that contain them.`,
|
|
238
|
+
unread,
|
|
239
|
+
]
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.join('\n\n');
|
|
242
|
+
}
|
|
243
|
+
/* ---------------------------------- lines ---------------------------------- */
|
|
244
|
+
function viewLines(content, selection) {
|
|
245
|
+
const { lines } = content;
|
|
246
|
+
const out = new Out();
|
|
247
|
+
const head = `Read as: ${content.from}, ${lines.length} lines.`;
|
|
248
|
+
if (selection.find !== undefined) {
|
|
249
|
+
const fits = matcher(selection.find);
|
|
250
|
+
let found = 0;
|
|
251
|
+
let shown = 0;
|
|
252
|
+
lines.forEach((line, i) => {
|
|
253
|
+
if (!fits(line))
|
|
254
|
+
return;
|
|
255
|
+
found++;
|
|
256
|
+
if (!out.full && out.add(`${i + 1}: ${line}`))
|
|
257
|
+
shown++;
|
|
258
|
+
});
|
|
259
|
+
return [
|
|
260
|
+
`${head}\nLines containing "${selection.find}": ${found}${shown < found ? `, the first ${shown} shown` : ''}.`,
|
|
261
|
+
out.lines.join('\n'),
|
|
262
|
+
found > 0 ? 'Read the lines around one with `rows`.' : undefined,
|
|
263
|
+
]
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join('\n\n');
|
|
266
|
+
}
|
|
267
|
+
const render = (from, to) => {
|
|
268
|
+
let last = from - 1;
|
|
269
|
+
for (let n = from; n <= Math.min(to, lines.length); n++) {
|
|
270
|
+
if (!out.add(`${n}: ${lines[n - 1] ?? ''}`))
|
|
271
|
+
break;
|
|
272
|
+
last = n;
|
|
273
|
+
}
|
|
274
|
+
return last;
|
|
275
|
+
};
|
|
276
|
+
if (selection.rows !== undefined) {
|
|
277
|
+
const { from, to } = parseRange(selection.rows, 'rows');
|
|
278
|
+
if (from > lines.length)
|
|
279
|
+
throw new TaigaError(`The file has ${lines.length} lines.`);
|
|
280
|
+
const last = render(from, to);
|
|
281
|
+
const end = Math.min(to, lines.length);
|
|
282
|
+
return [
|
|
283
|
+
`${head}\nLines ${from} to ${last}.`,
|
|
284
|
+
out.lines.join('\n'),
|
|
285
|
+
last < end ? `Continue with \`rows\`: "${last + 1}-${end}".` : undefined,
|
|
286
|
+
]
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
.join('\n\n');
|
|
289
|
+
}
|
|
290
|
+
const total = lines.reduce((sum, line) => sum + clip(line).length + 1, 0);
|
|
291
|
+
if (total <= BUDGET)
|
|
292
|
+
return `${head}\n\n${lines.map(clip).join('\n')}`;
|
|
293
|
+
render(1, PREVIEW_LINES);
|
|
294
|
+
return [
|
|
295
|
+
`${head}\nToo large to show at once (${total} characters); these are the first ${out.lines.length} lines, numbered.`,
|
|
296
|
+
out.lines.join('\n'),
|
|
297
|
+
'To read more: `rows` for a range of lines (e.g. "61-400"), or `find` with words to get only the lines that contain them.',
|
|
298
|
+
].join('\n\n');
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=file-view.js.map
|