@xfey/tutti 0.1.113 → 0.1.114
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/dist/server-shell/cli/relay-registration.js +1 -0
- package/dist/server-shell/http/routes/agent-context-skills-routes.js +2 -1
- package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +23 -0
- package/dist/server-shell/http/routes/project-api/openapi-skills-routes.js +9 -1
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +22 -0
- package/dist/server-shell/http/routes/project-api/project-timeline-projection.js +27 -33
- package/dist/server-shell/http/routes/project-api/skills-helpers.js +9 -0
- package/dist/server-shell/http/routes/project-api/skills-routes.js +8 -4
- package/dist/server-shell/http/validation.js +1 -0
- package/dist/skills/constants.d.ts +5 -0
- package/dist/skills/constants.js +6 -0
- package/dist/skills/errors.d.ts +1 -1
- package/dist/skills/file-reader.d.ts +14 -0
- package/dist/skills/file-reader.js +123 -0
- package/dist/skills/index.d.ts +5 -2
- package/dist/skills/index.js +36 -41
- package/dist/skills/metadata.d.ts +2 -0
- package/dist/skills/metadata.js +10 -1
- package/dist/skills/zip-import.js +2 -2
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +2 -2
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BwdH6Iss.js → homepage-motion-scene-DwIRFWMo.js} +1 -1
- package/web/assets/{index-DvfZ57Nl.css → index-C9grZQCJ.css} +1 -1
- package/web/assets/index-Igpofaw2.js +69 -0
- package/web/index.html +2 -2
- package/web/assets/index-B29RuNsd.js +0 -69
|
@@ -39,6 +39,7 @@ export async function registerHostWithRelay(options) {
|
|
|
39
39
|
integration_source_ingestion_v1: true,
|
|
40
40
|
integration_development_bot_v1: true,
|
|
41
41
|
integration_clarification_v1: true,
|
|
42
|
+
feishu_supporting_reads_v1: true,
|
|
42
43
|
integration_external_references_v1: true,
|
|
43
44
|
integration_attachment_ingestion_v1: true,
|
|
44
45
|
integration_managed_reference_ingestion_v1: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactText } from "@tutti/shared/utils";
|
|
2
|
-
import { listUserSkills, readUserSkill, SkillServiceError
|
|
2
|
+
import { listUserSkills, readUserSkill, SkillServiceError } from "../../../skills/index.js";
|
|
3
3
|
import { authorizeAgentContextRequest } from "./agent-context-auth.js";
|
|
4
4
|
import { AgentContextBadRequestError } from "./agent-context-errors.js";
|
|
5
5
|
import { boundedLimit, pageByOffset, queryString, truncateText } from "./agent-context-query.js";
|
|
@@ -23,6 +23,7 @@ function handleSkillReadError(error, skillName) {
|
|
|
23
23
|
case "validation_failed":
|
|
24
24
|
return skillUnavailable({ skillName, reason: "invalid" });
|
|
25
25
|
case "filesystem_error":
|
|
26
|
+
case "read_limit_exceeded":
|
|
26
27
|
return skillUnavailable({ skillName, reason: "unavailable" });
|
|
27
28
|
case "invalid_zip":
|
|
28
29
|
return skillUnavailable({ skillName, reason: "invalid" });
|
|
@@ -17,6 +17,17 @@ export declare const HOST_PROJECT_SKILLS_OPENAPI_ROUTES: ({
|
|
|
17
17
|
}>>;
|
|
18
18
|
}>;
|
|
19
19
|
};
|
|
20
|
+
413: {
|
|
21
|
+
description: string;
|
|
22
|
+
schema: import("@sinclair/typebox").TObject<{
|
|
23
|
+
error: import("@sinclair/typebox").TObject<{
|
|
24
|
+
code: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"bad_request">, import("@sinclair/typebox").TLiteral<"unauthorized">, import("@sinclair/typebox").TLiteral<"forbidden">, import("@sinclair/typebox").TLiteral<"not_found">, import("@sinclair/typebox").TLiteral<"conflict">, import("@sinclair/typebox").TLiteral<"validation_failed">, import("@sinclair/typebox").TLiteral<"provider_not_configured">, import("@sinclair/typebox").TLiteral<"provider_auth_invalid">, import("@sinclair/typebox").TLiteral<"provider_quota_or_billing_required">, import("@sinclair/typebox").TLiteral<"provider_rate_limited">, import("@sinclair/typebox").TLiteral<"provider_model_unavailable">, import("@sinclair/typebox").TLiteral<"provider_network_error">, import("@sinclair/typebox").TLiteral<"relay_session_invalid">, import("@sinclair/typebox").TLiteral<"project_membership_required">, import("@sinclair/typebox").TLiteral<"command_replay_mismatch">, import("@sinclair/typebox").TLiteral<"repo_snapshot_unavailable">, import("@sinclair/typebox").TLiteral<"host_unavailable">, import("@sinclair/typebox").TLiteral<"skill_read_limit_exceeded">, import("@sinclair/typebox").TLiteral<"internal_error">]>;
|
|
25
|
+
message: import("@sinclair/typebox").TString;
|
|
26
|
+
retryable: import("@sinclair/typebox").TBoolean;
|
|
27
|
+
details: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
20
31
|
};
|
|
21
32
|
parameters?: never;
|
|
22
33
|
requestBody?: never;
|
|
@@ -41,6 +52,17 @@ export declare const HOST_PROJECT_SKILLS_OPENAPI_ROUTES: ({
|
|
|
41
52
|
}>;
|
|
42
53
|
}>;
|
|
43
54
|
};
|
|
55
|
+
413: {
|
|
56
|
+
description: string;
|
|
57
|
+
schema: import("@sinclair/typebox").TObject<{
|
|
58
|
+
error: import("@sinclair/typebox").TObject<{
|
|
59
|
+
code: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"bad_request">, import("@sinclair/typebox").TLiteral<"unauthorized">, import("@sinclair/typebox").TLiteral<"forbidden">, import("@sinclair/typebox").TLiteral<"not_found">, import("@sinclair/typebox").TLiteral<"conflict">, import("@sinclair/typebox").TLiteral<"validation_failed">, import("@sinclair/typebox").TLiteral<"provider_not_configured">, import("@sinclair/typebox").TLiteral<"provider_auth_invalid">, import("@sinclair/typebox").TLiteral<"provider_quota_or_billing_required">, import("@sinclair/typebox").TLiteral<"provider_rate_limited">, import("@sinclair/typebox").TLiteral<"provider_model_unavailable">, import("@sinclair/typebox").TLiteral<"provider_network_error">, import("@sinclair/typebox").TLiteral<"relay_session_invalid">, import("@sinclair/typebox").TLiteral<"project_membership_required">, import("@sinclair/typebox").TLiteral<"command_replay_mismatch">, import("@sinclair/typebox").TLiteral<"repo_snapshot_unavailable">, import("@sinclair/typebox").TLiteral<"host_unavailable">, import("@sinclair/typebox").TLiteral<"skill_read_limit_exceeded">, import("@sinclair/typebox").TLiteral<"internal_error">]>;
|
|
60
|
+
message: import("@sinclair/typebox").TString;
|
|
61
|
+
retryable: import("@sinclair/typebox").TBoolean;
|
|
62
|
+
details: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
|
|
63
|
+
}>;
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
44
66
|
};
|
|
45
67
|
requestBody?: never;
|
|
46
68
|
} | {
|
|
@@ -54,6 +76,7 @@ export declare const HOST_PROJECT_SKILLS_OPENAPI_ROUTES: ({
|
|
|
54
76
|
description: string;
|
|
55
77
|
schema: import("@sinclair/typebox").TSchema;
|
|
56
78
|
};
|
|
79
|
+
413?: never;
|
|
57
80
|
};
|
|
58
81
|
parameters?: never;
|
|
59
82
|
})[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeleteSkillBodySchema, DeleteSkillResponseSchema, GetSkillResponseSchema, GetSkillsResponseSchema, ImportSkillZipBodySchema, ImportSkillZipResponseSchema, } from "@tutti/shared/schemas/api";
|
|
1
|
+
import { ApiErrorResponseSchema, DeleteSkillBodySchema, DeleteSkillResponseSchema, GetSkillResponseSchema, GetSkillsResponseSchema, ImportSkillZipBodySchema, ImportSkillZipResponseSchema, } from "@tutti/shared/schemas/api";
|
|
2
2
|
import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
|
|
3
3
|
import { SkillNamePathParameter } from "./openapi-path-parameters.js";
|
|
4
4
|
export const HOST_PROJECT_SKILLS_OPENAPI_ROUTES = [
|
|
@@ -9,6 +9,10 @@ export const HOST_PROJECT_SKILLS_OPENAPI_ROUTES = [
|
|
|
9
9
|
tags: ["skills"],
|
|
10
10
|
responses: {
|
|
11
11
|
200: { description: "User skills projection.", schema: GetSkillsResponseSchema },
|
|
12
|
+
413: {
|
|
13
|
+
description: "Feishu preview list budget exceeded; no partial list.",
|
|
14
|
+
schema: ApiErrorResponseSchema,
|
|
15
|
+
},
|
|
12
16
|
},
|
|
13
17
|
},
|
|
14
18
|
{
|
|
@@ -19,6 +23,10 @@ export const HOST_PROJECT_SKILLS_OPENAPI_ROUTES = [
|
|
|
19
23
|
parameters: [SkillNamePathParameter],
|
|
20
24
|
responses: {
|
|
21
25
|
200: { description: "User skill detail projection.", schema: GetSkillResponseSchema },
|
|
26
|
+
413: {
|
|
27
|
+
description: "Skill read budget exceeded; no truncated skill.",
|
|
28
|
+
schema: ApiErrorResponseSchema,
|
|
29
|
+
},
|
|
22
30
|
},
|
|
23
31
|
},
|
|
24
32
|
{
|
|
@@ -885,6 +885,17 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
|
|
|
885
885
|
}>>;
|
|
886
886
|
}>;
|
|
887
887
|
};
|
|
888
|
+
413: {
|
|
889
|
+
description: string;
|
|
890
|
+
schema: import("@sinclair/typebox").TObject<{
|
|
891
|
+
error: import("@sinclair/typebox").TObject<{
|
|
892
|
+
code: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"bad_request">, import("@sinclair/typebox").TLiteral<"unauthorized">, import("@sinclair/typebox").TLiteral<"forbidden">, import("@sinclair/typebox").TLiteral<"not_found">, import("@sinclair/typebox").TLiteral<"conflict">, import("@sinclair/typebox").TLiteral<"validation_failed">, import("@sinclair/typebox").TLiteral<"provider_not_configured">, import("@sinclair/typebox").TLiteral<"provider_auth_invalid">, import("@sinclair/typebox").TLiteral<"provider_quota_or_billing_required">, import("@sinclair/typebox").TLiteral<"provider_rate_limited">, import("@sinclair/typebox").TLiteral<"provider_model_unavailable">, import("@sinclair/typebox").TLiteral<"provider_network_error">, import("@sinclair/typebox").TLiteral<"relay_session_invalid">, import("@sinclair/typebox").TLiteral<"project_membership_required">, import("@sinclair/typebox").TLiteral<"command_replay_mismatch">, import("@sinclair/typebox").TLiteral<"repo_snapshot_unavailable">, import("@sinclair/typebox").TLiteral<"host_unavailable">, import("@sinclair/typebox").TLiteral<"skill_read_limit_exceeded">, import("@sinclair/typebox").TLiteral<"internal_error">]>;
|
|
893
|
+
message: import("@sinclair/typebox").TString;
|
|
894
|
+
retryable: import("@sinclair/typebox").TBoolean;
|
|
895
|
+
details: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
|
|
896
|
+
}>;
|
|
897
|
+
}>;
|
|
898
|
+
};
|
|
888
899
|
};
|
|
889
900
|
parameters?: never;
|
|
890
901
|
requestBody?: never;
|
|
@@ -909,6 +920,17 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
|
|
|
909
920
|
}>;
|
|
910
921
|
}>;
|
|
911
922
|
};
|
|
923
|
+
413: {
|
|
924
|
+
description: string;
|
|
925
|
+
schema: import("@sinclair/typebox").TObject<{
|
|
926
|
+
error: import("@sinclair/typebox").TObject<{
|
|
927
|
+
code: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"bad_request">, import("@sinclair/typebox").TLiteral<"unauthorized">, import("@sinclair/typebox").TLiteral<"forbidden">, import("@sinclair/typebox").TLiteral<"not_found">, import("@sinclair/typebox").TLiteral<"conflict">, import("@sinclair/typebox").TLiteral<"validation_failed">, import("@sinclair/typebox").TLiteral<"provider_not_configured">, import("@sinclair/typebox").TLiteral<"provider_auth_invalid">, import("@sinclair/typebox").TLiteral<"provider_quota_or_billing_required">, import("@sinclair/typebox").TLiteral<"provider_rate_limited">, import("@sinclair/typebox").TLiteral<"provider_model_unavailable">, import("@sinclair/typebox").TLiteral<"provider_network_error">, import("@sinclair/typebox").TLiteral<"relay_session_invalid">, import("@sinclair/typebox").TLiteral<"project_membership_required">, import("@sinclair/typebox").TLiteral<"command_replay_mismatch">, import("@sinclair/typebox").TLiteral<"repo_snapshot_unavailable">, import("@sinclair/typebox").TLiteral<"host_unavailable">, import("@sinclair/typebox").TLiteral<"skill_read_limit_exceeded">, import("@sinclair/typebox").TLiteral<"internal_error">]>;
|
|
928
|
+
message: import("@sinclair/typebox").TString;
|
|
929
|
+
retryable: import("@sinclair/typebox").TBoolean;
|
|
930
|
+
details: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
|
|
931
|
+
}>;
|
|
932
|
+
}>;
|
|
933
|
+
};
|
|
912
934
|
};
|
|
913
935
|
requestBody?: never;
|
|
914
936
|
} | {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { clarificationAnswerRole } from "@tutti/shared/utils";
|
|
2
|
+
import { readSubmittedClarificationMessages } from "../../../../collaboration-state/index.js";
|
|
1
3
|
import { readProviderUsageBreakdownForAnchors, readProviderUsageModelSummariesForAnchors, readProviderUsageProjection, readProviderUsageSourceSummariesForAnchors, } from "../../../../provider-usage/index.js";
|
|
2
4
|
import { runGitRaw } from "../../../../workspace-ops/git.js";
|
|
3
5
|
const TIMELINE_LIMIT = 50;
|
|
@@ -178,34 +180,24 @@ function clarificationChatLink(messageId) {
|
|
|
178
180
|
message_id: messageId,
|
|
179
181
|
};
|
|
180
182
|
}
|
|
181
|
-
function readClarificationAnswerSnippets(db,
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`);
|
|
199
|
-
const answers = submittedRefs.flatMap((messageId) => {
|
|
200
|
-
const row = readMessage.get(messageId);
|
|
201
|
-
return row === undefined ? [] : [row];
|
|
183
|
+
function readClarificationAnswerSnippets(db, roundId) {
|
|
184
|
+
const answers = readSubmittedClarificationMessages(db, roundId).filter((message) => clarificationAnswerRole(message) !== null);
|
|
185
|
+
const visibleAnswers = answers.slice(0, TRACE_SOURCE_SNIPPET_LIMIT).map((answer) => {
|
|
186
|
+
let author = authorLabel({
|
|
187
|
+
author_kind: answer.author.kind,
|
|
188
|
+
author_display_name: answer.author.display_name ?? null,
|
|
189
|
+
});
|
|
190
|
+
if (clarificationAnswerRole(answer) === "operator") {
|
|
191
|
+
const displayName = answer.author.display_name?.trim();
|
|
192
|
+
author = displayName ? `${displayName} (operator)` : "Development operator";
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
message_id: answer.id,
|
|
196
|
+
author,
|
|
197
|
+
body: truncateSnippetBody(answer.body),
|
|
198
|
+
created_at: answer.created_at,
|
|
199
|
+
};
|
|
202
200
|
});
|
|
203
|
-
const visibleAnswers = answers.slice(0, TRACE_SOURCE_SNIPPET_LIMIT).map((answer) => ({
|
|
204
|
-
message_id: answer.message_id,
|
|
205
|
-
author: authorLabel(answer),
|
|
206
|
-
body: truncateSnippetBody(answer.body),
|
|
207
|
-
created_at: answer.created_at,
|
|
208
|
-
}));
|
|
209
201
|
return {
|
|
210
202
|
answer_count: answers.length,
|
|
211
203
|
answers: visibleAnswers,
|
|
@@ -213,7 +205,7 @@ function readClarificationAnswerSnippets(db, submittedRefsJson) {
|
|
|
213
205
|
}
|
|
214
206
|
function clarificationTraceFromRow(db, row) {
|
|
215
207
|
const payload = parseJsonObject(row.request_payload_json);
|
|
216
|
-
const answers = readClarificationAnswerSnippets(db, row.
|
|
208
|
+
const answers = readClarificationAnswerSnippets(db, row.round_id);
|
|
217
209
|
const visibleAnswers = answers.answers;
|
|
218
210
|
const title = stringValue(payload, "title");
|
|
219
211
|
const summary = stringValue(payload, "summary");
|
|
@@ -247,7 +239,6 @@ function readLatestTaskCompileClarificationTrace(db, workflowRef) {
|
|
|
247
239
|
clarification_rounds.status AS round_status,
|
|
248
240
|
clarification_rounds.request_payload_json,
|
|
249
241
|
clarification_rounds.card_message_id,
|
|
250
|
-
clarification_rounds.submitted_message_refs_json,
|
|
251
242
|
clarification_rounds.submitted_at,
|
|
252
243
|
clarification_rounds.updated_at
|
|
253
244
|
FROM clarification_rounds
|
|
@@ -273,7 +264,6 @@ function readLatestClarificationTraceByThread(db, threadId) {
|
|
|
273
264
|
clarification_rounds.status AS round_status,
|
|
274
265
|
clarification_rounds.request_payload_json,
|
|
275
266
|
clarification_rounds.card_message_id,
|
|
276
|
-
clarification_rounds.submitted_message_refs_json,
|
|
277
267
|
clarification_rounds.submitted_at,
|
|
278
268
|
clarification_rounds.updated_at
|
|
279
269
|
FROM clarification_threads
|
|
@@ -302,7 +292,6 @@ function readLatestResolvedTaskClarificationTrace(db, taskId, throughCreatedAt)
|
|
|
302
292
|
clarification_rounds.status AS round_status,
|
|
303
293
|
clarification_rounds.request_payload_json,
|
|
304
294
|
clarification_rounds.card_message_id,
|
|
305
|
-
clarification_rounds.submitted_message_refs_json,
|
|
306
295
|
clarification_rounds.submitted_at,
|
|
307
296
|
clarification_rounds.updated_at
|
|
308
297
|
FROM clarification_rounds
|
|
@@ -533,7 +522,10 @@ function readPromotedCommitChanges(workspaceRoot, commitOid) {
|
|
|
533
522
|
if (result.status !== 0) {
|
|
534
523
|
return undefined;
|
|
535
524
|
}
|
|
536
|
-
const fields = result.stdout
|
|
525
|
+
const fields = result.stdout
|
|
526
|
+
.toString("utf8")
|
|
527
|
+
.split("\0")
|
|
528
|
+
.filter((field) => field !== "");
|
|
537
529
|
const changes = [];
|
|
538
530
|
for (let index = 0; index < fields.length;) {
|
|
539
531
|
const status = fields[index];
|
|
@@ -843,7 +835,9 @@ export function readProjectTimelineProjection(input) {
|
|
|
843
835
|
].sort(compareTimelineItemsByTimeDesc);
|
|
844
836
|
return {
|
|
845
837
|
generated_at: now().toISOString(),
|
|
846
|
-
usage_overview: input.db === undefined
|
|
838
|
+
usage_overview: input.db === undefined
|
|
839
|
+
? emptyUsageOverview()
|
|
840
|
+
: readProviderUsageProjection(input.db, { now }),
|
|
847
841
|
items: items.slice(0, TIMELINE_LIMIT),
|
|
848
842
|
};
|
|
849
843
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { getProjectUserSkillsRoot, SkillServiceError } from "../../../../skills/index.js";
|
|
2
2
|
import { HostProjectBadRequestError, HostProjectNotFoundError, HostProjectStoreUnavailableError, HostProjectValidationError, } from "./errors.js";
|
|
3
|
+
class HostSkillReadLimitError extends Error {
|
|
4
|
+
statusCode = 413;
|
|
5
|
+
code = "skill_read_limit_exceeded";
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Skill read exceeds the supported limit");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
3
10
|
export function skillsChangedInvalidates() {
|
|
4
11
|
return [{ kind: "skills" }, { kind: "read_state" }, { kind: "bootstrap" }];
|
|
5
12
|
}
|
|
@@ -24,6 +31,8 @@ function toHostProjectSkillError(error) {
|
|
|
24
31
|
return new HostProjectValidationError(error.message);
|
|
25
32
|
case "filesystem_error":
|
|
26
33
|
return new HostProjectStoreUnavailableError();
|
|
34
|
+
case "read_limit_exceeded":
|
|
35
|
+
return new HostSkillReadLimitError();
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
export async function withAsyncSkillServiceErrorMapping(fn) {
|
|
@@ -14,9 +14,11 @@ export function registerSkillRoutes(app, { options, workspaceEvents, }) {
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
}, (request) => {
|
|
17
|
-
requireHostProjectSession(request, options);
|
|
17
|
+
const session = requireHostProjectSession(request, options);
|
|
18
18
|
return withSkillServiceErrorMapping(() => ({
|
|
19
|
-
items: listUserSkills(requireProjectUserSkillsRoot(options)
|
|
19
|
+
items: listUserSkills(requireProjectUserSkillsRoot(options), {
|
|
20
|
+
mode: session.client_session.transport === "feishu_platform_session" ? "preview" : "full",
|
|
21
|
+
}),
|
|
20
22
|
}));
|
|
21
23
|
});
|
|
22
24
|
app.get(`${HOST_PROJECT_API_BASE_PATH}/skills/:skillName`, {
|
|
@@ -26,9 +28,11 @@ export function registerSkillRoutes(app, { options, workspaceEvents, }) {
|
|
|
26
28
|
},
|
|
27
29
|
},
|
|
28
30
|
}, (request) => {
|
|
29
|
-
requireHostProjectSession(request, options);
|
|
31
|
+
const session = requireHostProjectSession(request, options);
|
|
30
32
|
return withSkillServiceErrorMapping(() => ({
|
|
31
|
-
skill: readUserSkill(requireProjectUserSkillsRoot(options), request.params.skillName
|
|
33
|
+
skill: readUserSkill(requireProjectUserSkillsRoot(options), request.params.skillName, {
|
|
34
|
+
mode: session.client_session.transport === "feishu_platform_session" ? "preview" : "full",
|
|
35
|
+
}),
|
|
32
36
|
}));
|
|
33
37
|
});
|
|
34
38
|
app.post(`${HOST_PROJECT_API_BASE_PATH}/skills/import`, {
|
|
@@ -5,4 +5,9 @@ export declare const MAX_SKILL_TOTAL_UNCOMPRESSED_BYTES: number;
|
|
|
5
5
|
export declare const MAX_SKILL_FILE_BYTES: number;
|
|
6
6
|
export declare const MAX_SKILL_FILE_COUNT = 1000;
|
|
7
7
|
export declare const MAX_SKILL_DIRECTORY_DEPTH = 24;
|
|
8
|
+
export declare const MAX_SKILL_PREVIEW_BYTES: number;
|
|
9
|
+
export declare const MAX_SKILL_PREVIEW_METADATA_BYTES: number;
|
|
10
|
+
export declare const MAX_SKILL_PREVIEW_SCAN_ENTRIES = 512;
|
|
11
|
+
export declare const MAX_SKILL_PREVIEW_ITEMS = 256;
|
|
12
|
+
export declare const MAX_SKILL_PREVIEW_LIST_BYTES: number;
|
|
8
13
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/skills/constants.js
CHANGED
|
@@ -6,4 +6,10 @@ export const MAX_SKILL_TOTAL_UNCOMPRESSED_BYTES = 200 * 1024 * 1024;
|
|
|
6
6
|
export const MAX_SKILL_FILE_BYTES = 50 * 1024 * 1024;
|
|
7
7
|
export const MAX_SKILL_FILE_COUNT = 1_000;
|
|
8
8
|
export const MAX_SKILL_DIRECTORY_DEPTH = 24;
|
|
9
|
+
// Browser preview budgets are independent of the full skill used by the runtime.
|
|
10
|
+
export const MAX_SKILL_PREVIEW_BYTES = 200 * 1024;
|
|
11
|
+
export const MAX_SKILL_PREVIEW_METADATA_BYTES = 32 * 1024;
|
|
12
|
+
export const MAX_SKILL_PREVIEW_SCAN_ENTRIES = 512;
|
|
13
|
+
export const MAX_SKILL_PREVIEW_ITEMS = 256;
|
|
14
|
+
export const MAX_SKILL_PREVIEW_LIST_BYTES = 512 * 1024;
|
|
9
15
|
//# sourceMappingURL=constants.js.map
|
package/dist/skills/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SkillServiceErrorCode = "invalid_skill_name" | "skill_not_found" | "invalid_zip" | "validation_failed" | "filesystem_error";
|
|
1
|
+
export type SkillServiceErrorCode = "invalid_skill_name" | "skill_not_found" | "invalid_zip" | "validation_failed" | "read_limit_exceeded" | "filesystem_error";
|
|
2
2
|
export declare class SkillServiceError extends Error {
|
|
3
3
|
readonly code: SkillServiceErrorCode;
|
|
4
4
|
constructor(code: SkillServiceErrorCode, message: string);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function skillReadLimitExceeded(): never;
|
|
2
|
+
/** Read from one checked descriptor; never follow a SKILL.md symlink or block on a FIFO. */
|
|
3
|
+
export declare function readSkillFile(input: {
|
|
4
|
+
root: string;
|
|
5
|
+
name: string;
|
|
6
|
+
maxBytes: number;
|
|
7
|
+
metadataOnly?: boolean;
|
|
8
|
+
}): {
|
|
9
|
+
markdown: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
};
|
|
12
|
+
/** Bound enumeration before collecting names; a limit never returns a partial list. */
|
|
13
|
+
export declare function listSkillDirectories(root: string, maxEntries?: number): string[];
|
|
14
|
+
//# sourceMappingURL=file-reader.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync, realpathSync, } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { SKILL_MD_FILE_NAME } from "./constants.js";
|
|
4
|
+
import { SkillServiceError } from "./errors.js";
|
|
5
|
+
import { decodeSkillMarkdown, isValidSkillName } from "./metadata.js";
|
|
6
|
+
function unsafePath() {
|
|
7
|
+
throw new SkillServiceError("validation_failed", "Skill path is not a regular project skill");
|
|
8
|
+
}
|
|
9
|
+
export function skillReadLimitExceeded() {
|
|
10
|
+
throw new SkillServiceError("read_limit_exceeded", "Skill read exceeds the supported limit");
|
|
11
|
+
}
|
|
12
|
+
function mapReadError(error) {
|
|
13
|
+
if (error instanceof SkillServiceError)
|
|
14
|
+
throw error;
|
|
15
|
+
const code = error.code;
|
|
16
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
17
|
+
throw new SkillServiceError("skill_not_found", "Skill does not exist");
|
|
18
|
+
}
|
|
19
|
+
throw new SkillServiceError("filesystem_error", "Skill could not be read");
|
|
20
|
+
}
|
|
21
|
+
function realDirectory(path) {
|
|
22
|
+
if (!lstatSync(path).isDirectory())
|
|
23
|
+
unsafePath();
|
|
24
|
+
return realpathSync(path);
|
|
25
|
+
}
|
|
26
|
+
function resolveSkillFile(root, name) {
|
|
27
|
+
const realRoot = realDirectory(resolve(root));
|
|
28
|
+
const directory = join(realRoot, name);
|
|
29
|
+
if (realDirectory(directory) !== directory)
|
|
30
|
+
unsafePath();
|
|
31
|
+
const file = join(directory, SKILL_MD_FILE_NAME);
|
|
32
|
+
if (!lstatSync(file).isFile() || realpathSync(file) !== file)
|
|
33
|
+
unsafePath();
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
function sameFile(left, right) {
|
|
37
|
+
return (left.dev === right.dev &&
|
|
38
|
+
left.ino === right.ino &&
|
|
39
|
+
left.size === right.size &&
|
|
40
|
+
left.mtimeMs === right.mtimeMs &&
|
|
41
|
+
left.ctimeMs === right.ctimeMs);
|
|
42
|
+
}
|
|
43
|
+
/** Read from one checked descriptor; never follow a SKILL.md symlink or block on a FIFO. */
|
|
44
|
+
export function readSkillFile(input) {
|
|
45
|
+
if (!isValidSkillName(input.name)) {
|
|
46
|
+
throw new SkillServiceError("invalid_skill_name", "Skill name is invalid");
|
|
47
|
+
}
|
|
48
|
+
let fd;
|
|
49
|
+
try {
|
|
50
|
+
const file = resolveSkillFile(input.root, input.name);
|
|
51
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
52
|
+
const before = fstatSync(fd);
|
|
53
|
+
if (!before.isFile() || !sameFile(before, lstatSync(file)))
|
|
54
|
+
unsafePath();
|
|
55
|
+
if (!input.metadataOnly && before.size > input.maxBytes)
|
|
56
|
+
skillReadLimitExceeded();
|
|
57
|
+
const buffer = Buffer.alloc(Math.min(before.size, input.maxBytes) + 1);
|
|
58
|
+
let length = 0;
|
|
59
|
+
while (length < buffer.length) {
|
|
60
|
+
const read = readSync(fd, buffer, length, buffer.length - length, length);
|
|
61
|
+
if (read === 0)
|
|
62
|
+
break;
|
|
63
|
+
length += read;
|
|
64
|
+
}
|
|
65
|
+
if (!sameFile(before, fstatSync(fd)) ||
|
|
66
|
+
resolveSkillFile(input.root, input.name) !== file ||
|
|
67
|
+
!sameFile(before, lstatSync(file))) {
|
|
68
|
+
throw new SkillServiceError("filesystem_error", "Skill changed while being read");
|
|
69
|
+
}
|
|
70
|
+
const bytes = buffer.subarray(0, length);
|
|
71
|
+
const text = decodeSkillMarkdown(bytes, input.metadataOnly === true && before.size > length);
|
|
72
|
+
if (input.metadataOnly) {
|
|
73
|
+
// Frontmatter syntax stays owned by metadata.ts. Only its bounded prefix is decoded.
|
|
74
|
+
const prefix = text.replace(/\r\n/gu, "\n");
|
|
75
|
+
const end = prefix.startsWith("---\n") ? prefix.indexOf("\n---", 4) : -1;
|
|
76
|
+
if (end !== -1) {
|
|
77
|
+
const metadata = prefix.slice(0, end + 4);
|
|
78
|
+
if (Buffer.byteLength(metadata) > input.maxBytes)
|
|
79
|
+
skillReadLimitExceeded();
|
|
80
|
+
return { markdown: metadata, updatedAt: before.mtime.toISOString() };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (length > input.maxBytes)
|
|
84
|
+
skillReadLimitExceeded();
|
|
85
|
+
return { markdown: text, updatedAt: before.mtime.toISOString() };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return mapReadError(error);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (fd !== undefined)
|
|
92
|
+
closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Bound enumeration before collecting names; a limit never returns a partial list. */
|
|
96
|
+
export function listSkillDirectories(root, maxEntries = Infinity) {
|
|
97
|
+
try {
|
|
98
|
+
const realRoot = realDirectory(resolve(root));
|
|
99
|
+
const directory = opendirSync(realRoot);
|
|
100
|
+
const names = [];
|
|
101
|
+
let count = 0;
|
|
102
|
+
try {
|
|
103
|
+
for (let entry = directory.readSync(); entry !== null; entry = directory.readSync()) {
|
|
104
|
+
if (++count > maxEntries)
|
|
105
|
+
skillReadLimitExceeded();
|
|
106
|
+
if (entry.isDirectory() && isValidSkillName(entry.name))
|
|
107
|
+
names.push(entry.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
directory.closeSync();
|
|
112
|
+
}
|
|
113
|
+
if (realDirectory(resolve(root)) !== realRoot)
|
|
114
|
+
unsafePath();
|
|
115
|
+
return names.sort();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === "ENOENT")
|
|
119
|
+
return [];
|
|
120
|
+
return mapReadError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=file-reader.js.map
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -26,9 +26,12 @@ export type UserSkillImportResult = {
|
|
|
26
26
|
disposition: UserSkillImportDisposition;
|
|
27
27
|
skill: SkillDetailProjection;
|
|
28
28
|
};
|
|
29
|
+
export type UserSkillReadOptions = {
|
|
30
|
+
mode?: "full" | "preview";
|
|
31
|
+
};
|
|
29
32
|
export declare function getProjectUserSkillsRoot(tuttiHome: string, projectId: ProjectId): string;
|
|
30
|
-
export declare function listUserSkills(userSkillsRoot: string): SkillProjection[];
|
|
31
|
-
export declare function readUserSkill(userSkillsRoot: string, skillName: string): SkillDetailProjection;
|
|
33
|
+
export declare function listUserSkills(userSkillsRoot: string, options?: UserSkillReadOptions): SkillProjection[];
|
|
34
|
+
export declare function readUserSkill(userSkillsRoot: string, skillName: string, options?: UserSkillReadOptions): SkillDetailProjection;
|
|
32
35
|
export declare function importUserSkillZip(options: {
|
|
33
36
|
userSkillsRoot: string;
|
|
34
37
|
zipBuffer: Buffer;
|
package/dist/skills/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdtempSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { MAX_SKILL_FILE_BYTES, MAX_SKILL_PREVIEW_BYTES, MAX_SKILL_PREVIEW_METADATA_BYTES, MAX_SKILL_PREVIEW_SCAN_ENTRIES, MAX_SKILL_PREVIEW_ITEMS, MAX_SKILL_PREVIEW_LIST_BYTES, USER_SKILLS_DIRECTORY_NAME, } from "./constants.js";
|
|
5
5
|
import { SkillServiceError } from "./errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { listSkillDirectories, readSkillFile, skillReadLimitExceeded } from "./file-reader.js";
|
|
7
|
+
import { isValidSkillName, parseSkillMarkdown } from "./metadata.js";
|
|
7
8
|
import { prepareUserSkillZipImport, writeCandidateFiles, } from "./zip-import.js";
|
|
8
9
|
export { MAX_SKILL_DIRECTORY_DEPTH, MAX_SKILL_FILE_BYTES, MAX_SKILL_FILE_COUNT, MAX_SKILL_TOTAL_UNCOMPRESSED_BYTES, MAX_SKILL_ZIP_BYTES, SKILL_MD_FILE_NAME, USER_SKILLS_DIRECTORY_NAME, } from "./constants.js";
|
|
9
10
|
export { SkillServiceError } from "./errors.js";
|
|
@@ -30,17 +31,6 @@ function skillDirectory(userSkillsRoot, skillName) {
|
|
|
30
31
|
ensureInside(userSkillsRoot, directory);
|
|
31
32
|
return directory;
|
|
32
33
|
}
|
|
33
|
-
function readSkillMarkdown(filePath) {
|
|
34
|
-
let skillMd;
|
|
35
|
-
try {
|
|
36
|
-
skillMd = readFileSync(filePath, "utf8");
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new SkillServiceError("skill_not_found", "Skill does not exist");
|
|
40
|
-
}
|
|
41
|
-
const metadata = parseSkillMarkdown(skillMd);
|
|
42
|
-
return { ...metadata, skill_md: skillMd };
|
|
43
|
-
}
|
|
44
34
|
function toSkillProjection(options) {
|
|
45
35
|
return {
|
|
46
36
|
name: options.metadata.name,
|
|
@@ -53,53 +43,58 @@ function toSkillProjection(options) {
|
|
|
53
43
|
...(options.updatedAt === undefined ? {} : { updated_at: options.updatedAt }),
|
|
54
44
|
};
|
|
55
45
|
}
|
|
56
|
-
function readSkillProjection(
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
function readSkillProjection(root, name, preview) {
|
|
47
|
+
const file = readSkillFile({
|
|
48
|
+
root,
|
|
49
|
+
name,
|
|
50
|
+
maxBytes: preview ? MAX_SKILL_PREVIEW_METADATA_BYTES : MAX_SKILL_FILE_BYTES,
|
|
51
|
+
metadataOnly: preview,
|
|
52
|
+
});
|
|
60
53
|
return toSkillProjection({
|
|
61
|
-
metadata:
|
|
62
|
-
updatedAt:
|
|
54
|
+
metadata: parseSkillMarkdown(file.markdown),
|
|
55
|
+
updatedAt: file.updatedAt,
|
|
63
56
|
});
|
|
64
57
|
}
|
|
65
|
-
export function listUserSkills(userSkillsRoot) {
|
|
66
|
-
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
58
|
+
export function listUserSkills(userSkillsRoot, options = {}) {
|
|
59
|
+
const preview = options.mode === "preview";
|
|
69
60
|
const items = [];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
61
|
+
let responseBytes = Buffer.byteLength('{"items":[]}');
|
|
62
|
+
for (const name of listSkillDirectories(userSkillsRoot, preview ? MAX_SKILL_PREVIEW_SCAN_ENTRIES : undefined)) {
|
|
74
63
|
try {
|
|
75
|
-
const projection = readSkillProjection(
|
|
76
|
-
if (projection.name ===
|
|
64
|
+
const projection = readSkillProjection(userSkillsRoot, name, preview);
|
|
65
|
+
if (projection.name === name) {
|
|
66
|
+
responseBytes += Buffer.byteLength(JSON.stringify(projection)) + 1;
|
|
67
|
+
if (preview &&
|
|
68
|
+
(items.length >= MAX_SKILL_PREVIEW_ITEMS || responseBytes > MAX_SKILL_PREVIEW_LIST_BYTES)) {
|
|
69
|
+
skillReadLimitExceeded();
|
|
70
|
+
}
|
|
77
71
|
items.push(projection);
|
|
78
72
|
}
|
|
79
73
|
}
|
|
80
|
-
catch {
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (preview && error instanceof SkillServiceError && error.code === "read_limit_exceeded")
|
|
76
|
+
throw error;
|
|
81
77
|
// Keep the management surface available even if a stale local directory is malformed.
|
|
82
78
|
}
|
|
83
79
|
}
|
|
84
80
|
return items.sort((left, right) => left.name.localeCompare(right.name));
|
|
85
81
|
}
|
|
86
|
-
export function readUserSkill(userSkillsRoot, skillName) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
const parsed =
|
|
82
|
+
export function readUserSkill(userSkillsRoot, skillName, options = {}) {
|
|
83
|
+
const file = readSkillFile({
|
|
84
|
+
root: userSkillsRoot,
|
|
85
|
+
name: skillName,
|
|
86
|
+
maxBytes: options.mode === "preview" ? MAX_SKILL_PREVIEW_BYTES : MAX_SKILL_FILE_BYTES,
|
|
87
|
+
});
|
|
88
|
+
const parsed = parseSkillMarkdown(file.markdown);
|
|
93
89
|
if (parsed.name !== skillName) {
|
|
94
90
|
throw new SkillServiceError("validation_failed", "Skill metadata is invalid");
|
|
95
91
|
}
|
|
96
|
-
const stat = statSync(skillMdPath);
|
|
97
92
|
return {
|
|
98
93
|
...toSkillProjection({
|
|
99
94
|
metadata: parsed,
|
|
100
|
-
updatedAt:
|
|
95
|
+
updatedAt: file.updatedAt,
|
|
101
96
|
}),
|
|
102
|
-
skill_md:
|
|
97
|
+
skill_md: file.markdown,
|
|
103
98
|
};
|
|
104
99
|
}
|
|
105
100
|
export async function importUserSkillZip(options) {
|
|
@@ -4,5 +4,7 @@ export type ParsedSkillMarkdown = {
|
|
|
4
4
|
short_description?: string;
|
|
5
5
|
};
|
|
6
6
|
export declare function isValidSkillName(value: string): boolean;
|
|
7
|
+
/** Keep import preflight and runtime reads consistent; partial applies only to bounded prefixes. */
|
|
8
|
+
export declare function decodeSkillMarkdown(bytes: Uint8Array, partial?: boolean): string;
|
|
7
9
|
export declare function parseSkillMarkdown(skillMd: string): ParsedSkillMarkdown;
|
|
8
10
|
//# sourceMappingURL=metadata.d.ts.map
|
package/dist/skills/metadata.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { SkillServiceError } from "./errors.js";
|
|
2
2
|
const SKILL_NAME_PATTERN = /^[a-z0-9_-]+$/u;
|
|
3
3
|
export function isValidSkillName(value) {
|
|
4
|
-
return SKILL_NAME_PATTERN.test(value);
|
|
4
|
+
return value.length <= 96 && SKILL_NAME_PATTERN.test(value);
|
|
5
|
+
}
|
|
6
|
+
/** Keep import preflight and runtime reads consistent; partial applies only to bounded prefixes. */
|
|
7
|
+
export function decodeSkillMarkdown(bytes, partial = false) {
|
|
8
|
+
try {
|
|
9
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: partial });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new SkillServiceError("validation_failed", "Skill text is not valid UTF-8");
|
|
13
|
+
}
|
|
5
14
|
}
|
|
6
15
|
export function parseSkillMarkdown(skillMd) {
|
|
7
16
|
const frontmatter = extractFrontmatter(skillMd);
|