talentos 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -103
- package/dist/args.js +49 -0
- package/dist/client.js +87 -0
- package/dist/commands/ask.js +74 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/mcp.js +48 -0
- package/dist/commands/memory.js +61 -0
- package/dist/commands/routines.js +44 -0
- package/dist/commands/skills.js +183 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/config.js +70 -0
- package/dist/format.js +54 -0
- package/dist/index.js +117 -91
- package/dist/install.js +253 -0
- package/dist/login.js +86 -0
- package/dist/mcp-proxy.js +17 -0
- package/dist/skill-files.js +109 -0
- package/package.json +11 -4
- package/dist/api.js +0 -210
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
|
|
3
|
+
export const SKILL_ENTRY = "SKILL.md";
|
|
4
|
+
export const MAX_SKILL_FILES = 100;
|
|
5
|
+
export const MAX_SKILL_BYTES = 512 * 1024;
|
|
6
|
+
const PATH_SEPARATORS = /[/\\]/;
|
|
7
|
+
const PARENT = "..";
|
|
8
|
+
const SKIPPED_DIRS = new Set(["node_modules", ".git"]);
|
|
9
|
+
/**
|
|
10
|
+
* Read a skill package from a SKILL.md path or the directory that contains one.
|
|
11
|
+
* Paths in the result are POSIX-relative to the package root, SKILL.md first,
|
|
12
|
+
* which is the order the backend hashes for idempotent `save_skill` retries.
|
|
13
|
+
*/
|
|
14
|
+
export async function readSkillPackage(entry) {
|
|
15
|
+
const info = await stat(entry).catch(() => null);
|
|
16
|
+
if (!info) {
|
|
17
|
+
throw new Error(`No such file or directory: ${entry}`);
|
|
18
|
+
}
|
|
19
|
+
if (info.isFile()) {
|
|
20
|
+
if (basename(entry) !== SKILL_ENTRY) {
|
|
21
|
+
throw new Error(`Expected ${SKILL_ENTRY} or a directory containing it: ${entry}`);
|
|
22
|
+
}
|
|
23
|
+
return [{ path: SKILL_ENTRY, text: await readFile(entry, "utf8") }];
|
|
24
|
+
}
|
|
25
|
+
const files = await readDirectoryFiles(entry);
|
|
26
|
+
if (!files.some((file) => file.path === SKILL_ENTRY)) {
|
|
27
|
+
throw new Error(`No ${SKILL_ENTRY} at the root of ${entry}`);
|
|
28
|
+
}
|
|
29
|
+
return sortSkillFiles(files);
|
|
30
|
+
}
|
|
31
|
+
/** Write a package to `<dir>/<slug>/...`; returns the package directory. */
|
|
32
|
+
export async function writeSkillPackage(dir, slug, files) {
|
|
33
|
+
const root = resolve(dir, slug);
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (escapesRoot(root, file.path)) {
|
|
36
|
+
throw new Error(`Refusing to write outside the skill folder: ${file.path}`);
|
|
37
|
+
}
|
|
38
|
+
const target = resolve(root, file.path);
|
|
39
|
+
await mkdir(dirname(target), { recursive: true });
|
|
40
|
+
await writeFile(target, file.text, "utf8");
|
|
41
|
+
}
|
|
42
|
+
return root;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Server-sent paths are untrusted. `relative()` alone misses an absolute path
|
|
46
|
+
* and a Windows drive or UNC path, none of which need a `..` segment to land
|
|
47
|
+
* outside the folder — while a name like `..notes.md` is perfectly legitimate.
|
|
48
|
+
*/
|
|
49
|
+
function escapesRoot(root, path) {
|
|
50
|
+
if (isAbsolute(path) || win32.isAbsolute(path)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (path.split(PATH_SEPARATORS).includes(PARENT)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const climb = relative(root, resolve(root, path));
|
|
57
|
+
return climb === PARENT || climb.startsWith(`${PARENT}${sep}`);
|
|
58
|
+
}
|
|
59
|
+
async function readDirectoryFiles(root) {
|
|
60
|
+
const files = [];
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
const queue = [root];
|
|
63
|
+
while (queue.length > 0) {
|
|
64
|
+
const current = queue.shift();
|
|
65
|
+
if (current === undefined) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
69
|
+
for (const dirent of entries) {
|
|
70
|
+
const path = join(current, dirent.name);
|
|
71
|
+
if (dirent.isDirectory()) {
|
|
72
|
+
if (!SKIPPED_DIRS.has(dirent.name)) {
|
|
73
|
+
queue.push(path);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!dirent.isFile()) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const buffer = await readFile(path);
|
|
81
|
+
if (buffer.includes(0)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
bytes += buffer.byteLength;
|
|
85
|
+
if (bytes > MAX_SKILL_BYTES) {
|
|
86
|
+
throw new Error(`Skill package is larger than ${MAX_SKILL_BYTES / 1024} KiB`);
|
|
87
|
+
}
|
|
88
|
+
files.push({
|
|
89
|
+
path: relative(root, path).split(sep).join("/"),
|
|
90
|
+
text: buffer.toString("utf8"),
|
|
91
|
+
});
|
|
92
|
+
if (files.length > MAX_SKILL_FILES) {
|
|
93
|
+
throw new Error(`Skill package has more than ${MAX_SKILL_FILES} files`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
function sortSkillFiles(files) {
|
|
100
|
+
return [...files].sort((a, b) => {
|
|
101
|
+
if (a.path === SKILL_ENTRY) {
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
104
|
+
if (b.path === SKILL_ENTRY) {
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
return a.path.localeCompare(b.path);
|
|
108
|
+
});
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talentos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "TalentOS CLI: push and pull skills, read and write team memory, and delegate work to your Talent from any coding agent.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/TalentOSOfficial/talentos-team.git",
|
|
9
|
+
"directory": "tools/talentos-cli"
|
|
10
|
+
},
|
|
6
11
|
"bin": {
|
|
7
12
|
"talentos": "./dist/index.js"
|
|
8
13
|
},
|
|
@@ -10,12 +15,13 @@
|
|
|
10
15
|
"dist"
|
|
11
16
|
],
|
|
12
17
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
18
|
+
"node": ">=20"
|
|
14
19
|
},
|
|
15
20
|
"scripts": {
|
|
16
21
|
"start": "tsx src/index.ts",
|
|
17
22
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
18
23
|
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
19
25
|
"prepack": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
20
26
|
},
|
|
21
27
|
"dependencies": {
|
|
@@ -25,6 +31,7 @@
|
|
|
25
31
|
"devDependencies": {
|
|
26
32
|
"@types/node": "^22",
|
|
27
33
|
"tsx": "^4.21.0",
|
|
28
|
-
"typescript": "^6.0.3"
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"vitest": "^4.1.4"
|
|
29
36
|
}
|
|
30
37
|
}
|
package/dist/api.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// The HTTP-client half of the MCP server: one function per TalentOS
|
|
2
|
-
// /api/v0/* endpoint, returning MCP-shaped text results. Deliberately free of
|
|
3
|
-
// MCP SDK imports (ToolResult is structurally CallToolResult-compatible) so
|
|
4
|
-
// these handlers unit-test from the repo root against a mocked fetch without
|
|
5
|
-
// installing this package's dependencies. Zero business logic lives here —
|
|
6
|
-
// authz, org scoping, and visibility are all server-side.
|
|
7
|
-
const WORKFLOWS_PATH = "/api/v0/workflows";
|
|
8
|
-
const NODES_PATH = "/api/v0/nodes";
|
|
9
|
-
const CONTEXT_PATH = "/api/v0/context";
|
|
10
|
-
const SKILLS_PATH = "/api/v0/skills";
|
|
11
|
-
// A hung deployment (connection accepted, no response) would otherwise leave the
|
|
12
|
-
// tool call pending forever with no error; abort so it surfaces as networkError.
|
|
13
|
-
const REQUEST_TIMEOUT_MS = 15_000;
|
|
14
|
-
export async function listWorkflows(config) {
|
|
15
|
-
const result = await fetchJson(config, WORKFLOWS_PATH);
|
|
16
|
-
if ("error" in result) {
|
|
17
|
-
return result.error;
|
|
18
|
-
}
|
|
19
|
-
const workflows = result.body.workflows ?? [];
|
|
20
|
-
if (workflows.length === 0) {
|
|
21
|
-
return textResult("No workflows are visible to this API key.");
|
|
22
|
-
}
|
|
23
|
-
const lines = workflows.map((workflow) => {
|
|
24
|
-
const parts = [
|
|
25
|
-
`- ${workflow.title} (id: ${workflow.id})`,
|
|
26
|
-
`status: ${workflow.status}`,
|
|
27
|
-
`${workflow.stepCount} step(s)`,
|
|
28
|
-
`dept: ${workflow.department ?? "—"}`,
|
|
29
|
-
`updated ${workflow.updatedAt ?? "unknown"}`,
|
|
30
|
-
];
|
|
31
|
-
if (workflow.description) {
|
|
32
|
-
parts.push(workflow.description);
|
|
33
|
-
}
|
|
34
|
-
return parts.join(" · ");
|
|
35
|
-
});
|
|
36
|
-
return textResult(`${workflows.length} workflow(s):\n${lines.join("\n")}`);
|
|
37
|
-
}
|
|
38
|
-
export async function getWorkflow(config, workflowId) {
|
|
39
|
-
const url = `${config.baseUrl}${WORKFLOWS_PATH}/${encodeURIComponent(workflowId)}`;
|
|
40
|
-
let response;
|
|
41
|
-
try {
|
|
42
|
-
response = await fetch(url, {
|
|
43
|
-
headers: {
|
|
44
|
-
Accept: "text/markdown",
|
|
45
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
46
|
-
},
|
|
47
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
return networkError(error);
|
|
52
|
-
}
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
return await httpError(response, workflowId);
|
|
55
|
-
}
|
|
56
|
-
// The wrong-URL failure mode is a login page or JSON error served from the
|
|
57
|
-
// wrong domain; a rewritten/stripped content-type on a real runbook is not.
|
|
58
|
-
// Reject only the clearly-wrong types so a text/plain runbook still passes.
|
|
59
|
-
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
60
|
-
if (contentType.includes("text/html") ||
|
|
61
|
-
contentType.includes("application/json")) {
|
|
62
|
-
return toolError(`Expected a Markdown runbook but got content-type "${contentType || "unknown"}". ${URL_HINT}`);
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
return textResult(await response.text());
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return networkError(error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export async function listNodes(config) {
|
|
72
|
-
const result = await fetchJson(config, NODES_PATH);
|
|
73
|
-
if ("error" in result) {
|
|
74
|
-
return result.error;
|
|
75
|
-
}
|
|
76
|
-
const nodes = result.body.nodes ?? [];
|
|
77
|
-
if (nodes.length === 0) {
|
|
78
|
-
return textResult("No company nodes are published.");
|
|
79
|
-
}
|
|
80
|
-
const lines = nodes.map((node) => {
|
|
81
|
-
const parts = [
|
|
82
|
-
`- ${node.name} (id: ${node.id})`,
|
|
83
|
-
node.stepType,
|
|
84
|
-
node.summary,
|
|
85
|
-
];
|
|
86
|
-
if (Array.isArray(node.toolNames) && node.toolNames.length > 0) {
|
|
87
|
-
parts.push(`tools: ${node.toolNames.join(", ")}`);
|
|
88
|
-
}
|
|
89
|
-
return parts.join(" · ");
|
|
90
|
-
});
|
|
91
|
-
return textResult(`${nodes.length} node(s):\n${lines.join("\n")}`);
|
|
92
|
-
}
|
|
93
|
-
export async function getContext(config) {
|
|
94
|
-
const result = await fetchJson(config, CONTEXT_PATH);
|
|
95
|
-
if ("error" in result) {
|
|
96
|
-
return result.error;
|
|
97
|
-
}
|
|
98
|
-
// The org summary as-is: it is already a compact priming payload, and any
|
|
99
|
-
// formatting opinion belongs server-side, not in this wrapper.
|
|
100
|
-
return textResult(JSON.stringify(result.body, null, 2));
|
|
101
|
-
}
|
|
102
|
-
export async function createSkill(config, skill) {
|
|
103
|
-
return await writeSkill(config, SKILLS_PATH, skill, "Created");
|
|
104
|
-
}
|
|
105
|
-
export async function updateSkill(config, skillId, skill) {
|
|
106
|
-
return await writeSkill(config, `${SKILLS_PATH}/${encodeURIComponent(skillId)}`, skill, "Updated", skillId);
|
|
107
|
-
}
|
|
108
|
-
const URL_HINT = "The TALENTOS_API_URL may be wrong — it must point at the deployment's https://<name>.convex.site domain.";
|
|
109
|
-
async function writeSkill(config, path, skill, action, skillId) {
|
|
110
|
-
const result = await fetchJson(config, path, path === SKILLS_PATH ? "POST" : "PUT", skill);
|
|
111
|
-
if ("error" in result) {
|
|
112
|
-
return result.error;
|
|
113
|
-
}
|
|
114
|
-
const id = result.body.skillId ?? skillId;
|
|
115
|
-
const version = result.body.version;
|
|
116
|
-
if (!id || version === undefined) {
|
|
117
|
-
return toolError(`The response was missing a skill id or version. ${URL_HINT}`);
|
|
118
|
-
}
|
|
119
|
-
return textResult(`${action} skill (id: ${id}) · version ${version}\nPrivate to this API key's owner; they curate it in their TalentOS Skills library.`);
|
|
120
|
-
}
|
|
121
|
-
function textResult(text) {
|
|
122
|
-
return { content: [{ text, type: "text" }] };
|
|
123
|
-
}
|
|
124
|
-
function toolError(text) {
|
|
125
|
-
return { content: [{ text, type: "text" }], isError: true };
|
|
126
|
-
}
|
|
127
|
-
async function httpError(response, workflowId) {
|
|
128
|
-
const { message } = await parseErrorBody(response);
|
|
129
|
-
if (response.status === 400 && message) {
|
|
130
|
-
return toolError(message);
|
|
131
|
-
}
|
|
132
|
-
if (response.status === 401) {
|
|
133
|
-
return toolError("Unauthorized (401). Check that TALENTOS_API_KEY is a valid, active key for this deployment.");
|
|
134
|
-
}
|
|
135
|
-
if (response.status === 404 && workflowId) {
|
|
136
|
-
return toolError(`No workflow with id "${workflowId}" exists, or it is not visible to this API key.`);
|
|
137
|
-
}
|
|
138
|
-
if (response.status === 409) {
|
|
139
|
-
return toolError(message ??
|
|
140
|
-
"This write conflicts with the current state. Reload and retry.");
|
|
141
|
-
}
|
|
142
|
-
if (response.status === 429) {
|
|
143
|
-
return toolError("Rate limited (429). This API key is over its requests-per-minute budget — wait a moment and retry.");
|
|
144
|
-
}
|
|
145
|
-
if (message) {
|
|
146
|
-
return toolError(`The TalentOS API returned ${response.status}: ${message}`);
|
|
147
|
-
}
|
|
148
|
-
return toolError(`The TalentOS API returned an unexpected status ${response.status}.`);
|
|
149
|
-
}
|
|
150
|
-
async function parseErrorBody(response) {
|
|
151
|
-
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
152
|
-
if (!contentType.includes("application/json")) {
|
|
153
|
-
return {};
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const body = await response.json();
|
|
157
|
-
if (typeof body === "object" &&
|
|
158
|
-
body !== null &&
|
|
159
|
-
typeof body.error === "string") {
|
|
160
|
-
return { message: body.error };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return {};
|
|
165
|
-
}
|
|
166
|
-
return {};
|
|
167
|
-
}
|
|
168
|
-
async function fetchJson(config, path, method = "GET", body, workflowId) {
|
|
169
|
-
let response;
|
|
170
|
-
try {
|
|
171
|
-
response = await fetch(`${config.baseUrl}${path}`, {
|
|
172
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
173
|
-
headers: {
|
|
174
|
-
Accept: "application/json",
|
|
175
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
176
|
-
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
177
|
-
},
|
|
178
|
-
method,
|
|
179
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
return { error: networkError(error) };
|
|
184
|
-
}
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
return { error: await httpError(response, workflowId) };
|
|
187
|
-
}
|
|
188
|
-
let parsed;
|
|
189
|
-
try {
|
|
190
|
-
parsed = await response.json();
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
return {
|
|
194
|
-
error: toolError(`The response was not valid JSON. ${URL_HINT}`),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
198
|
-
return {
|
|
199
|
-
error: toolError(`The response was not a JSON object. ${URL_HINT}`),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
return { body: parsed };
|
|
203
|
-
}
|
|
204
|
-
function networkError(error) {
|
|
205
|
-
if (error instanceof Error && error.name === "TimeoutError") {
|
|
206
|
-
return toolError(`The TalentOS API did not respond within ${REQUEST_TIMEOUT_MS / 1000}s. The deployment may be overloaded or unreachable — check TALENTOS_API_URL and retry.`);
|
|
207
|
-
}
|
|
208
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
209
|
-
return toolError(`Could not reach the TalentOS API. Check TALENTOS_API_URL points at the deployment's .convex.site domain. (${detail})`);
|
|
210
|
-
}
|