@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 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.