@piagent/platform 1.11.1 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +18 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +18 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +19 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +19 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +11 -5
- package/packages/piagent-core/extensions/source-checkout-paths.ts +31 -0
- package/packages/piagent-core/managed/agent-skills.mjs +44 -0
- package/packages/piagent-core/managed/doctor.mjs +23 -2
- package/packages/piagent-core/managed/language-environment.mjs +118 -0
- package/packages/piagent-core/managed/resource-loader.mjs +5 -4
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -1
- package/packages/piagent-core/managed/session.mjs +15 -3
- package/packages/piagent-core/managed/tool-boundary.mjs +110 -28
- package/packages/piagent-core/managed/tool-worker.mjs +3 -3
- package/packages/piagent-core/managed/toolchain.mjs +10 -8
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/registration/agent-resources.ts +12 -0
- package/packages/piagent-core/runtime/resources/agent-resources.d.mts +6 -0
- package/packages/piagent-core/runtime/resources/agent-resources.mjs +140 -0
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-D70xkf0d.js → MarkdownMessage-FWfOWZjV.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-BuIJR5rd.js → index-DFKHLVYk.js} +14 -14
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-service.ts +16 -0
- package/packages/piagent-webui/gateway/path-suggestions.ts +118 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +3 -1
- package/packages/piagent-webui/server/loopback-server.ts +2 -0
- package/packages/piagent-webui/server/member-routes.ts +22 -3
- package/packages/piagent-webui/server/transcript-projection.ts +7 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DFKHLVYk.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type GatewayDescriptor
|
|
20
20
|
} from "./profile-state.ts";
|
|
21
21
|
import { buildSessionCatalog, projectRefForCwd } from "./session-catalog.ts";
|
|
22
|
+
import { suggestPaths } from "./path-suggestions.ts";
|
|
23
|
+
import { listAgentCommands } from "../../piagent-core/runtime/resources/agent-resources.mjs";
|
|
22
24
|
import { sessionRefForPath } from "../ownership/session-refs.ts";
|
|
23
25
|
import { SessionMetadataStore } from "./session-metadata-store.ts";
|
|
24
26
|
import { GatewayProtocolService } from "./gateway-protocol-service.ts";
|
|
@@ -222,6 +224,9 @@ export async function startPiagentGateway(options: {
|
|
|
222
224
|
// Every revision the browser sees and sends back is the merged one.
|
|
223
225
|
const hubCatalog = relay ? async () => relay!.merge(await readCatalog()) : readCatalog;
|
|
224
226
|
const company = (sessionRef: unknown) => !!relay?.owns(sessionRef);
|
|
227
|
+
const projectFolder = async (projectRef: string) => projects.resolve(projectRef)
|
|
228
|
+
?? (await runtimes!.listSessions()).find((info) => typeof info.cwd === "string" && projectRefForCwd(key, info.cwd) === projectRef)?.cwd
|
|
229
|
+
?? (relay ? (await relay.folders()).find((folder) => projectRefForCwd(key, folder) === projectRef) : undefined);
|
|
225
230
|
const relayPost = (path: string, body: unknown) => relay!.json("POST", path, body);
|
|
226
231
|
const commands = new SessionCommandController({ catalog: hubCatalog, runtimes, metadata,
|
|
227
232
|
store: new SessionCommandStore(state.root, key), events,
|
|
@@ -286,6 +291,17 @@ export async function startPiagentGateway(options: {
|
|
|
286
291
|
// conversation runs (the company Gateway for a company conversation).
|
|
287
292
|
readSessionQuestions: (sessionRef) => company(sessionRef)
|
|
288
293
|
? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/questions`) : runtimes!.questions(sessionRef),
|
|
294
|
+
// What an @ or a / in the composer offers, under a folder this dashboard
|
|
295
|
+
// knows: an imported project or the folder of a conversation (company
|
|
296
|
+
// ones too).
|
|
297
|
+
suggestPaths: async (projectRef, query) => {
|
|
298
|
+
const root = await projectFolder(projectRef);
|
|
299
|
+
return root ? await suggestPaths({ root, query }) : null;
|
|
300
|
+
},
|
|
301
|
+
listCommands: async (projectRef) => {
|
|
302
|
+
const root = await projectFolder(projectRef);
|
|
303
|
+
return root ? { commands: listAgentCommands({ cwd: root }) } : null;
|
|
304
|
+
},
|
|
289
305
|
updates: { status: () => updates!.status(), check: () => updates!.check(), apply: (request) => updates!.apply(request) },
|
|
290
306
|
answerSessionQuestion: (sessionRef, questionRef, answer) => company(sessionRef)
|
|
291
307
|
? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/questions/${encodeURIComponent(questionRef)}/answer`, answer)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
// What an @ in the composer offers, as Pi's terminal does: files and folders
|
|
7
|
+
// of the project matched loosely by name (fd, which honours .gitignore), and,
|
|
8
|
+
// after a folder and a slash (src/, ~/Documents/, /Volumes/), the names under
|
|
9
|
+
// that folder anywhere on this Mac. Only names reach the browser; no file is
|
|
10
|
+
// opened.
|
|
11
|
+
export type PathSuggestion = { value: string; label: string; detail: string; kind: "file" | "directory" };
|
|
12
|
+
type Entry = { path: string; directory: boolean };
|
|
13
|
+
|
|
14
|
+
const LIMIT = 20;
|
|
15
|
+
const SEARCH_RESULTS = 100;
|
|
16
|
+
const LISTED_ENTRIES = 5_000;
|
|
17
|
+
// Outside the project a search stays shallow and skips app data and
|
|
18
|
+
// dependency folders: a home folder is far larger than a repository.
|
|
19
|
+
const OUTSIDE_DEPTH = 4;
|
|
20
|
+
const fold = (value: string) => value.normalize("NFC").toLowerCase();
|
|
21
|
+
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
// A name typed composed (Dự án) may be stored decomposed on disk: either form matches.
|
|
23
|
+
const either = (value: string) => value.normalize("NFC") === value.normalize("NFD") ? escape(value)
|
|
24
|
+
: `(?:${escape(value.normalize("NFC"))}|${escape(value.normalize("NFD"))})`;
|
|
25
|
+
const inside = (file: string, root: string) => file === root || file.startsWith(root + path.sep);
|
|
26
|
+
|
|
27
|
+
export function findFd(home = os.homedir()): string | null {
|
|
28
|
+
for (const candidate of ["/opt/homebrew/bin/fd", "/usr/local/bin/fd", path.join(home, ".pi/agent/bin/fd")]) {
|
|
29
|
+
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch { /* next */ }
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Higher is better: the name itself, then its start, then anywhere in it,
|
|
35
|
+
// then anywhere in the path; a folder before a file with the same score.
|
|
36
|
+
function score(entry: string, query: string, directory: boolean): number {
|
|
37
|
+
const name = fold(path.basename(entry)), wanted = fold(query);
|
|
38
|
+
const value = name === wanted ? 100 : name.startsWith(wanted) ? 80 : name.includes(wanted) ? 50 : fold(entry).includes(wanted) ? 30 : 0;
|
|
39
|
+
return value && directory ? value + 10 : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(file: string, args: string[], signal?: AbortSignal, input?: string): Promise<string> {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const child = execFile(file, args, { encoding: "utf8", timeout: 4_000, maxBuffer: 8 << 20, signal }, (error, stdout) => resolve(error ? "" : stdout));
|
|
45
|
+
child.stdin?.on("error", () => {}); child.stdin?.end(input ?? "");
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The folder's own names, without what its repository ignores (dist/,
|
|
50
|
+
// node_modules/), as fd lists them for Pi.
|
|
51
|
+
async function listed(folder: string, signal?: AbortSignal): Promise<Entry[]> {
|
|
52
|
+
const entries: Entry[] = [];
|
|
53
|
+
try {
|
|
54
|
+
for await (const entry of await fs.promises.opendir(folder)) {
|
|
55
|
+
let directory = entry.isDirectory();
|
|
56
|
+
if (entry.isSymbolicLink()) try { directory = fs.statSync(path.join(folder, entry.name)).isDirectory(); } catch { /* broken link */ }
|
|
57
|
+
entries.push({ path: entry.name, directory });
|
|
58
|
+
if (entries.length >= LISTED_ENTRIES) break;
|
|
59
|
+
}
|
|
60
|
+
} catch { /* unreadable */ }
|
|
61
|
+
if (!entries.length) return entries;
|
|
62
|
+
const ignored = new Set((await run("/usr/bin/git", ["-C", folder, "check-ignore", "-z", "--stdin"], signal, entries.map((entry) => `${entry.path}\0`).join("")))
|
|
63
|
+
.split("\0").filter(Boolean));
|
|
64
|
+
return ignored.size ? entries.filter((entry) => !ignored.has(entry.path)) : entries;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Names deeper down that match: fd as Pi runs it in the project; shallow and
|
|
68
|
+
// without hidden folders elsewhere; without fd, the files git knows about.
|
|
69
|
+
async function searched(folder: string, query: string, options: { fd: string | null; project: boolean; atRoot: boolean; signal?: AbortSignal }): Promise<Entry[]> {
|
|
70
|
+
if (options.fd) {
|
|
71
|
+
const args = ["--base-directory", folder, "--type", "f", "--type", "d", "--max-results", String(SEARCH_RESULTS), "--ignore-case", "--exclude", ".git",
|
|
72
|
+
...(options.project ? ["--follow", "--hidden"] : ["--max-depth", String(OUTSIDE_DEPTH), "--exclude", "Library", "--exclude", "node_modules", ...(query.startsWith(".") ? ["--hidden"] : [])]),
|
|
73
|
+
...(query.includes("/") ? ["--full-path", query.split("/").filter(Boolean).map(either).join("/")] : [either(query)])];
|
|
74
|
+
return (await run(options.fd, args, options.signal)).split("\n").filter(Boolean)
|
|
75
|
+
.map((line) => ({ path: line.replace(/^\.\//, "").replace(/\/$/, ""), directory: line.endsWith("/") }));
|
|
76
|
+
}
|
|
77
|
+
if (!options.atRoot) return [];
|
|
78
|
+
const files = (await run("/usr/bin/git", ["-C", folder, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], options.signal))
|
|
79
|
+
.split("\0").filter(Boolean).slice(0, 50_000);
|
|
80
|
+
const folders = new Set<string>();
|
|
81
|
+
for (const file of files) for (let dir = path.posix.dirname(file); dir !== "."; dir = path.posix.dirname(dir)) folders.add(dir);
|
|
82
|
+
const wanted = fold(query);
|
|
83
|
+
return [...[...folders].map((dir) => ({ path: dir, directory: true })), ...files.map((file) => ({ path: file, directory: false }))]
|
|
84
|
+
.filter((entry) => fold(entry.path).includes(wanted));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function suggestPaths(options: { root: string; query: string; home?: string; fd?: string | null; signal?: AbortSignal }): Promise<{ suggestions: PathSuggestion[] }> {
|
|
88
|
+
const home = options.home ?? os.homedir(), fd = options.fd === undefined ? findFd(home) : options.fd;
|
|
89
|
+
const root = fs.realpathSync(options.root);
|
|
90
|
+
const typed = options.query.trim() === "~" ? "~/" : options.query.trim();
|
|
91
|
+
// Text up to the last slash names a folder: search under it (Pi's scoped
|
|
92
|
+
// search). When it is not a folder, the whole text is matched in the project.
|
|
93
|
+
const cut = typed.lastIndexOf("/") + 1;
|
|
94
|
+
let base = typed.slice(0, cut), name = typed.slice(cut), folder = root;
|
|
95
|
+
if (base) {
|
|
96
|
+
try {
|
|
97
|
+
const candidate = fs.realpathSync(base.startsWith("~/") ? path.join(home, base.slice(2)) : path.resolve(root, base));
|
|
98
|
+
if (fs.statSync(candidate).isDirectory()) folder = candidate; else { base = ""; name = typed; }
|
|
99
|
+
} catch { base = ""; name = typed; }
|
|
100
|
+
}
|
|
101
|
+
const project = inside(folder, root);
|
|
102
|
+
const near = await listed(folder, options.signal);
|
|
103
|
+
const deep = name ? await searched(folder, name, { fd, project, atRoot: folder === root, signal: options.signal }) : [];
|
|
104
|
+
const seen = new Set<string>(), entries: Array<Entry & { score: number }> = [];
|
|
105
|
+
for (const entry of [...near, ...deep]) {
|
|
106
|
+
if (seen.has(entry.path)) continue; seen.add(entry.path);
|
|
107
|
+
// Hidden entries (.github, .ssh, .zshrc) for a name typed with a dot, and
|
|
108
|
+
// in the project for any name typed, as Pi's search finds them there.
|
|
109
|
+
if (entry.path.split("/").some((part) => part.startsWith(".")) && !name.startsWith(".") && !(project && name)) continue;
|
|
110
|
+
const value = name ? score(entry.path, name.split("/").filter(Boolean).at(-1) ?? name, entry.directory) : 1;
|
|
111
|
+
if (value > 0) entries.push({ ...entry, score: value });
|
|
112
|
+
}
|
|
113
|
+
const depth = (entry: Entry) => entry.path.split("/").length;
|
|
114
|
+
entries.sort((a, b) => b.score - a.score || depth(a) - depth(b)
|
|
115
|
+
|| (name ? a.path.length - b.path.length : Number(b.directory) - Number(a.directory)) || a.path.localeCompare(b.path));
|
|
116
|
+
return { suggestions: entries.slice(0, LIMIT).map((entry) => ({ value: `${base}${entry.path}${entry.directory ? "/" : ""}`,
|
|
117
|
+
label: path.basename(entry.path) + (entry.directory ? "/" : ""), detail: `${base}${entry.path}`, kind: entry.directory ? "directory" : "file" })) };
|
|
118
|
+
}
|
|
@@ -426,7 +426,9 @@ export class SessionRuntimeSupervisor {
|
|
|
426
426
|
if (!file) throw new Error("session-fork-unavailable");
|
|
427
427
|
forkManager = this.#host.SessionManager.open(file);
|
|
428
428
|
} else {
|
|
429
|
-
|
|
429
|
+
// Managed members keep sessions flat in one folder; Pi would otherwise write
|
|
430
|
+
// the fork into its per-cwd folder, where the gateway never finds it again.
|
|
431
|
+
forkManager = this.#host.SessionManager.forkFrom(source.path, source.cwd, this.#sessionDirectory);
|
|
430
432
|
}
|
|
431
433
|
if (title) forkManager.appendSessionInfo(title);
|
|
432
434
|
const file = forkManager.getSessionFile(), id = forkManager.getSessionId?.();
|
|
@@ -70,6 +70,8 @@ export async function startLoopbackServer(options: {
|
|
|
70
70
|
executeApproval?: (approvalRef: string, decision: unknown) => unknown | Promise<unknown>;
|
|
71
71
|
readSessionQuestions?: (sessionRef: string) => unknown | Promise<unknown>;
|
|
72
72
|
answerSessionQuestion?: (sessionRef: string, questionRef: string, answer: unknown) => unknown | Promise<unknown>;
|
|
73
|
+
suggestPaths?: (projectRef: string, query: string) => unknown | Promise<unknown>;
|
|
74
|
+
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
73
75
|
updates?: UpdateRoutes;
|
|
74
76
|
bootstrapTtlMs?: number;
|
|
75
77
|
sessionTtlMs?: number;
|
|
@@ -4,12 +4,14 @@ import { errorResponse, jsonResponse, requestBody } from "./http-security.ts";
|
|
|
4
4
|
import type { SessionAuthority } from "./session-auth.ts";
|
|
5
5
|
|
|
6
6
|
// Requests a member makes beside a conversation: answering the main agent's
|
|
7
|
-
// questions,
|
|
8
|
-
//
|
|
9
|
-
// a share of the control
|
|
7
|
+
// questions, what an @ or a / in the composer offers, and keeping
|
|
8
|
+
// Piagent itself up to date. Reads need the browser session; every change
|
|
9
|
+
// also needs this origin, the session's CSRF token and a share of the control
|
|
10
|
+
// rate.
|
|
10
11
|
const MAX_CONTROL_BODY_BYTES = 70_000;
|
|
11
12
|
const CURSOR = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
12
13
|
const QUESTION_PATH = /^\/api\/v1\/sessions\/([^/]+)\/questions(?:\/([^/]+)\/answer)?$/;
|
|
14
|
+
const PROJECT_PATH = /^\/api\/v1\/projects\/([^/]+)\/(paths|commands)$/;
|
|
13
15
|
|
|
14
16
|
export type UpdateRoutes = {
|
|
15
17
|
// What is installed, what is available, and the last update run.
|
|
@@ -27,6 +29,11 @@ type Context = {
|
|
|
27
29
|
type Options = {
|
|
28
30
|
readSessionQuestions?: (sessionRef: string) => unknown | Promise<unknown>;
|
|
29
31
|
answerSessionQuestion?: (sessionRef: string, questionRef: string, answer: unknown) => unknown | Promise<unknown>;
|
|
32
|
+
// Names under the project (or the folder typed after ~/ or /); null when the
|
|
33
|
+
// project is not one this dashboard knows.
|
|
34
|
+
suggestPaths?: (projectRef: string, query: string) => unknown | Promise<unknown>;
|
|
35
|
+
// The project's and the member's commands and skills; null as above.
|
|
36
|
+
listCommands?: (projectRef: string) => unknown | Promise<unknown>;
|
|
30
37
|
updates?: UpdateRoutes;
|
|
31
38
|
};
|
|
32
39
|
|
|
@@ -76,6 +83,18 @@ export async function routeMemberRequest(context: Context, options: Options): Pr
|
|
|
76
83
|
}
|
|
77
84
|
return true;
|
|
78
85
|
}
|
|
86
|
+
const project = request.method === "GET" ? PROJECT_PATH.exec(url.pathname) : null;
|
|
87
|
+
const read = project?.[2] === "paths" ? options.suggestPaths : project ? options.listCommands : undefined;
|
|
88
|
+
if (project && read) {
|
|
89
|
+
if (!auth.authenticate(request)) { errorResponse(response, 401, "authentication-required"); return true; }
|
|
90
|
+
const projectRef = decodeURIComponent(project[1]), query = url.searchParams.get("query") ?? "";
|
|
91
|
+
if (!CURSOR.test(projectRef) || query.length > 512 || /[\x00-\x1f]/.test(query)) { errorResponse(response, 400, `invalid-${project[2] === "paths" ? "path" : "command"}-query`); return true; }
|
|
92
|
+
try {
|
|
93
|
+
const value = await read(projectRef, query);
|
|
94
|
+
if (value === null) errorResponse(response, 404, "project-not-found"); else jsonResponse(response, 200, value);
|
|
95
|
+
} catch { errorResponse(response, 503, `${project[2] === "paths" ? "path-suggestions" : "commands"}-unavailable`); }
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
79
98
|
|
|
80
99
|
if (!options.updates || !url.pathname.startsWith("/api/v1/updates")) return false;
|
|
81
100
|
const updates = options.updates;
|
|
@@ -98,7 +98,13 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
|
|
|
98
98
|
// hashing them here would turn the chat bubble into a document dump and make
|
|
99
99
|
// a bounded UI digest an oracle over user files. The first block is the exact
|
|
100
100
|
// text the operator typed; the remaining recognized blocks become cards.
|
|
101
|
-
return { text: attachments.length ? String(textParts[0]?.text ?? "") : messageText(message), attachments: attachments.slice(0, 4) };
|
|
101
|
+
return { text: typedSkillCall(attachments.length ? String(textParts[0]?.text ?? "") : messageText(message)), attachments: attachments.slice(0, 4) };
|
|
102
|
+
}
|
|
103
|
+
// A skill the member called (/skill:name): the message holds the skill's
|
|
104
|
+
// instructions for the model; the chat shows what was typed.
|
|
105
|
+
function typedSkillCall(text: string): string {
|
|
106
|
+
const skill = /^<skill name="([^"\n]+)" location="[^"\n]+">\n[\s\S]*?\n<\/skill>(?:\n\n([\s\S]+))?$/.exec(text);
|
|
107
|
+
return skill ? `/skill:${skill[1]}${skill[2] ? ` ${skill[2]}` : ""}` : text;
|
|
102
108
|
}
|
|
103
109
|
// A company request fails with Studio's (or Agent Watch's) own code: who
|
|
104
110
|
// failed, which kind of failure, and Studio's request id. Text is never shown.
|