@otoreach/telmeeh-cli 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 +58 -0
- package/dist/client.js +98 -0
- package/dist/commands/auth.js +78 -0
- package/dist/commands/categories.js +26 -0
- package/dist/commands/prompts.js +110 -0
- package/dist/commands/skills.js +151 -0
- package/dist/config.js +52 -0
- package/dist/index.js +36 -0
- package/dist/installer.js +100 -0
- package/dist/output.js +44 -0
- package/dist/postinstall.js +18 -0
- package/package.json +45 -0
- package/skills/telmeeh/SKILL.md +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @telmeeh/cli
|
|
2
|
+
|
|
3
|
+
Telmeeh from your terminal — search, fetch, create, and improve prompts and agent
|
|
4
|
+
skills, and install a Telmeeh skill into your AI assistant.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g @telmeeh/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
On install you'll be asked which AI assistant(s) to add the Telmeeh agent skill to
|
|
13
|
+
(Claude Code, Cursor, Windsurf, …). You can re-run this anytime with
|
|
14
|
+
`telmeeh skills install`.
|
|
15
|
+
|
|
16
|
+
## Authenticate
|
|
17
|
+
|
|
18
|
+
Create an API key in **Settings → API Keys** in the Telmeeh dashboard, then:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
telmeeh auth login # prompts for your tk_live_… key
|
|
22
|
+
telmeeh auth whoami # confirm who you are
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The key is stored in `~/.telmeeh/config.json` (mode `0600`). You can also set
|
|
26
|
+
`TELMEEH_API_KEY` and `TELMEEH_API_URL` environment variables, or pass `--key` /
|
|
27
|
+
`--api-url` per command.
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
telmeeh categories list
|
|
33
|
+
|
|
34
|
+
telmeeh prompts list [--view all|starred|recent|uncategorized] [--category <id>]
|
|
35
|
+
telmeeh prompts search <query>
|
|
36
|
+
telmeeh prompts get <id>
|
|
37
|
+
telmeeh prompts create --title <t> --content <text|-> [--category <id>] [--starred]
|
|
38
|
+
telmeeh prompts improve <text|-> [--context <c>]
|
|
39
|
+
|
|
40
|
+
telmeeh skills list [--view ...] [--category <id>]
|
|
41
|
+
telmeeh skills search <query>
|
|
42
|
+
telmeeh skills get <id>
|
|
43
|
+
telmeeh skills create --name <n> [--desc <d>] [--category <id>]
|
|
44
|
+
telmeeh skills update <id> [--name <n>] [--desc <d>] [--file remote=local.md] [--note <n>]
|
|
45
|
+
telmeeh skills generate --goal <g> [--context <c>] [--category <id>]
|
|
46
|
+
telmeeh skills export <id> [--out <dir>]
|
|
47
|
+
telmeeh skills install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `-` for `--content`/`<text>` to read from stdin, and `--json` for raw JSON output.
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm install
|
|
56
|
+
pnpm dev -- prompts list # run from source with tsx
|
|
57
|
+
pnpm build # compile to dist/
|
|
58
|
+
```
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { resolveConfig } from './config.js';
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(status, message, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ApiError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Thrown when no API key is configured. */
|
|
13
|
+
export class NotAuthenticatedError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('Not authenticated. Run `telmeeh auth login` first.');
|
|
16
|
+
this.name = 'NotAuthenticatedError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function buildUrl(base, path, query) {
|
|
20
|
+
const url = new URL(path, base + '/');
|
|
21
|
+
if (query) {
|
|
22
|
+
for (const [key, value] of Object.entries(query)) {
|
|
23
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
24
|
+
url.searchParams.set(key, String(value));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
async function parseError(res) {
|
|
31
|
+
let body = undefined;
|
|
32
|
+
let message = `Request failed (${res.status})`;
|
|
33
|
+
try {
|
|
34
|
+
body = await res.json();
|
|
35
|
+
if (body && typeof body === 'object' && 'error' in body) {
|
|
36
|
+
message = String(body.error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* non-JSON body */
|
|
41
|
+
}
|
|
42
|
+
if (res.status === 401) {
|
|
43
|
+
message = 'Unauthorized — your API key is missing or invalid. Run `telmeeh auth login`.';
|
|
44
|
+
}
|
|
45
|
+
else if (res.status === 429) {
|
|
46
|
+
const reset = body?.resetTime
|
|
47
|
+
?? body?.rateLimit?.resetTime;
|
|
48
|
+
const when = reset ? ` Resets at ${new Date(reset).toLocaleString()}.` : '';
|
|
49
|
+
message = `Rate limit exceeded.${when}`;
|
|
50
|
+
}
|
|
51
|
+
return new ApiError(res.status, message, body);
|
|
52
|
+
}
|
|
53
|
+
export class TelmeehClient {
|
|
54
|
+
apiKey;
|
|
55
|
+
apiUrl;
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
const cfg = resolveConfig(options);
|
|
58
|
+
this.apiKey = cfg.apiKey;
|
|
59
|
+
this.apiUrl = cfg.apiUrl;
|
|
60
|
+
}
|
|
61
|
+
get baseUrl() {
|
|
62
|
+
return this.apiUrl;
|
|
63
|
+
}
|
|
64
|
+
headers(extra) {
|
|
65
|
+
if (!this.apiKey)
|
|
66
|
+
throw new NotAuthenticatedError();
|
|
67
|
+
return {
|
|
68
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
69
|
+
...extra,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async get(path, query) {
|
|
73
|
+
const res = await fetch(buildUrl(this.apiUrl, path, query), {
|
|
74
|
+
headers: this.headers(),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
throw await parseError(res);
|
|
78
|
+
return (await res.json());
|
|
79
|
+
}
|
|
80
|
+
async getBuffer(path, query) {
|
|
81
|
+
const res = await fetch(buildUrl(this.apiUrl, path, query), {
|
|
82
|
+
headers: this.headers(),
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw await parseError(res);
|
|
86
|
+
return Buffer.from(await res.arrayBuffer());
|
|
87
|
+
}
|
|
88
|
+
async post(path, body) {
|
|
89
|
+
const res = await fetch(buildUrl(this.apiUrl, path), {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: this.headers({ 'Content-Type': 'application/json' }),
|
|
92
|
+
body: JSON.stringify(body ?? {}),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
throw await parseError(res);
|
|
96
|
+
return (await res.json());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { TelmeehClient } from '../client.js';
|
|
3
|
+
import { resolveConfig, writeStoredConfig, readStoredConfig, clearApiKey } from '../config.js';
|
|
4
|
+
import { wantsJson, printJson, success, fail, info } from '../output.js';
|
|
5
|
+
export function registerAuth(program) {
|
|
6
|
+
const auth = program.command('auth').description('Manage CLI authentication');
|
|
7
|
+
auth
|
|
8
|
+
.command('login')
|
|
9
|
+
.description('Save and validate your Telmeeh API key')
|
|
10
|
+
.option('-k, --key <key>', 'API key (tk_live_…). Omit to be prompted.')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const globals = program.opts();
|
|
13
|
+
// The global `--key` option captures `--key` passed after the subcommand,
|
|
14
|
+
// so check both the subcommand option and the global.
|
|
15
|
+
let key = opts.key || globals.key;
|
|
16
|
+
if (!key) {
|
|
17
|
+
if (!process.stdin.isTTY) {
|
|
18
|
+
fail('No API key provided. Pass --key tk_live_… (no interactive terminal available).');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const entered = await p.password({
|
|
22
|
+
message: 'Paste your Telmeeh API key (tk_live_…)',
|
|
23
|
+
validate: (v) => (v.startsWith('tk_live_') ? undefined : 'Keys start with tk_live_'),
|
|
24
|
+
});
|
|
25
|
+
if (p.isCancel(entered)) {
|
|
26
|
+
info('Cancelled.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
key = entered;
|
|
30
|
+
}
|
|
31
|
+
const client = new TelmeehClient({ apiKey: key, apiUrl: globals.apiUrl });
|
|
32
|
+
let me;
|
|
33
|
+
try {
|
|
34
|
+
me = await client.get('api/cli/whoami');
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const e = err;
|
|
38
|
+
if (e.status === 404) {
|
|
39
|
+
fail(`Could not validate key: the server at ${client.baseUrl} does not expose /api/cli/whoami yet. ` +
|
|
40
|
+
`Deploy the latest backend, or point at a server that has it (e.g. --api-url http://localhost:3000).`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
fail(`Could not validate key: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const cfg = resolveConfig({ apiUrl: globals.apiUrl });
|
|
49
|
+
const stored = readStoredConfig();
|
|
50
|
+
stored.apiKey = key;
|
|
51
|
+
if (globals.apiUrl)
|
|
52
|
+
stored.apiUrl = cfg.apiUrl;
|
|
53
|
+
writeStoredConfig(stored);
|
|
54
|
+
success(`Logged in as ${me.user.email} — team "${me.team.name}" (${me.team.plan})`);
|
|
55
|
+
});
|
|
56
|
+
auth
|
|
57
|
+
.command('logout')
|
|
58
|
+
.description('Remove the stored API key')
|
|
59
|
+
.action(() => {
|
|
60
|
+
clearApiKey();
|
|
61
|
+
success('Logged out. Stored API key removed.');
|
|
62
|
+
});
|
|
63
|
+
auth
|
|
64
|
+
.command('whoami')
|
|
65
|
+
.description('Show the currently authenticated identity')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
const globals = program.opts();
|
|
68
|
+
const client = new TelmeehClient({ apiKey: globals.key, apiUrl: globals.apiUrl });
|
|
69
|
+
const me = await client.get('api/cli/whoami');
|
|
70
|
+
if (wantsJson()) {
|
|
71
|
+
printJson(me);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
info(`Email: ${me.user.email}`);
|
|
75
|
+
info(`Team: ${me.team.name} (id ${me.team.id})`);
|
|
76
|
+
info(`Plan: ${me.team.plan}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TelmeehClient } from '../client.js';
|
|
2
|
+
import { wantsJson, printJson, printTable } from '../output.js';
|
|
3
|
+
function clientFrom(program) {
|
|
4
|
+
const g = program.opts();
|
|
5
|
+
return new TelmeehClient({ apiKey: g.key, apiUrl: g.apiUrl });
|
|
6
|
+
}
|
|
7
|
+
export function registerCategories(program) {
|
|
8
|
+
const cmd = program.command('categories').description('Browse your categories');
|
|
9
|
+
cmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.alias('ls')
|
|
12
|
+
.description('List all categories')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const client = clientFrom(program);
|
|
15
|
+
const data = await client.get('api/categories/list');
|
|
16
|
+
if (wantsJson()) {
|
|
17
|
+
printJson(data.categories);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
printTable(data.categories, [
|
|
21
|
+
['ID', (c) => c.id],
|
|
22
|
+
['NAME', (c) => c.name, 40],
|
|
23
|
+
['ITEMS', (c) => c.filesCount],
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { TelmeehClient } from '../client.js';
|
|
3
|
+
import { wantsJson, printJson, printTable, success, info } from '../output.js';
|
|
4
|
+
function clientFrom(program) {
|
|
5
|
+
const g = program.opts();
|
|
6
|
+
return new TelmeehClient({ apiKey: g.key, apiUrl: g.apiUrl });
|
|
7
|
+
}
|
|
8
|
+
/** Read `value` directly, or from stdin when value is "-". */
|
|
9
|
+
function readMaybeStdin(value) {
|
|
10
|
+
if (value === '-')
|
|
11
|
+
return readFileSync(0, 'utf8').trim();
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function printPromptList(data) {
|
|
15
|
+
if (wantsJson()) {
|
|
16
|
+
printJson(data.prompts);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
printTable(data.prompts, [
|
|
20
|
+
['ID', (p) => p.id],
|
|
21
|
+
['★', (p) => (p.starred ? '★' : '')],
|
|
22
|
+
['TITLE', (p) => p.title, 50],
|
|
23
|
+
['CATEGORY', (p) => p.categoryName ?? '', 20],
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
export function registerPrompts(program) {
|
|
27
|
+
const cmd = program.command('prompts').description('Manage your prompts');
|
|
28
|
+
cmd
|
|
29
|
+
.command('list')
|
|
30
|
+
.alias('ls')
|
|
31
|
+
.description('List prompts')
|
|
32
|
+
.option('--view <view>', 'all | starred | recent | uncategorized', 'all')
|
|
33
|
+
.option('--category <id>', 'filter by category id')
|
|
34
|
+
.option('--page <n>', 'page number', '1')
|
|
35
|
+
.option('--page-size <n>', 'results per page', '50')
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const client = clientFrom(program);
|
|
38
|
+
const data = await client.get('api/prompts/list', {
|
|
39
|
+
view: opts.view,
|
|
40
|
+
categoryId: opts.category,
|
|
41
|
+
page: opts.page,
|
|
42
|
+
pageSize: opts.pageSize,
|
|
43
|
+
});
|
|
44
|
+
printPromptList(data);
|
|
45
|
+
});
|
|
46
|
+
cmd
|
|
47
|
+
.command('search <query>')
|
|
48
|
+
.description('Search prompts by title/content')
|
|
49
|
+
.action(async (query) => {
|
|
50
|
+
const client = clientFrom(program);
|
|
51
|
+
const data = await client.get('api/prompts/list', { search: query });
|
|
52
|
+
printPromptList(data);
|
|
53
|
+
});
|
|
54
|
+
cmd
|
|
55
|
+
.command('get <id>')
|
|
56
|
+
.description('Show a prompt\'s full content')
|
|
57
|
+
.action(async (id) => {
|
|
58
|
+
const client = clientFrom(program);
|
|
59
|
+
const data = await client.get('api/prompts/get', {
|
|
60
|
+
id,
|
|
61
|
+
});
|
|
62
|
+
if (wantsJson()) {
|
|
63
|
+
printJson(data.prompt);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const p = data.prompt;
|
|
67
|
+
info(`# ${p.title} (id ${p.id}${p.categoryName ? `, ${p.categoryName}` : ''})`);
|
|
68
|
+
process.stdout.write(p.content + '\n');
|
|
69
|
+
});
|
|
70
|
+
cmd
|
|
71
|
+
.command('create')
|
|
72
|
+
.description('Create a new prompt')
|
|
73
|
+
.requiredOption('--title <title>', 'prompt title')
|
|
74
|
+
.requiredOption('--content <content|->', 'prompt content, or "-" to read stdin')
|
|
75
|
+
.option('--category <id>', 'category id')
|
|
76
|
+
.option('--starred', 'mark as starred')
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const client = clientFrom(program);
|
|
79
|
+
const data = await client.post('api/prompts/create', {
|
|
80
|
+
title: opts.title,
|
|
81
|
+
content: readMaybeStdin(opts.content),
|
|
82
|
+
categoryId: opts.category ? Number(opts.category) : undefined,
|
|
83
|
+
starred: Boolean(opts.starred),
|
|
84
|
+
});
|
|
85
|
+
if (wantsJson()) {
|
|
86
|
+
printJson(data.prompt);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
success(`Created prompt "${data.prompt.title}" (id ${data.prompt.id})`);
|
|
90
|
+
});
|
|
91
|
+
cmd
|
|
92
|
+
.command('improve <text|->')
|
|
93
|
+
.description('Run text through the Telmeeh AI improver')
|
|
94
|
+
.option('--context <context>', 'extra context for the improver')
|
|
95
|
+
.action(async (text, opts) => {
|
|
96
|
+
const client = clientFrom(program);
|
|
97
|
+
const data = await client.post('api/extension/improve', {
|
|
98
|
+
content: readMaybeStdin(text),
|
|
99
|
+
context: opts.context,
|
|
100
|
+
});
|
|
101
|
+
if (wantsJson()) {
|
|
102
|
+
printJson(data);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(data.improvedContent + '\n');
|
|
106
|
+
if (data.rateLimit?.remaining !== undefined) {
|
|
107
|
+
info(`Improvements remaining: ${data.rateLimit.remaining}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
|
+
import { TelmeehClient } from '../client.js';
|
|
5
|
+
import { wantsJson, printJson, printTable, success, info } from '../output.js';
|
|
6
|
+
import { runInstaller } from '../installer.js';
|
|
7
|
+
function clientFrom(program) {
|
|
8
|
+
const g = program.opts();
|
|
9
|
+
return new TelmeehClient({ apiKey: g.key, apiUrl: g.apiUrl });
|
|
10
|
+
}
|
|
11
|
+
function printSkillList(data) {
|
|
12
|
+
if (wantsJson()) {
|
|
13
|
+
printJson(data.skills);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
printTable(data.skills, [
|
|
17
|
+
['ID', (s) => s.id],
|
|
18
|
+
['NAME', (s) => s.name, 40],
|
|
19
|
+
['FILES', (s) => s.fileCount ?? ''],
|
|
20
|
+
['DESCRIPTION', (s) => s.description ?? '', 50],
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
/** Parse repeated `--file path=./local.md` flags into {path, content}. */
|
|
24
|
+
function collectFiles(value, acc) {
|
|
25
|
+
const eq = value.indexOf('=');
|
|
26
|
+
if (eq === -1) {
|
|
27
|
+
throw new Error(`--file expects "remotePath=localFile", got "${value}"`);
|
|
28
|
+
}
|
|
29
|
+
const path = value.slice(0, eq).trim();
|
|
30
|
+
const localFile = value.slice(eq + 1).trim();
|
|
31
|
+
acc.push({ path, content: readFileSync(localFile, 'utf8') });
|
|
32
|
+
return acc;
|
|
33
|
+
}
|
|
34
|
+
export function registerSkills(program) {
|
|
35
|
+
const cmd = program.command('skills').description('Manage your agent skills');
|
|
36
|
+
cmd
|
|
37
|
+
.command('list')
|
|
38
|
+
.alias('ls')
|
|
39
|
+
.description('List skills')
|
|
40
|
+
.option('--view <view>', 'all | starred | recent | uncategorized', 'all')
|
|
41
|
+
.option('--category <id>', 'filter by category id')
|
|
42
|
+
.action(async (opts) => {
|
|
43
|
+
const client = clientFrom(program);
|
|
44
|
+
const data = await client.get('api/skills/list', {
|
|
45
|
+
view: opts.view,
|
|
46
|
+
categoryId: opts.category,
|
|
47
|
+
});
|
|
48
|
+
printSkillList(data);
|
|
49
|
+
});
|
|
50
|
+
cmd
|
|
51
|
+
.command('search <query>')
|
|
52
|
+
.description('Search skills by name')
|
|
53
|
+
.action(async (query) => {
|
|
54
|
+
const client = clientFrom(program);
|
|
55
|
+
const data = await client.get('api/skills/list', { search: query });
|
|
56
|
+
printSkillList(data);
|
|
57
|
+
});
|
|
58
|
+
cmd
|
|
59
|
+
.command('get <id>')
|
|
60
|
+
.description('Show a skill\'s files')
|
|
61
|
+
.action(async (id) => {
|
|
62
|
+
const client = clientFrom(program);
|
|
63
|
+
const data = await client.get('api/skills/files', {
|
|
64
|
+
skillId: id,
|
|
65
|
+
});
|
|
66
|
+
if (wantsJson()) {
|
|
67
|
+
printJson(data.files);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const file of data.files) {
|
|
71
|
+
info(`\n=== ${file.path} ===`);
|
|
72
|
+
process.stdout.write(file.content + '\n');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
cmd
|
|
76
|
+
.command('create')
|
|
77
|
+
.description('Create a new skill')
|
|
78
|
+
.requiredOption('--name <name>', 'skill name')
|
|
79
|
+
.option('--desc <description>', 'short description')
|
|
80
|
+
.option('--category <id>', 'category id')
|
|
81
|
+
.action(async (opts) => {
|
|
82
|
+
const client = clientFrom(program);
|
|
83
|
+
const data = await client.post('api/skills/create', {
|
|
84
|
+
name: opts.name,
|
|
85
|
+
description: opts.desc,
|
|
86
|
+
categoryId: opts.category ? Number(opts.category) : undefined,
|
|
87
|
+
});
|
|
88
|
+
if (wantsJson()) {
|
|
89
|
+
printJson(data.skill);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
success(`Created skill "${data.skill.name}" (id ${data.skill.id})`);
|
|
93
|
+
});
|
|
94
|
+
cmd
|
|
95
|
+
.command('update <id>')
|
|
96
|
+
.description('Update a skill (metadata and/or files)')
|
|
97
|
+
.option('--name <name>', 'new name')
|
|
98
|
+
.option('--desc <description>', 'new description')
|
|
99
|
+
.option('--category <id>', 'new category id')
|
|
100
|
+
.option('--file <path=localFile>', 'attach a file (repeatable)', collectFiles, [])
|
|
101
|
+
.option('--note <note>', 'change note for the version history')
|
|
102
|
+
.action(async (id, opts) => {
|
|
103
|
+
const client = clientFrom(program);
|
|
104
|
+
await client.post('api/skills/update', {
|
|
105
|
+
id: Number(id),
|
|
106
|
+
name: opts.name,
|
|
107
|
+
description: opts.desc,
|
|
108
|
+
categoryId: opts.category ? Number(opts.category) : undefined,
|
|
109
|
+
files: opts.file.length > 0 ? opts.file : undefined,
|
|
110
|
+
changeNote: opts.note,
|
|
111
|
+
});
|
|
112
|
+
success(`Updated skill ${id}`);
|
|
113
|
+
});
|
|
114
|
+
cmd
|
|
115
|
+
.command('generate')
|
|
116
|
+
.description('AI-generate a complete skill from a goal')
|
|
117
|
+
.requiredOption('--goal <goal>', 'what the skill should do')
|
|
118
|
+
.option('--context <context>', 'extra context')
|
|
119
|
+
.option('--category <id>', 'category id')
|
|
120
|
+
.action(async (opts) => {
|
|
121
|
+
const client = clientFrom(program);
|
|
122
|
+
const data = await client.post('api/skills/generate', {
|
|
123
|
+
goal: opts.goal,
|
|
124
|
+
context: opts.context,
|
|
125
|
+
categoryId: opts.category ? Number(opts.category) : undefined,
|
|
126
|
+
});
|
|
127
|
+
if (wantsJson()) {
|
|
128
|
+
printJson(data);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
success(`Generated skill "${data.skill.name}" (id ${data.skill.id}) with ${data.files.length} file(s)`);
|
|
132
|
+
});
|
|
133
|
+
cmd
|
|
134
|
+
.command('export <id>')
|
|
135
|
+
.description('Download a skill and extract its files to a directory')
|
|
136
|
+
.option('--out <dir>', 'output directory', '.')
|
|
137
|
+
.action(async (id, opts) => {
|
|
138
|
+
const client = clientFrom(program);
|
|
139
|
+
const buffer = await client.getBuffer('api/skills/export', { skillId: id });
|
|
140
|
+
const outDir = resolve(opts.out);
|
|
141
|
+
const zip = new AdmZip(buffer);
|
|
142
|
+
zip.extractAllTo(outDir, /* overwrite */ true);
|
|
143
|
+
success(`Extracted skill ${id} to ${outDir}`);
|
|
144
|
+
});
|
|
145
|
+
cmd
|
|
146
|
+
.command('install')
|
|
147
|
+
.description('Install the Telmeeh agent skill into your AI assistant(s)')
|
|
148
|
+
.action(async () => {
|
|
149
|
+
await runInstaller({ interactive: true });
|
|
150
|
+
});
|
|
151
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
export const DEFAULT_API_URL = 'https://telmeeh.com';
|
|
5
|
+
function configDir() {
|
|
6
|
+
return join(homedir(), '.telmeeh');
|
|
7
|
+
}
|
|
8
|
+
function configPath() {
|
|
9
|
+
return join(configDir(), 'config.json');
|
|
10
|
+
}
|
|
11
|
+
export function readStoredConfig() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(configPath(), 'utf8');
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writeStoredConfig(config) {
|
|
21
|
+
const dir = configDir();
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
export function clearApiKey() {
|
|
28
|
+
const config = readStoredConfig();
|
|
29
|
+
delete config.apiKey;
|
|
30
|
+
if (Object.keys(config).length === 0) {
|
|
31
|
+
try {
|
|
32
|
+
rmSync(configPath());
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* ignore */
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
writeStoredConfig(config);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve config with precedence: explicit CLI flags > env vars > stored config > defaults.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveConfig(overrides = {}) {
|
|
45
|
+
const stored = readStoredConfig();
|
|
46
|
+
const apiKey = overrides.apiKey || process.env.TELMEEH_API_KEY || stored.apiKey || undefined;
|
|
47
|
+
const apiUrl = (overrides.apiUrl ||
|
|
48
|
+
process.env.TELMEEH_API_URL ||
|
|
49
|
+
stored.apiUrl ||
|
|
50
|
+
DEFAULT_API_URL).replace(/\/+$/, '');
|
|
51
|
+
return { apiKey, apiUrl };
|
|
52
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { ApiError, NotAuthenticatedError } from './client.js';
|
|
4
|
+
import { fail } from './output.js';
|
|
5
|
+
import { registerAuth } from './commands/auth.js';
|
|
6
|
+
import { registerCategories } from './commands/categories.js';
|
|
7
|
+
import { registerPrompts } from './commands/prompts.js';
|
|
8
|
+
import { registerSkills } from './commands/skills.js';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('telmeeh')
|
|
12
|
+
.description('Telmeeh CLI — manage your prompt & skill library from the terminal.')
|
|
13
|
+
.version('1.0.0')
|
|
14
|
+
.option('--json', 'output raw JSON (for scripting)')
|
|
15
|
+
.option('--api-url <url>', 'override the API base URL')
|
|
16
|
+
.option('--key <key>', 'override the API key for this command');
|
|
17
|
+
registerAuth(program);
|
|
18
|
+
registerCategories(program);
|
|
19
|
+
registerPrompts(program);
|
|
20
|
+
registerSkills(program);
|
|
21
|
+
async function run() {
|
|
22
|
+
try {
|
|
23
|
+
await program.parseAsync(process.argv);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err instanceof NotAuthenticatedError || err instanceof ApiError) {
|
|
27
|
+
fail(err.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
fail(err.message || 'Unexpected error');
|
|
31
|
+
if (process.env.TELMEEH_DEBUG)
|
|
32
|
+
console.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
void run();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
import { info, success } from './output.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
/** Path to the bundled skill source (package root: skills/telmeeh/SKILL.md). */
|
|
9
|
+
function bundledSkillPath() {
|
|
10
|
+
return join(__dirname, '..', 'skills', 'telmeeh', 'SKILL.md');
|
|
11
|
+
}
|
|
12
|
+
function buildTargets() {
|
|
13
|
+
const home = homedir();
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
id: 'claude-code',
|
|
17
|
+
label: 'Claude Code (~/.claude/skills/telmeeh/SKILL.md)',
|
|
18
|
+
detectDir: join(home, '.claude'),
|
|
19
|
+
dest: join(home, '.claude', 'skills', 'telmeeh', 'SKILL.md'),
|
|
20
|
+
format: 'skill',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'cursor',
|
|
24
|
+
label: 'Cursor (~/.cursor/rules/telmeeh.md)',
|
|
25
|
+
detectDir: join(home, '.cursor'),
|
|
26
|
+
dest: join(home, '.cursor', 'rules', 'telmeeh.md'),
|
|
27
|
+
format: 'rules',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'windsurf',
|
|
31
|
+
label: 'Windsurf (~/.codeium/windsurf/memories/telmeeh.md)',
|
|
32
|
+
detectDir: join(home, '.codeium', 'windsurf'),
|
|
33
|
+
dest: join(home, '.codeium', 'windsurf', 'memories', 'telmeeh.md'),
|
|
34
|
+
format: 'rules',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'project',
|
|
38
|
+
label: 'This project (./.claude/skills/telmeeh/SKILL.md)',
|
|
39
|
+
detectDir: process.cwd(),
|
|
40
|
+
dest: join(process.cwd(), '.claude', 'skills', 'telmeeh', 'SKILL.md'),
|
|
41
|
+
format: 'skill',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
/** Strip YAML frontmatter for agents that use plain-markdown rules. */
|
|
46
|
+
function toRules(skillMarkdown) {
|
|
47
|
+
return skillMarkdown.replace(/^---\n[\s\S]*?\n---\n+/, '');
|
|
48
|
+
}
|
|
49
|
+
function writeTarget(target, source) {
|
|
50
|
+
const content = target.format === 'rules' ? toRules(source) : source;
|
|
51
|
+
mkdirSync(dirname(target.dest), { recursive: true });
|
|
52
|
+
writeFileSync(target.dest, content, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
export async function runInstaller(options) {
|
|
55
|
+
const skillSrc = bundledSkillPath();
|
|
56
|
+
if (!existsSync(skillSrc)) {
|
|
57
|
+
info('Bundled Telmeeh skill not found; skipping skill installation.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const source = readFileSync(skillSrc, 'utf8');
|
|
61
|
+
const targets = buildTargets();
|
|
62
|
+
if (!options.interactive) {
|
|
63
|
+
info('Telmeeh agent skill is bundled. Run `telmeeh skills install` to add it to your AI assistant.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const detected = targets.filter((t) => existsSync(t.detectDir));
|
|
67
|
+
const choices = (detected.length > 0 ? detected : targets).map((t) => ({
|
|
68
|
+
value: t.id,
|
|
69
|
+
label: t.label,
|
|
70
|
+
hint: detected.includes(t) ? 'detected' : undefined,
|
|
71
|
+
}));
|
|
72
|
+
p.intro('Install the Telmeeh agent skill');
|
|
73
|
+
const selected = await p.multiselect({
|
|
74
|
+
message: 'Which AI assistant(s) should learn to use the Telmeeh CLI?',
|
|
75
|
+
options: choices,
|
|
76
|
+
required: false,
|
|
77
|
+
});
|
|
78
|
+
if (p.isCancel(selected) || !Array.isArray(selected) || selected.length === 0) {
|
|
79
|
+
p.outro('No assistants selected.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const id of selected) {
|
|
83
|
+
const target = targets.find((t) => t.id === id);
|
|
84
|
+
if (!target)
|
|
85
|
+
continue;
|
|
86
|
+
if (existsSync(target.dest)) {
|
|
87
|
+
const overwrite = await p.confirm({
|
|
88
|
+
message: `${target.dest} already exists. Overwrite?`,
|
|
89
|
+
initialValue: true,
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
92
|
+
info(`Skipped ${target.label}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
writeTarget(target, source);
|
|
97
|
+
success(`Installed → ${target.dest}`);
|
|
98
|
+
}
|
|
99
|
+
p.outro('Done. Ask your assistant to "use the Telmeeh CLI" to try it.');
|
|
100
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/** Whether the current invocation requested raw JSON output (`--json`). */
|
|
3
|
+
export function wantsJson() {
|
|
4
|
+
return process.argv.includes('--json');
|
|
5
|
+
}
|
|
6
|
+
export function printJson(data) {
|
|
7
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
8
|
+
}
|
|
9
|
+
export function info(msg) {
|
|
10
|
+
process.stderr.write(pc.dim(msg) + '\n');
|
|
11
|
+
}
|
|
12
|
+
export function success(msg) {
|
|
13
|
+
process.stderr.write(pc.green('✓ ') + msg + '\n');
|
|
14
|
+
}
|
|
15
|
+
export function fail(msg) {
|
|
16
|
+
process.stderr.write(pc.red('✗ ') + msg + '\n');
|
|
17
|
+
}
|
|
18
|
+
function truncate(value, max) {
|
|
19
|
+
const clean = value.replace(/\s+/g, ' ').trim();
|
|
20
|
+
return clean.length > max ? clean.slice(0, max - 1) + '…' : clean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Render an array of records as a simple aligned table to stdout.
|
|
24
|
+
* Columns: [header, accessor, maxWidth].
|
|
25
|
+
*/
|
|
26
|
+
export function printTable(rows, columns) {
|
|
27
|
+
if (rows.length === 0) {
|
|
28
|
+
info('No results.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const headers = columns.map((c) => c[0]);
|
|
32
|
+
const cells = rows.map((row) => columns.map(([, accessor, max]) => {
|
|
33
|
+
const raw = accessor(row);
|
|
34
|
+
const str = raw === null || raw === undefined ? '' : String(raw);
|
|
35
|
+
return max ? truncate(str, max) : str;
|
|
36
|
+
}));
|
|
37
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((r) => r[i].length)));
|
|
38
|
+
const renderRow = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(' ');
|
|
39
|
+
process.stdout.write(pc.bold(renderRow(headers)) + '\n');
|
|
40
|
+
process.stdout.write(pc.dim(widths.map((w) => '─'.repeat(w)).join(' ')) + '\n');
|
|
41
|
+
for (const row of cells) {
|
|
42
|
+
process.stdout.write(renderRow(row) + '\n');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runInstaller } from './installer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs automatically after `npm i -g @telmeeh/cli`.
|
|
4
|
+
*
|
|
5
|
+
* Only prompts when attached to an interactive TTY and not in CI — otherwise
|
|
6
|
+
* it prints a hint and exits cleanly so package installs never hang or fail.
|
|
7
|
+
*/
|
|
8
|
+
async function main() {
|
|
9
|
+
const isCI = Boolean(process.env.CI);
|
|
10
|
+
const interactive = process.stdout.isTTY === true && process.stdin.isTTY === true && !isCI;
|
|
11
|
+
try {
|
|
12
|
+
await runInstaller({ interactive });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Never let a postinstall failure break the package installation.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
void main();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@otoreach/telmeeh-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Telmeeh command-line tool — manage your prompt & skill library and AI agents from the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"telmeeh": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"clean": "rimraf dist",
|
|
21
|
+
"postinstall": "node dist/postinstall.js || true"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"telmeeh",
|
|
25
|
+
"cli",
|
|
26
|
+
"prompts",
|
|
27
|
+
"ai",
|
|
28
|
+
"agent",
|
|
29
|
+
"skills"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@clack/prompts": "^0.7.0",
|
|
34
|
+
"adm-zip": "^0.5.16",
|
|
35
|
+
"commander": "^12.1.0",
|
|
36
|
+
"picocolors": "^1.1.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/adm-zip": "^0.5.7",
|
|
40
|
+
"@types/node": "^22.10.0",
|
|
41
|
+
"rimraf": "^6.0.1",
|
|
42
|
+
"tsx": "^4.19.2",
|
|
43
|
+
"typescript": "^5.7.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telmeeh
|
|
3
|
+
description: Use the Telmeeh CLI to search, fetch, create, and improve the user's prompt and skill library from the terminal. Trigger when the user mentions Telmeeh, asks to find/reuse one of their saved prompts, save a new prompt, improve a prompt, or work with their agent skills.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Telmeeh CLI
|
|
7
|
+
|
|
8
|
+
Telmeeh is the user's prompt & skill library. The `telmeeh` CLI exposes it from the
|
|
9
|
+
terminal. Prefer it whenever the user wants to reuse, save, or improve prompts/skills
|
|
10
|
+
instead of writing them from scratch.
|
|
11
|
+
|
|
12
|
+
## Authentication
|
|
13
|
+
|
|
14
|
+
Commands require a logged-in API key. If a command fails with an auth error, tell the
|
|
15
|
+
user to run `telmeeh auth login` (it prompts for a `tk_live_…` key from
|
|
16
|
+
Settings → API Keys in the Telmeeh dashboard). Check status with `telmeeh auth whoami`.
|
|
17
|
+
|
|
18
|
+
Add `--json` to any command to get machine-readable output you can parse.
|
|
19
|
+
|
|
20
|
+
## Common commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Categories
|
|
24
|
+
telmeeh categories list
|
|
25
|
+
|
|
26
|
+
# Prompts
|
|
27
|
+
telmeeh prompts list # newest first
|
|
28
|
+
telmeeh prompts search "cold email" # full-text search
|
|
29
|
+
telmeeh prompts get <id> # full content of one prompt
|
|
30
|
+
telmeeh prompts create --title "T" --content - # content from stdin
|
|
31
|
+
telmeeh prompts improve "draft text" # AI-improve text (rate limited)
|
|
32
|
+
|
|
33
|
+
# Skills (reusable agent instruction packages)
|
|
34
|
+
telmeeh skills list
|
|
35
|
+
telmeeh skills search "summarize"
|
|
36
|
+
telmeeh skills get <id> # print every file in the skill
|
|
37
|
+
telmeeh skills generate --goal "review PRs" # AI-generate a new skill
|
|
38
|
+
telmeeh skills export <id> --out ./skill # download + unzip files locally
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Recommended workflow
|
|
42
|
+
|
|
43
|
+
1. **Before writing a prompt from scratch**, run `telmeeh prompts search "<topic>"` to
|
|
44
|
+
see if the user already has one. If a match exists, `telmeeh prompts get <id>` and
|
|
45
|
+
reuse/adapt it.
|
|
46
|
+
2. **When the user crafts a good prompt**, offer to save it with `telmeeh prompts create`.
|
|
47
|
+
3. **To sharpen a rough prompt**, pipe it through `telmeeh prompts improve`.
|
|
48
|
+
4. **For repeatable multi-file instructions**, use skills: search first, then
|
|
49
|
+
`skills export` to pull files locally or `skills generate` to create a new one.
|
|
50
|
+
|
|
51
|
+
Always pass `--json` when you need to read values (ids, content) programmatically.
|