docstar-cli 1.0.1 → 1.1.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 +55 -21
- package/dist/commands/complete.d.ts +9 -0
- package/dist/commands/complete.js +20 -0
- package/dist/commands/{hello/world.d.ts → completion-script.d.ts} +1 -3
- package/dist/commands/completion-script.js +39 -0
- package/dist/commands/init.js +49 -22
- package/dist/commands/list.d.ts +13 -0
- package/dist/commands/list.js +71 -0
- package/dist/commands/mcp.js +31 -20
- package/dist/lib/codegen.d.ts +1 -2
- package/dist/lib/codegen.js +13 -37
- package/dist/lib/complete.d.ts +2 -0
- package/dist/lib/complete.js +30 -0
- package/dist/lib/http.d.ts +2 -2
- package/dist/lib/http.js +39 -6
- package/dist/lib/paths.d.ts +2 -1
- package/dist/lib/paths.js +2 -1
- package/dist/lib/types.d.ts +12 -6
- package/oclif.manifest.json +80 -46
- package/package.json +5 -7
- package/dist/commands/hello/index.d.ts +0 -12
- package/dist/commands/hello/index.js +0 -19
- package/dist/commands/hello/world.js +0 -14
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# docstar-cli
|
|
2
2
|
|
|
3
|
-
A CLI for calling published DocStar API endpoints directly from your terminal, without leaving the shell to read docs. Point it at a DocStar docs site, pick which modules (e.g. "Slack") you want, and it turns each published endpoint into a real command you can run.
|
|
3
|
+
A CLI for calling published DocStar API endpoints directly from your terminal, without leaving the shell to read docs. Point it at a DocStar docs site, pick which modules (e.g. "Slack") you want, and it turns each published endpoint into a real command you can run. You can install from multiple docs sites/collections at once — each is kept separate under its own **CLI name**.
|
|
4
|
+
|
|
5
|
+
## Concepts: collection → module → endpoint
|
|
6
|
+
|
|
7
|
+
Every command follows a fixed three-level hierarchy:
|
|
8
|
+
|
|
9
|
+
1. **Collection** — a docs site, identified by its **CLI name** (e.g. `msg91`). This is what `docstar-cli init` installs.
|
|
10
|
+
2. **Module** — a top-level section within that collection (e.g. `slack`).
|
|
11
|
+
3. **Endpoint** — one published API within a module, turned into a command (e.g. `send-message-1`).
|
|
12
|
+
|
|
13
|
+
Put together: `docstar-cli <collection> <module> <endpoint> [--flags]`, e.g. `docstar-cli msg91 slack send-message-1 --message "hi"`.
|
|
4
14
|
|
|
5
15
|
## Install
|
|
6
16
|
|
|
@@ -26,48 +36,70 @@ For a local/non-custom-domain docs site, pass the collection id explicitly:
|
|
|
26
36
|
docstar-cli init localhost:3000 --collectionId paM4R4A26Hvb
|
|
27
37
|
```
|
|
28
38
|
|
|
29
|
-
This fetches the list of published modules for that
|
|
39
|
+
This fetches the list of published modules for that collection and shows an interactive checklist (nothing checked by default — press `space` to toggle a module, or select the **"Select all modules"** entry at the bottom of the list to install everything, then `enter` to confirm). For each selected module it:
|
|
30
40
|
|
|
31
|
-
- saves its full endpoint contract to `~/.docstar/modules/<module>.json`
|
|
32
|
-
- registers a real CLI command for every endpoint
|
|
33
|
-
- registers a `<module> list` command
|
|
41
|
+
- saves its full endpoint contract to `~/.docstar/modules/<cli-name>/<module>.json`
|
|
42
|
+
- registers a real CLI command for every endpoint: `docstar-cli <cli-name> <module> <command>`
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
The **CLI name** comes from the collection's settings (set it under the collection's Domain settings in the docs editor — lowercase letters, numbers, `-` and `_` only; auto-generated from the collection name if not set yet). Running `init` against a different domain/collection adds it alongside any others already installed — nothing gets overwritten.
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
Everything is saved to `~/.docstar/config.json`, grouped by collection.
|
|
47
|
+
|
|
48
|
+
### 2. See what's installed
|
|
38
49
|
|
|
39
50
|
```sh
|
|
40
|
-
docstar-cli
|
|
51
|
+
docstar-cli list # every installed collection
|
|
52
|
+
docstar-cli list msg91 # modules installed for the "msg91" collection
|
|
53
|
+
docstar-cli list msg91 slack # Slack's installed endpoints in "msg91"
|
|
41
54
|
```
|
|
42
55
|
|
|
43
56
|
```
|
|
44
57
|
send-message-1
|
|
45
58
|
send message
|
|
46
59
|
POST
|
|
47
|
-
required:
|
|
60
|
+
required: message
|
|
48
61
|
```
|
|
49
62
|
|
|
50
63
|
### 3. Call an endpoint
|
|
51
64
|
|
|
52
|
-
Pass required parameters as flags:
|
|
53
|
-
|
|
54
65
|
```sh
|
|
55
|
-
docstar-cli slack send-message-1 --
|
|
66
|
+
docstar-cli msg91 slack send-message-1 --message "hello from the CLI"
|
|
56
67
|
```
|
|
57
68
|
|
|
58
|
-
Or omit
|
|
69
|
+
Or omit required parameters and answer the prompts instead:
|
|
59
70
|
|
|
60
71
|
```sh
|
|
61
|
-
docstar-cli slack send-message-1
|
|
62
|
-
? Enter value for
|
|
72
|
+
docstar-cli msg91 slack send-message-1
|
|
73
|
+
? Enter value for message
|
|
63
74
|
```
|
|
64
75
|
|
|
65
76
|
The command prints the response status and body from the actual API call.
|
|
66
77
|
|
|
78
|
+
### 4. Shell tab-completion
|
|
79
|
+
|
|
80
|
+
This is a purpose-built completion, not a generic "complete every command" plugin: it **only**
|
|
81
|
+
completes a module name (once you've typed a collection) and an endpoint name (once you've typed
|
|
82
|
+
a collection and module). It never completes the collection name itself, and never completes
|
|
83
|
+
top-level commands like `list`/`init`/`mcp` — you always type the collection by hand.
|
|
84
|
+
|
|
85
|
+
Set it up once (bash, or zsh with `bashcompinit` loaded):
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
echo 'eval "$(docstar-cli completion-script)"' >> ~/.bashrc # or ~/.zshrc
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then, after typing a known collection name, `Tab` completes modules and endpoints:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
docstar-cli msg91 sla<Tab> # -> slack
|
|
95
|
+
docstar-cli msg91 slack send<Tab> # -> send-message-1
|
|
96
|
+
```
|
|
97
|
+
|
|
67
98
|
## Use from an AI client (MCP)
|
|
68
99
|
|
|
69
100
|
`docstar-cli` can run as an [MCP](https://modelcontextprotocol.io) server, turning every installed
|
|
70
|
-
endpoint into a proper tool an AI client can call directly — no
|
|
101
|
+
endpoint (across all installed collections) into a proper tool an AI client can call directly — no
|
|
102
|
+
shell access required.
|
|
71
103
|
|
|
72
104
|
```sh
|
|
73
105
|
docstar-cli mcp
|
|
@@ -86,17 +118,19 @@ Register it with Claude Code (`.mcp.json`) or Claude Desktop (`claude_desktop_co
|
|
|
86
118
|
}
|
|
87
119
|
```
|
|
88
120
|
|
|
89
|
-
Each installed endpoint becomes one tool named `<module>__<
|
|
90
|
-
|
|
91
|
-
|
|
121
|
+
Each installed endpoint becomes one tool named `<collection>__<module>__<endpoint>` (e.g.
|
|
122
|
+
`msg91__slack__send-message-1`) — the same collection → module → endpoint hierarchy described
|
|
123
|
+
above, just joined with `__` instead of spaces because MCP tool names can't contain spaces.
|
|
124
|
+
Required/optional parameters are exposed in the tool's schema. A prompt like "call the slack api
|
|
125
|
+
and send a message saying hi" resolves directly to that tool call — the AI client asks you for any
|
|
92
126
|
required value it can't infer, exactly like any other MCP tool. Run `docstar-cli init` first so
|
|
93
127
|
there's at least one installed module for `mcp` to expose.
|
|
94
128
|
|
|
95
129
|
## How it works
|
|
96
130
|
|
|
97
|
-
- `docstar-cli init` talks to `GET /p/module.json?collectionId=...` (list of modules) and `GET /p/<module>/module.json?collectionId=...` (a module's endpoint contract) on the docs site.
|
|
131
|
+
- `docstar-cli init` talks to `GET /p/module.json?collectionId=...` (the collection's name, CLI name, and list of modules) and `GET /p/<module>/module.json?collectionId=...` (a module's endpoint contract) on the docs site.
|
|
98
132
|
- Each endpoint's contract (method, url, base url, headers, body, and CLI parameter names) is saved locally and used to build the real HTTP request when you run its command — no network round-trip to the docs site is needed to run a command once a module is installed.
|
|
99
|
-
- Commands are generated as real files on disk under this repo's `dist/commands/<module>/`, which is why `npm run build` (it wipes `dist/`) requires re-running `docstar-cli init` afterwards to reinstall your modules.
|
|
133
|
+
- Commands are generated as real files on disk under this repo's `dist/commands/<cli-name>/<module>/`, which is why `npm run build` (it wipes `dist/`) requires re-running `docstar-cli init` for each installed collection afterwards to reinstall your modules.
|
|
100
134
|
|
|
101
135
|
## Development
|
|
102
136
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Complete extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
collection: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
module: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static hidden: boolean;
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { completeEndpoints, completeModules } from '../lib/complete.js';
|
|
3
|
+
// Hidden helper invoked by the shell completion function (see `completion-script`), not meant to
|
|
4
|
+
// be run directly. Prints one suggestion per line: module names for `docstar-cli complete
|
|
5
|
+
// <collection>`, endpoint command names/aliases for `docstar-cli complete <collection> <module>`.
|
|
6
|
+
// Never suggests a collection name or a top-level command — completion only starts once the
|
|
7
|
+
// collection has already been typed.
|
|
8
|
+
export default class Complete extends Command {
|
|
9
|
+
static args = {
|
|
10
|
+
collection: Args.string({ required: true }),
|
|
11
|
+
module: Args.string({ required: false }),
|
|
12
|
+
};
|
|
13
|
+
static hidden = true;
|
|
14
|
+
async run() {
|
|
15
|
+
const { args } = await this.parse(Complete);
|
|
16
|
+
const suggestions = args.module ? await completeEndpoints(args.collection, args.module) : await completeModules(args.collection);
|
|
17
|
+
for (const suggestion of suggestions)
|
|
18
|
+
this.log(suggestion);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export default class
|
|
3
|
-
static args: {};
|
|
2
|
+
export default class CompletionScript extends Command {
|
|
4
3
|
static description: string;
|
|
5
4
|
static examples: string[];
|
|
6
|
-
static flags: {};
|
|
7
5
|
run(): Promise<void>;
|
|
8
6
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
const scriptFor = (bin) => {
|
|
3
|
+
const fnName = `_${bin.replaceAll(/[^a-zA-Z0-9]/g, '_')}_complete`;
|
|
4
|
+
return `# zsh doesn't support the bash-style \`complete\` builtin used below until compinit (for
|
|
5
|
+
# compdef) and bashcompinit (for complete/compgen) are both loaded, in that order.
|
|
6
|
+
if [ -n "$ZSH_VERSION" ]; then
|
|
7
|
+
autoload -Uz compinit && compinit -u
|
|
8
|
+
autoload -U +X bashcompinit && bashcompinit
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
${fnName}() {
|
|
12
|
+
local cur collection module
|
|
13
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
14
|
+
collection="\${COMP_WORDS[1]}"
|
|
15
|
+
module="\${COMP_WORDS[2]}"
|
|
16
|
+
|
|
17
|
+
COMPREPLY=()
|
|
18
|
+
|
|
19
|
+
if [ "$COMP_CWORD" -eq 2 ] && [ -n "$collection" ]; then
|
|
20
|
+
COMPREPLY=( $(compgen -W "$(${bin} complete "$collection" 2>/dev/null)" -- "$cur") )
|
|
21
|
+
elif [ "$COMP_CWORD" -eq 3 ] && [ -n "$collection" ] && [ -n "$module" ]; then
|
|
22
|
+
COMPREPLY=( $(compgen -W "$(${bin} complete "$collection" "$module" 2>/dev/null)" -- "$cur") )
|
|
23
|
+
fi
|
|
24
|
+
# COMP_CWORD 0/1 (the binary name and the collection itself) intentionally get no
|
|
25
|
+
# suggestions — the collection is always typed out by hand.
|
|
26
|
+
}
|
|
27
|
+
complete -F ${fnName} ${bin}
|
|
28
|
+
`;
|
|
29
|
+
};
|
|
30
|
+
export default class CompletionScript extends Command {
|
|
31
|
+
static description = [
|
|
32
|
+
'Print a shell completion function that completes ONLY module names (once a collection is typed) and endpoint names (once a collection and module are typed).',
|
|
33
|
+
'It never completes the collection name itself, and never completes top-level commands like `list`/`init`/`mcp` — those are typed out by hand.',
|
|
34
|
+
].join('\n');
|
|
35
|
+
static examples = ['echo \'eval "$(<%= config.bin %> <%= command.id %>)"\' >> ~/.bashrc # or ~/.zshrc'];
|
|
36
|
+
async run() {
|
|
37
|
+
this.log(scriptFor(this.config.bin));
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { checkbox } from '@inquirer/prompts';
|
|
2
2
|
import { Args, Command, Flags } from '@oclif/core';
|
|
3
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { generateCommandFile
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { generateCommandFile } from '../lib/codegen.js';
|
|
5
5
|
import { configDir, configPath, modulePath } from '../lib/paths.js';
|
|
6
6
|
const buildBaseUrl = (domain) => {
|
|
7
7
|
if (domain.startsWith('http://') || domain.startsWith('https://'))
|
|
@@ -15,11 +15,23 @@ const fetchJson = async (url) => {
|
|
|
15
15
|
throw new Error(`Request to ${url} failed (${response.status} ${response.statusText})`);
|
|
16
16
|
return (await response.json());
|
|
17
17
|
};
|
|
18
|
+
const readSavedConfig = async () => {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(configPath(), 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { collections: [] };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
18
26
|
export default class Init extends Command {
|
|
19
27
|
static args = {
|
|
20
28
|
domain: Args.string({ description: 'DocStar docs domain, e.g. docs.msg91.com or localhost:3000', required: true }),
|
|
21
29
|
};
|
|
22
|
-
static description =
|
|
30
|
+
static description = [
|
|
31
|
+
'Discover published modules for a DocStar docs site, pick which ones to install, and register their CLI commands.',
|
|
32
|
+
'You can run this against multiple docs sites/collections — each is kept separate under its own CLI name (`docstar-cli <cli-name> <module> <command>`).',
|
|
33
|
+
'After installing, run `docstar-cli list` to see what got installed.',
|
|
34
|
+
].join('\n');
|
|
23
35
|
static examples = [
|
|
24
36
|
'<%= config.bin %> <%= command.id %> docs.msg91.com',
|
|
25
37
|
'<%= config.bin %> <%= command.id %> localhost:3000 --collectionId paM4R4A26Hvb',
|
|
@@ -35,21 +47,30 @@ export default class Init extends Command {
|
|
|
35
47
|
listUrl.searchParams.set('collectionId', flags.collectionId);
|
|
36
48
|
this.log(`Fetching modules from ${listUrl.toString()} ...`);
|
|
37
49
|
const listResponse = await fetchJson(listUrl.toString());
|
|
50
|
+
if (!listResponse.collection?.cliName) {
|
|
51
|
+
this.error('The docs site did not return a CLI name for this collection. Please update hitman-api/hitman-ui, or set one in the collection settings.');
|
|
52
|
+
}
|
|
38
53
|
if (!listResponse.modules || listResponse.modules.length === 0) {
|
|
39
54
|
this.log('No published modules found.');
|
|
40
55
|
return;
|
|
41
56
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const { cliName } = listResponse.collection;
|
|
58
|
+
const SELECT_ALL = '__select_all__';
|
|
59
|
+
const rawSelection = await checkbox({
|
|
60
|
+
choices: [
|
|
61
|
+
...listResponse.modules.map((module) => {
|
|
62
|
+
const count = module.endpointCount ?? module.endpoints?.length ?? 0;
|
|
63
|
+
return {
|
|
64
|
+
checked: false,
|
|
65
|
+
name: `${module.name} (${module.path}) — ${count} endpoint${count === 1 ? '' : 's'}`,
|
|
66
|
+
value: module.path,
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
{ checked: false, name: '➤ Select all modules', value: SELECT_ALL },
|
|
70
|
+
],
|
|
71
|
+
message: `Select modules to install for "${cliName}" (space to toggle, enter to confirm)`,
|
|
52
72
|
});
|
|
73
|
+
const selectedPaths = rawSelection.includes(SELECT_ALL) ? listResponse.modules.map((module) => module.path) : rawSelection;
|
|
53
74
|
if (selectedPaths.length === 0) {
|
|
54
75
|
this.log('No modules selected, nothing installed.');
|
|
55
76
|
return;
|
|
@@ -61,26 +82,32 @@ export default class Init extends Command {
|
|
|
61
82
|
if (flags.collectionId)
|
|
62
83
|
moduleUrl.searchParams.set('collectionId', flags.collectionId);
|
|
63
84
|
this.log(`Fetching ${path} ...`);
|
|
85
|
+
// eslint-disable-next-line no-await-in-loop
|
|
64
86
|
const moduleJson = await fetchJson(moduleUrl.toString());
|
|
65
|
-
|
|
66
|
-
await
|
|
87
|
+
// eslint-disable-next-line no-await-in-loop
|
|
88
|
+
await mkdir(configDir() + `/modules/${cliName}`, { recursive: true });
|
|
89
|
+
// eslint-disable-next-line no-await-in-loop
|
|
90
|
+
await writeFile(modulePath(cliName, path), JSON.stringify(moduleJson, null, 2));
|
|
67
91
|
for (const endpoint of moduleJson.endpoints) {
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
// eslint-disable-next-line no-await-in-loop
|
|
93
|
+
await generateCommandFile(this.config.root, cliName, path, endpoint);
|
|
94
|
+
this.log(` registered command: ${this.config.bin} ${cliName} ${path} ${endpoint.cli.command.name}`);
|
|
70
95
|
}
|
|
71
|
-
await generateListCommandFile(this.config.root, path);
|
|
72
|
-
this.log(` registered command: ${this.config.bin} ${path} list`);
|
|
73
96
|
selectedModules.push({ name: moduleJson.module.name, path });
|
|
74
97
|
}
|
|
75
|
-
const
|
|
98
|
+
const collectionConfig = {
|
|
76
99
|
baseUrl,
|
|
77
|
-
|
|
100
|
+
cliName,
|
|
78
101
|
collectionId: flags.collectionId ?? null,
|
|
79
102
|
domain: args.domain,
|
|
80
103
|
modules: selectedModules,
|
|
104
|
+
name: listResponse.collection.name,
|
|
81
105
|
updatedAt: new Date().toISOString(),
|
|
82
106
|
};
|
|
107
|
+
const savedConfig = await readSavedConfig();
|
|
108
|
+
const otherCollections = savedConfig.collections.filter((collection) => collection.cliName !== cliName);
|
|
109
|
+
savedConfig.collections = [...otherCollections, collectionConfig];
|
|
83
110
|
await writeFile(configPath(), JSON.stringify(savedConfig, null, 2));
|
|
84
|
-
this.log(`\nInstalled ${selectedModules.length} module(s). Config saved to ${configPath()}`);
|
|
111
|
+
this.log(`\nInstalled ${selectedModules.length} module(s) for "${cliName}". Config saved to ${configPath()}`);
|
|
85
112
|
}
|
|
86
113
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class List extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
collection: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
module: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { loadModule } from '../lib/http.js';
|
|
4
|
+
import { configPath } from '../lib/paths.js';
|
|
5
|
+
export default class List extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
collection: Args.string({ description: 'Show modules installed for this collection (CLI name), e.g. msg91', required: false }),
|
|
8
|
+
module: Args.string({ description: 'Show all endpoints for this module within the collection, e.g. slack', required: false }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'List installed collections, the modules within one, or every endpoint of one module';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %> # list every installed collection',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --all # same as above',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> msg91 # list modules installed for the "msg91" collection',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> msg91 slack # list all of Slack\'s installed endpoints in "msg91"',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
all: Flags.boolean({ description: 'List every installed collection (default when no arguments are given)' }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { args } = await this.parse(List);
|
|
22
|
+
let savedConfig;
|
|
23
|
+
try {
|
|
24
|
+
savedConfig = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
this.error('No installed modules found. Run `docstar-cli init <domain>` first.');
|
|
28
|
+
}
|
|
29
|
+
if (savedConfig.collections.length === 0) {
|
|
30
|
+
this.log('No collections installed. Run `docstar-cli init <domain>` to install one.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!args.collection) {
|
|
34
|
+
for (const collection of savedConfig.collections) {
|
|
35
|
+
this.log(`${collection.name} (${collection.cliName}) — ${collection.domain} — ${collection.modules.length} module${collection.modules.length === 1 ? '' : 's'}`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const collection = savedConfig.collections.find((c) => c.cliName === args.collection);
|
|
40
|
+
if (!collection) {
|
|
41
|
+
this.error(`Collection "${args.collection}" isn't installed. Run \`docstar-cli list\` to see installed collections.`);
|
|
42
|
+
}
|
|
43
|
+
if (!args.module) {
|
|
44
|
+
for (const module of collection.modules) {
|
|
45
|
+
// eslint-disable-next-line no-await-in-loop
|
|
46
|
+
const moduleJson = await loadModule(collection.cliName, module.path);
|
|
47
|
+
const count = moduleJson.endpoints.length;
|
|
48
|
+
this.log(`${module.name} (${module.path}) — ${count} endpoint${count === 1 ? '' : 's'}`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const moduleJson = await loadModule(collection.cliName, args.module);
|
|
53
|
+
if (moduleJson.endpoints.length === 0) {
|
|
54
|
+
this.log(`No endpoints found for module "${args.module}".`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const endpoint of moduleJson.endpoints) {
|
|
58
|
+
const { cli } = endpoint;
|
|
59
|
+
const aliases = cli.command.aliases.filter((alias) => alias !== cli.command.name);
|
|
60
|
+
this.log(`${cli.command.name}${aliases.length > 0 ? ` (aliases: ${aliases.join(', ')})` : ''}`);
|
|
61
|
+
if (cli.description)
|
|
62
|
+
this.log(` ${cli.description}`);
|
|
63
|
+
this.log(` ${endpoint.method}`);
|
|
64
|
+
if (cli.parameters.required.length > 0)
|
|
65
|
+
this.log(` required: ${cli.parameters.required.join(', ')}`);
|
|
66
|
+
if (cli.parameters.optional.length > 0)
|
|
67
|
+
this.log(` optional: ${cli.parameters.optional.join(', ')}`);
|
|
68
|
+
this.log('');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -15,8 +15,17 @@ const buildInputShape = (required, optional) => {
|
|
|
15
15
|
return shape;
|
|
16
16
|
};
|
|
17
17
|
export default class Mcp extends Command {
|
|
18
|
-
static description =
|
|
19
|
-
|
|
18
|
+
static description = [
|
|
19
|
+
'Start an MCP (Model Context Protocol) server exposing every installed endpoint (across all installed collections) as a callable tool for AI clients (Claude Desktop, Claude Code, etc.).',
|
|
20
|
+
'Every tool follows the collection -> module -> endpoint hierarchy: e.g. collection "msg91" -> module "slack" -> endpoint "send-message-1" becomes the tool `msg91__slack__send-message-1`.',
|
|
21
|
+
'Run `docstar-cli init <domain>` first so there is at least one installed module to expose.',
|
|
22
|
+
'This command does not exit on its own — it stays running and communicates over stdio for as long as the connecting AI client keeps the connection open.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
static examples = [
|
|
25
|
+
'<%= config.bin %> <%= command.id %>',
|
|
26
|
+
'# Then register it in an MCP client config, e.g. .mcp.json:',
|
|
27
|
+
'# { "mcpServers": { "docstar": { "command": "docstar-cli", "args": ["mcp"] } } }',
|
|
28
|
+
];
|
|
20
29
|
async run() {
|
|
21
30
|
let savedConfig;
|
|
22
31
|
try {
|
|
@@ -27,24 +36,26 @@ export default class Mcp extends Command {
|
|
|
27
36
|
}
|
|
28
37
|
const server = new McpServer({ name: 'docstar-cli', version: '1.0.0' });
|
|
29
38
|
let toolCount = 0;
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
for (const collection of savedConfig.collections) {
|
|
40
|
+
for (const module of collection.modules) {
|
|
41
|
+
// eslint-disable-next-line no-await-in-loop
|
|
42
|
+
const moduleJson = await loadModule(collection.cliName, module.path);
|
|
43
|
+
for (const endpoint of moduleJson.endpoints) {
|
|
44
|
+
const { cli } = endpoint;
|
|
45
|
+
const toolName = sanitize(`${collection.cliName}__${module.path}__${cli.command.name}`);
|
|
46
|
+
const description = `[collection: ${collection.cliName} / module: ${module.path}] ${cli.description || cli.command.name}`;
|
|
47
|
+
const inputShape = buildInputShape(cli.parameters.required, cli.parameters.optional);
|
|
48
|
+
server.tool(toolName, description, inputShape, async (args) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await executeCliCommand(collection.cliName, module.path, cli.command.name, args);
|
|
51
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return { content: [{ type: 'text', text: error.message }], isError: true };
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
toolCount++;
|
|
58
|
+
}
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
if (toolCount === 0) {
|
package/dist/lib/codegen.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import type { EndpointContract } from './types.js';
|
|
2
|
-
export declare const generateCommandFile: (cliRoot: string, modulePathSegment: string, endpoint: EndpointContract) => Promise<string>;
|
|
3
|
-
export declare const generateListCommandFile: (cliRoot: string, modulePathSegment: string) => Promise<string>;
|
|
2
|
+
export declare const generateCommandFile: (cliRoot: string, cliName: string, modulePathSegment: string, endpoint: EndpointContract) => Promise<string>;
|
package/dist/lib/codegen.js
CHANGED
|
@@ -2,21 +2,25 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const flagLine = (name, required) =>
|
|
4
4
|
// Required params are collected interactively when missing, so the flag itself stays optional to oclif.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Param names come from endpoint body/param keys authored in a rich-text editor and can contain
|
|
6
|
+
// quotes or HTML markup (e.g. a stray `<span text-block='true'>...`) — JSON.stringify escapes
|
|
7
|
+
// that safely for embedding in generated source; naive string interpolation does not and can
|
|
8
|
+
// produce invalid JS.
|
|
9
|
+
` ${JSON.stringify(name)}: Flags.string({description: ${JSON.stringify(`${required ? 'Required' : 'Optional'} parameter`)}}),`;
|
|
10
|
+
const commandFileContent = (cliName, modulePathSegment, endpoint) => {
|
|
7
11
|
const { cli } = endpoint;
|
|
8
12
|
const flagLines = [
|
|
9
13
|
...cli.parameters.required.map((name) => flagLine(name, true)),
|
|
10
14
|
...cli.parameters.optional.map((name) => flagLine(name, false)),
|
|
11
15
|
].join('\n');
|
|
12
16
|
return `import {Command, Flags} from '@oclif/core'
|
|
13
|
-
import {executeCliCommand} from '
|
|
14
|
-
import {promptForMissingParams} from '
|
|
17
|
+
import {executeCliCommand} from '../../../lib/http.js'
|
|
18
|
+
import {promptForMissingParams} from '../../../lib/prompt.js'
|
|
15
19
|
|
|
16
20
|
const REQUIRED_PARAMS = ${JSON.stringify(cli.parameters.required)}
|
|
17
21
|
|
|
18
22
|
export default class Generated extends Command {
|
|
19
|
-
static description = ${JSON.stringify(cli.description || `${cli.name} (${modulePathSegment})`)}
|
|
23
|
+
static description = ${JSON.stringify(cli.description || `${cli.name} (${cliName} ${modulePathSegment})`)}
|
|
20
24
|
static aliases = ${JSON.stringify(cli.command.aliases.filter((alias) => alias !== cli.name))}
|
|
21
25
|
static flags = {
|
|
22
26
|
${flagLines}
|
|
@@ -32,7 +36,7 @@ ${flagLines}
|
|
|
32
36
|
Object.assign(params, await promptForMissingParams(REQUIRED_PARAMS, params))
|
|
33
37
|
|
|
34
38
|
try {
|
|
35
|
-
const result = await executeCliCommand(
|
|
39
|
+
const result = await executeCliCommand(${JSON.stringify(cliName)}, ${JSON.stringify(modulePathSegment)}, ${JSON.stringify(cli.command.name)}, params)
|
|
36
40
|
this.log(JSON.stringify(result, null, 2))
|
|
37
41
|
} catch (error) {
|
|
38
42
|
this.error(error.message)
|
|
@@ -41,38 +45,10 @@ ${flagLines}
|
|
|
41
45
|
}
|
|
42
46
|
`;
|
|
43
47
|
};
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export default class GeneratedList extends Command {
|
|
48
|
-
static description = 'List installed APIs for the ${modulePathSegment} module'
|
|
49
|
-
|
|
50
|
-
async run() {
|
|
51
|
-
const moduleJson = await loadModule('${modulePathSegment}')
|
|
52
|
-
|
|
53
|
-
for (const endpoint of moduleJson.endpoints) {
|
|
54
|
-
const {cli} = endpoint
|
|
55
|
-
this.log(\`\${cli.command.name}\${cli.command.aliases.filter((alias) => alias !== cli.command.name).length > 0 ? \` (aliases: \${cli.command.aliases.filter((alias) => alias !== cli.command.name).join(', ')})\` : ''}\`)
|
|
56
|
-
if (cli.description) this.log(\` \${cli.description}\`)
|
|
57
|
-
this.log(\` \${endpoint.method}\`)
|
|
58
|
-
if (cli.parameters.required.length > 0) this.log(\` required: \${cli.parameters.required.join(', ')}\`)
|
|
59
|
-
if (cli.parameters.optional.length > 0) this.log(\` optional: \${cli.parameters.optional.join(', ')}\`)
|
|
60
|
-
this.log('')
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
export const generateCommandFile = async (cliRoot, modulePathSegment, endpoint) => {
|
|
66
|
-
const dir = join(cliRoot, 'dist', 'commands', modulePathSegment);
|
|
48
|
+
export const generateCommandFile = async (cliRoot, cliName, modulePathSegment, endpoint) => {
|
|
49
|
+
const dir = join(cliRoot, 'dist', 'commands', cliName, modulePathSegment);
|
|
67
50
|
await mkdir(dir, { recursive: true });
|
|
68
51
|
const filePath = join(dir, `${endpoint.cli.command.name}.js`);
|
|
69
|
-
await writeFile(filePath, commandFileContent(modulePathSegment, endpoint));
|
|
70
|
-
return filePath;
|
|
71
|
-
};
|
|
72
|
-
export const generateListCommandFile = async (cliRoot, modulePathSegment) => {
|
|
73
|
-
const dir = join(cliRoot, 'dist', 'commands', modulePathSegment);
|
|
74
|
-
await mkdir(dir, { recursive: true });
|
|
75
|
-
const filePath = join(dir, 'list.js');
|
|
76
|
-
await writeFile(filePath, listCommandFileContent(modulePathSegment));
|
|
52
|
+
await writeFile(filePath, commandFileContent(cliName, modulePathSegment, endpoint));
|
|
77
53
|
return filePath;
|
|
78
54
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { loadModule } from './http.js';
|
|
3
|
+
import { configPath } from './paths.js';
|
|
4
|
+
const readSavedConfig = async () => {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(await readFile(configPath(), 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { collections: [] };
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
// Suggestions for shell completion — intentionally scoped to module names (given a collection)
|
|
13
|
+
// and endpoint command names/aliases (given a collection + module). Never suggests collection
|
|
14
|
+
// names or top-level commands (list/init/mcp/...); the user is expected to type the collection
|
|
15
|
+
// themselves.
|
|
16
|
+
export const completeModules = async (cliName) => {
|
|
17
|
+
const savedConfig = await readSavedConfig();
|
|
18
|
+
const collection = savedConfig.collections.find((c) => c.cliName === cliName);
|
|
19
|
+
return collection ? collection.modules.map((module) => module.path) : [];
|
|
20
|
+
};
|
|
21
|
+
export const completeEndpoints = async (cliName, modulePathSegment) => {
|
|
22
|
+
try {
|
|
23
|
+
const moduleJson = await loadModule(cliName, modulePathSegment);
|
|
24
|
+
const names = moduleJson.endpoints.flatMap((endpoint) => [endpoint.cli.command.name, ...endpoint.cli.command.aliases]);
|
|
25
|
+
return [...new Set(names)];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
};
|
package/dist/lib/http.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EndpointContract, ModuleJson } from './types.js';
|
|
2
2
|
export declare const stripHtml: (value: string | null | undefined) => string;
|
|
3
|
-
export declare const loadModule: (modulePathSegment: string) => Promise<ModuleJson>;
|
|
3
|
+
export declare const loadModule: (cliName: string, modulePathSegment: string) => Promise<ModuleJson>;
|
|
4
4
|
export declare const findEndpoint: (moduleJson: ModuleJson, commandName: string) => EndpointContract | undefined;
|
|
5
|
-
export declare const executeCliCommand: (modulePathSegment: string, commandName: string, params: Record<string, string>) => Promise<{
|
|
5
|
+
export declare const executeCliCommand: (cliName: string, modulePathSegment: string, commandName: string, params: Record<string, string>) => Promise<{
|
|
6
6
|
status: number;
|
|
7
7
|
body: unknown;
|
|
8
8
|
}>;
|
package/dist/lib/http.js
CHANGED
|
@@ -4,8 +4,8 @@ import { modulePath } from './paths.js';
|
|
|
4
4
|
// `<span text-block="true">...</span>`-style markup; unwrap to the raw value.
|
|
5
5
|
export const stripHtml = (value) => (value ?? '').replaceAll(/<[^>]*>/g, '');
|
|
6
6
|
const substitutePlaceholders = (template, params) => Object.entries(params).reduce((result, [key, value]) => result.replaceAll(`:${key}`, value).replaceAll(`{${key}}`, value).replaceAll(`{{${key}}}`, value), template);
|
|
7
|
-
export const loadModule = async (modulePathSegment) => {
|
|
8
|
-
const raw = await readFile(modulePath(modulePathSegment), 'utf8');
|
|
7
|
+
export const loadModule = async (cliName, modulePathSegment) => {
|
|
8
|
+
const raw = await readFile(modulePath(cliName, modulePathSegment), 'utf8');
|
|
9
9
|
return JSON.parse(raw);
|
|
10
10
|
};
|
|
11
11
|
export const findEndpoint = (moduleJson, commandName) => moduleJson.endpoints.find((endpoint) => endpoint.cli.command.name === commandName || endpoint.cli.command.aliases.includes(commandName));
|
|
@@ -17,14 +17,47 @@ const buildUrl = (endpoint, params) => {
|
|
|
17
17
|
return `${baseUrl}${path}`;
|
|
18
18
|
};
|
|
19
19
|
const buildHeaders = (endpoint, params) => {
|
|
20
|
+
// Header names are case-insensitive; the docs editor can end up storing two entries that only
|
|
21
|
+
// differ by case (e.g. "Content-Type" and "content-type"). Sending both as separate header
|
|
22
|
+
// lines has been observed to break body parsing on the receiving server, so dedupe by
|
|
23
|
+
// lowercased name — first entry wins, using its original casing for readability.
|
|
24
|
+
const seenLowerNames = new Set();
|
|
20
25
|
const headers = {};
|
|
21
26
|
for (const [rawKey, rawValue] of Object.entries(endpoint.headers || {})) {
|
|
22
27
|
const key = stripHtml(rawKey);
|
|
28
|
+
const lowerKey = key.toLowerCase();
|
|
29
|
+
if (seenLowerNames.has(lowerKey))
|
|
30
|
+
continue;
|
|
31
|
+
seenLowerNames.add(lowerKey);
|
|
23
32
|
const value = stripHtml(rawValue?.value ?? rawValue);
|
|
24
33
|
headers[key] = params[key] ?? value;
|
|
25
34
|
}
|
|
26
35
|
return headers;
|
|
27
36
|
};
|
|
37
|
+
// CLI flag values always arrive as strings, but the JSON body template's existing value for a
|
|
38
|
+
// key tells us its real type (boolean/number/array) — coerce back to that shape instead of
|
|
39
|
+
// clobbering e.g. `getUrl: true` with the literal string "true", or `collectionIds: []` with a
|
|
40
|
+
// plain string.
|
|
41
|
+
const coerceToTemplateType = (existingValue, rawValue) => {
|
|
42
|
+
if (typeof existingValue === 'boolean')
|
|
43
|
+
return rawValue === 'true';
|
|
44
|
+
if (typeof existingValue === 'number') {
|
|
45
|
+
const parsedNumber = Number(rawValue);
|
|
46
|
+
return Number.isNaN(parsedNumber) ? rawValue : parsedNumber;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(existingValue)) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(rawValue);
|
|
51
|
+
if (Array.isArray(parsed))
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// not a JSON array literal — fall through to comma-splitting
|
|
56
|
+
}
|
|
57
|
+
return rawValue.split(',').map((item) => item.trim());
|
|
58
|
+
}
|
|
59
|
+
return rawValue;
|
|
60
|
+
};
|
|
28
61
|
const buildBody = (endpoint, params) => {
|
|
29
62
|
const rawValue = endpoint.body?.raw?.value;
|
|
30
63
|
if (endpoint.body?.type !== 'JSON' || !rawValue)
|
|
@@ -38,15 +71,15 @@ const buildBody = (endpoint, params) => {
|
|
|
38
71
|
}
|
|
39
72
|
for (const [key, value] of Object.entries(params)) {
|
|
40
73
|
if (key in parsed)
|
|
41
|
-
parsed[key] = value;
|
|
74
|
+
parsed[key] = coerceToTemplateType(parsed[key], value);
|
|
42
75
|
}
|
|
43
76
|
return JSON.stringify(parsed);
|
|
44
77
|
};
|
|
45
|
-
export const executeCliCommand = async (modulePathSegment, commandName, params) => {
|
|
46
|
-
const moduleJson = await loadModule(modulePathSegment);
|
|
78
|
+
export const executeCliCommand = async (cliName, modulePathSegment, commandName, params) => {
|
|
79
|
+
const moduleJson = await loadModule(cliName, modulePathSegment);
|
|
47
80
|
const endpoint = findEndpoint(moduleJson, commandName);
|
|
48
81
|
if (!endpoint) {
|
|
49
|
-
throw new Error(`No endpoint found for command "${commandName}" in module "${modulePathSegment}"`);
|
|
82
|
+
throw new Error(`No endpoint found for command "${commandName}" in module "${cliName} ${modulePathSegment}"`);
|
|
50
83
|
}
|
|
51
84
|
const url = buildUrl(endpoint, params);
|
|
52
85
|
const headers = buildHeaders(endpoint, params);
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const configDir: () => string;
|
|
2
2
|
export declare const configPath: () => string;
|
|
3
3
|
export declare const modulesDir: () => string;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const collectionModulesDir: (cliName: string) => string;
|
|
5
|
+
export declare const modulePath: (cliName: string, modulePathSegment: string) => string;
|
package/dist/lib/paths.js
CHANGED
|
@@ -3,4 +3,5 @@ import { join } from 'node:path';
|
|
|
3
3
|
export const configDir = () => join(homedir(), '.docstar');
|
|
4
4
|
export const configPath = () => join(configDir(), 'config.json');
|
|
5
5
|
export const modulesDir = () => join(configDir(), 'modules');
|
|
6
|
-
export const
|
|
6
|
+
export const collectionModulesDir = (cliName) => join(modulesDir(), cliName);
|
|
7
|
+
export const modulePath = (cliName, modulePathSegment) => join(collectionModulesDir(cliName), `${modulePathSegment}.json`);
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -41,14 +41,20 @@ export type ModuleSummary = {
|
|
|
41
41
|
path: string;
|
|
42
42
|
}[];
|
|
43
43
|
};
|
|
44
|
-
export type
|
|
45
|
-
|
|
44
|
+
export type CollectionSummary = {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
cliName: string;
|
|
48
|
+
};
|
|
49
|
+
export type CollectionConfig = {
|
|
50
|
+
cliName: string;
|
|
51
|
+
name: string;
|
|
46
52
|
domain: string;
|
|
53
|
+
baseUrl: string;
|
|
47
54
|
collectionId: string | null;
|
|
48
|
-
collection: {
|
|
49
|
-
id: string;
|
|
50
|
-
name: string;
|
|
51
|
-
} | null;
|
|
52
55
|
modules: ModuleSummary[];
|
|
53
56
|
updatedAt: string;
|
|
54
57
|
};
|
|
58
|
+
export type SavedConfig = {
|
|
59
|
+
collections: CollectionConfig[];
|
|
60
|
+
};
|
package/oclif.manifest.json
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"commands": {
|
|
3
|
-
"
|
|
3
|
+
"complete": {
|
|
4
4
|
"aliases": [],
|
|
5
5
|
"args": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"name": "domain",
|
|
6
|
+
"collection": {
|
|
7
|
+
"name": "collection",
|
|
9
8
|
"required": true
|
|
9
|
+
},
|
|
10
|
+
"module": {
|
|
11
|
+
"name": "module",
|
|
12
|
+
"required": false
|
|
10
13
|
}
|
|
11
14
|
},
|
|
12
|
-
"
|
|
13
|
-
"examples": [
|
|
14
|
-
"<%= config.bin %> <%= command.id %> docs.msg91.com",
|
|
15
|
-
"<%= config.bin %> <%= command.id %> localhost:3000 --collectionId paM4R4A26Hvb"
|
|
16
|
-
],
|
|
17
|
-
"flags": {
|
|
18
|
-
"collectionId": {
|
|
19
|
-
"description": "Collection id (required when the domain is not a custom domain, e.g. localhost)",
|
|
20
|
-
"name": "collectionId",
|
|
21
|
-
"hasDynamicHelp": false,
|
|
22
|
-
"multiple": false,
|
|
23
|
-
"type": "option"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
15
|
+
"flags": {},
|
|
26
16
|
"hasDynamicHelp": false,
|
|
17
|
+
"hidden": true,
|
|
27
18
|
"hiddenAliases": [],
|
|
28
|
-
"id": "
|
|
19
|
+
"id": "complete",
|
|
29
20
|
"pluginAlias": "docstar-cli",
|
|
30
21
|
"pluginName": "docstar-cli",
|
|
31
22
|
"pluginType": "core",
|
|
@@ -35,20 +26,20 @@
|
|
|
35
26
|
"relativePath": [
|
|
36
27
|
"dist",
|
|
37
28
|
"commands",
|
|
38
|
-
"
|
|
29
|
+
"complete.js"
|
|
39
30
|
]
|
|
40
31
|
},
|
|
41
|
-
"
|
|
32
|
+
"completion-script": {
|
|
42
33
|
"aliases": [],
|
|
43
34
|
"args": {},
|
|
44
|
-
"description": "
|
|
35
|
+
"description": "Print a shell completion function that completes ONLY module names (once a collection is typed) and endpoint names (once a collection and module are typed).\nIt never completes the collection name itself, and never completes top-level commands like `list`/`init`/`mcp` — those are typed out by hand.",
|
|
45
36
|
"examples": [
|
|
46
|
-
"<%= config.bin %> <%= command.id %>"
|
|
37
|
+
"echo 'eval \"$(<%= config.bin %> <%= command.id %>)\"' >> ~/.bashrc # or ~/.zshrc"
|
|
47
38
|
],
|
|
48
39
|
"flags": {},
|
|
49
40
|
"hasDynamicHelp": false,
|
|
50
41
|
"hiddenAliases": [],
|
|
51
|
-
"id": "
|
|
42
|
+
"id": "completion-script",
|
|
52
43
|
"pluginAlias": "docstar-cli",
|
|
53
44
|
"pluginName": "docstar-cli",
|
|
54
45
|
"pluginType": "core",
|
|
@@ -58,28 +49,27 @@
|
|
|
58
49
|
"relativePath": [
|
|
59
50
|
"dist",
|
|
60
51
|
"commands",
|
|
61
|
-
"
|
|
52
|
+
"completion-script.js"
|
|
62
53
|
]
|
|
63
54
|
},
|
|
64
|
-
"
|
|
55
|
+
"init": {
|
|
65
56
|
"aliases": [],
|
|
66
57
|
"args": {
|
|
67
|
-
"
|
|
68
|
-
"description": "
|
|
69
|
-
"name": "
|
|
58
|
+
"domain": {
|
|
59
|
+
"description": "DocStar docs domain, e.g. docs.msg91.com or localhost:3000",
|
|
60
|
+
"name": "domain",
|
|
70
61
|
"required": true
|
|
71
62
|
}
|
|
72
63
|
},
|
|
73
|
-
"description": "
|
|
64
|
+
"description": "Discover published modules for a DocStar docs site, pick which ones to install, and register their CLI commands.\nYou can run this against multiple docs sites/collections — each is kept separate under its own CLI name (`docstar-cli <cli-name> <module> <command>`).\nAfter installing, run `docstar-cli list` to see what got installed.",
|
|
74
65
|
"examples": [
|
|
75
|
-
"<%= config.bin %> <%= command.id %>
|
|
66
|
+
"<%= config.bin %> <%= command.id %> docs.msg91.com",
|
|
67
|
+
"<%= config.bin %> <%= command.id %> localhost:3000 --collectionId paM4R4A26Hvb"
|
|
76
68
|
],
|
|
77
69
|
"flags": {
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"name": "from",
|
|
82
|
-
"required": true,
|
|
70
|
+
"collectionId": {
|
|
71
|
+
"description": "Collection id (required when the domain is not a custom domain, e.g. localhost)",
|
|
72
|
+
"name": "collectionId",
|
|
83
73
|
"hasDynamicHelp": false,
|
|
84
74
|
"multiple": false,
|
|
85
75
|
"type": "option"
|
|
@@ -87,7 +77,7 @@
|
|
|
87
77
|
},
|
|
88
78
|
"hasDynamicHelp": false,
|
|
89
79
|
"hiddenAliases": [],
|
|
90
|
-
"id": "
|
|
80
|
+
"id": "init",
|
|
91
81
|
"pluginAlias": "docstar-cli",
|
|
92
82
|
"pluginName": "docstar-cli",
|
|
93
83
|
"pluginType": "core",
|
|
@@ -97,21 +87,66 @@
|
|
|
97
87
|
"relativePath": [
|
|
98
88
|
"dist",
|
|
99
89
|
"commands",
|
|
100
|
-
"
|
|
101
|
-
"index.js"
|
|
90
|
+
"init.js"
|
|
102
91
|
]
|
|
103
92
|
},
|
|
104
|
-
"
|
|
93
|
+
"list": {
|
|
94
|
+
"aliases": [],
|
|
95
|
+
"args": {
|
|
96
|
+
"collection": {
|
|
97
|
+
"description": "Show modules installed for this collection (CLI name), e.g. msg91",
|
|
98
|
+
"name": "collection",
|
|
99
|
+
"required": false
|
|
100
|
+
},
|
|
101
|
+
"module": {
|
|
102
|
+
"description": "Show all endpoints for this module within the collection, e.g. slack",
|
|
103
|
+
"name": "module",
|
|
104
|
+
"required": false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"description": "List installed collections, the modules within one, or every endpoint of one module",
|
|
108
|
+
"examples": [
|
|
109
|
+
"<%= config.bin %> <%= command.id %> # list every installed collection",
|
|
110
|
+
"<%= config.bin %> <%= command.id %> --all # same as above",
|
|
111
|
+
"<%= config.bin %> <%= command.id %> msg91 # list modules installed for the \"msg91\" collection",
|
|
112
|
+
"<%= config.bin %> <%= command.id %> msg91 slack # list all of Slack's installed endpoints in \"msg91\""
|
|
113
|
+
],
|
|
114
|
+
"flags": {
|
|
115
|
+
"all": {
|
|
116
|
+
"description": "List every installed collection (default when no arguments are given)",
|
|
117
|
+
"name": "all",
|
|
118
|
+
"allowNo": false,
|
|
119
|
+
"type": "boolean"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"hasDynamicHelp": false,
|
|
123
|
+
"hiddenAliases": [],
|
|
124
|
+
"id": "list",
|
|
125
|
+
"pluginAlias": "docstar-cli",
|
|
126
|
+
"pluginName": "docstar-cli",
|
|
127
|
+
"pluginType": "core",
|
|
128
|
+
"strict": true,
|
|
129
|
+
"enableJsonFlag": false,
|
|
130
|
+
"isESM": true,
|
|
131
|
+
"relativePath": [
|
|
132
|
+
"dist",
|
|
133
|
+
"commands",
|
|
134
|
+
"list.js"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"mcp": {
|
|
105
138
|
"aliases": [],
|
|
106
139
|
"args": {},
|
|
107
|
-
"description": "
|
|
140
|
+
"description": "Start an MCP (Model Context Protocol) server exposing every installed endpoint (across all installed collections) as a callable tool for AI clients (Claude Desktop, Claude Code, etc.).\nEvery tool follows the collection -> module -> endpoint hierarchy: e.g. collection \"msg91\" -> module \"slack\" -> endpoint \"send-message-1\" becomes the tool `msg91__slack__send-message-1`.\nRun `docstar-cli init <domain>` first so there is at least one installed module to expose.\nThis command does not exit on its own — it stays running and communicates over stdio for as long as the connecting AI client keeps the connection open.",
|
|
108
141
|
"examples": [
|
|
109
|
-
"<%= config.bin %> <%= command.id
|
|
142
|
+
"<%= config.bin %> <%= command.id %>",
|
|
143
|
+
"# Then register it in an MCP client config, e.g. .mcp.json:",
|
|
144
|
+
"# { \"mcpServers\": { \"docstar\": { \"command\": \"docstar-cli\", \"args\": [\"mcp\"] } } }"
|
|
110
145
|
],
|
|
111
146
|
"flags": {},
|
|
112
147
|
"hasDynamicHelp": false,
|
|
113
148
|
"hiddenAliases": [],
|
|
114
|
-
"id": "
|
|
149
|
+
"id": "mcp",
|
|
115
150
|
"pluginAlias": "docstar-cli",
|
|
116
151
|
"pluginName": "docstar-cli",
|
|
117
152
|
"pluginType": "core",
|
|
@@ -121,10 +156,9 @@
|
|
|
121
156
|
"relativePath": [
|
|
122
157
|
"dist",
|
|
123
158
|
"commands",
|
|
124
|
-
"
|
|
125
|
-
"world.js"
|
|
159
|
+
"mcp.js"
|
|
126
160
|
]
|
|
127
161
|
}
|
|
128
162
|
},
|
|
129
|
-
"version": "1.0
|
|
163
|
+
"version": "1.1.0"
|
|
130
164
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docstar-cli",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "1.0
|
|
3
|
+
"description": "Call published DocStar API endpoints directly from your terminal, or expose them as MCP tools for AI clients.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"author": "Swayam Maheshwari",
|
|
6
6
|
"bin": {
|
|
7
7
|
"docstar-cli": "./bin/run.js"
|
|
@@ -56,11 +56,9 @@
|
|
|
56
56
|
"@oclif/plugin-plugins"
|
|
57
57
|
],
|
|
58
58
|
"topicSeparator": " ",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
59
|
+
"additionalVersionFlags": [
|
|
60
|
+
"-v"
|
|
61
|
+
]
|
|
64
62
|
},
|
|
65
63
|
"repository": "swayammaheshwari/docstar-cli",
|
|
66
64
|
"scripts": {
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
export default class Hello extends Command {
|
|
3
|
-
static args: {
|
|
4
|
-
person: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
-
};
|
|
6
|
-
static description: string;
|
|
7
|
-
static examples: string[];
|
|
8
|
-
static flags: {
|
|
9
|
-
from: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
};
|
|
11
|
-
run(): Promise<void>;
|
|
12
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
-
export default class Hello extends Command {
|
|
3
|
-
static args = {
|
|
4
|
-
person: Args.string({ description: 'Person to say hello to', required: true }),
|
|
5
|
-
};
|
|
6
|
-
static description = 'Say hello';
|
|
7
|
-
static examples = [
|
|
8
|
-
`<%= config.bin %> <%= command.id %> friend --from oclif
|
|
9
|
-
hello friend from oclif! (./src/commands/hello/index.ts)
|
|
10
|
-
`,
|
|
11
|
-
];
|
|
12
|
-
static flags = {
|
|
13
|
-
from: Flags.string({ char: 'f', description: 'Who is saying hello', required: true }),
|
|
14
|
-
};
|
|
15
|
-
async run() {
|
|
16
|
-
const { args, flags } = await this.parse(Hello);
|
|
17
|
-
this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
export default class World extends Command {
|
|
3
|
-
static args = {};
|
|
4
|
-
static description = 'Say hello world';
|
|
5
|
-
static examples = [
|
|
6
|
-
`<%= config.bin %> <%= command.id %>
|
|
7
|
-
hello world! (./src/commands/hello/world.ts)
|
|
8
|
-
`,
|
|
9
|
-
];
|
|
10
|
-
static flags = {};
|
|
11
|
-
async run() {
|
|
12
|
-
this.log('hello world! (./src/commands/hello/world.ts)');
|
|
13
|
-
}
|
|
14
|
-
}
|