@hzlmy2002/web-market 0.1.0 → 0.1.2
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 +16 -6
- package/dist/cli.js +21 -6
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +71 -0
- package/dist/install.d.ts +2 -0
- package/dist/install.js +37 -7
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,15 +4,25 @@ Node/TypeScript stdio MCP for focused website competitive analysis. Five tools c
|
|
|
4
4
|
|
|
5
5
|
## Install with npx
|
|
6
6
|
|
|
7
|
-
Requires Node 22 or newer.
|
|
7
|
+
Requires Node 22 or newer. Run one of these commands in a terminal:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx -y @hzlmy2002/web-market@0.1.
|
|
11
|
-
npx -y @hzlmy2002/web-market@0.1.
|
|
12
|
-
npx -y @hzlmy2002/web-market@0.1.
|
|
10
|
+
npx -y @hzlmy2002/web-market@0.1.2 setup --client codex
|
|
11
|
+
npx -y @hzlmy2002/web-market@0.1.2 setup --client claude-code
|
|
12
|
+
npx -y @hzlmy2002/web-market@0.1.2 setup --client hermes
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
This installs the Skill and registers a version-pinned `npx -y @hzlmy2002/web-market@0.1.
|
|
15
|
+
This installs the Skill and registers a version-pinned `npx -y @hzlmy2002/web-market@0.1.2 serve` MCP command. Clearing the npm cache does not invalidate the configured path: npx downloads the pinned release again when necessary. Restart or refresh the client to discover the Skill. If no key is configured, setup prompts for hidden input and saves the key in the selected client’s MCP `env.AISA_API_KEY` setting. This works independently of shell startup files on macOS, Linux, and Windows. Existing saved keys are reused. If `AISA_API_KEY` is already set in the setup environment, setup keeps using environment-based authentication without copying the key; the client must inherit that variable. Non-interactive setup without a saved or environment key exits with instructions. Use `uninstall --client ...` to remove an unchanged managed installation.
|
|
16
|
+
|
|
17
|
+
When testing the published release from this source repository, first change to another directory (for example, `cd ~`). npm exec/npx can select the current project when its name and version match the requested package, but a source checkout has no installed `aisa-web-market` command link. This results in `sh: aisa-web-market: command not found`. Alternatively, use the local setup command below after building.
|
|
18
|
+
|
|
19
|
+
## Automatic client detection
|
|
20
|
+
|
|
21
|
+
Run `npx -y @hzlmy2002/web-market@0.1.2 setup` to install for every detected client. From a built source checkout, use `node dist/cli.js setup`. Detection checks `.codex/`, `.claude/` or `.claude.json`, and `.hermes/` in the user's home directory (or `--home`). These are usage traces, not proof that the executable is still installed. Shared `.agents/` directories alone do not count as detection.
|
|
22
|
+
|
|
23
|
+
Use `--client` to select a single client or install before its first run. If nothing is detected, setup explains how to proceed without creating client configurations. Custom client configuration roots are not discovered automatically. A newly entered key is requested once and reused for clients needing a key; existing client keys remain in place. Installation results are reported per client; a failure does not undo successful installations, and any failure produces a nonzero exit status. Uninstall still requires `--client`.
|
|
24
|
+
|
|
25
|
+
Automatic detection is available starting with version 0.1.2. Windows uses the same home-directory markers, but the full workflow has not yet been tested on a Windows machine.
|
|
16
26
|
|
|
17
27
|
## Local setup
|
|
18
28
|
|
|
@@ -24,7 +34,7 @@ npm test
|
|
|
24
34
|
npm run build
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
Setup prompts for a missing AIsa API key and stores it as plaintext in the selected client configuration, with restrictive file permissions where supported. It is never printed or copied into Skills or installer state. This is a temporary Bearer authentication flow ahead of OAuth support. Setup does not edit shell profiles or Windows user environment variables. Alternatively, set `AISA_API_KEY` in the environment inherited by your MCP client. Desktop applications may not inherit variables exported in a terminal.
|
|
28
38
|
|
|
29
39
|
Run one of these from this checkout:
|
|
30
40
|
|
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { sharedSetupKeyResolver } from './credentials.js';
|
|
2
3
|
import { parseArgs } from 'node:util';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
5
|
import { createServer } from './server.js';
|
|
5
|
-
import { install } from './install.js';
|
|
6
|
+
import { detectClients, install } from './install.js';
|
|
6
7
|
async function main() {
|
|
7
8
|
const { values, positionals } = parseArgs({ allowPositionals: true, options: { client: { type: 'string' }, home: { type: 'string' }, 'install-skills': { type: 'boolean' }, help: { type: 'boolean' } } });
|
|
8
9
|
const command = positionals[0] ?? 'serve';
|
|
9
10
|
if (values.help) {
|
|
10
|
-
console.log('aisa-web-market serve [--install-skills --client codex|claude-code|hermes]\naisa-web-market setup|uninstall --client codex|claude-code|hermes [--home directory]\naisa-web-market status\
|
|
11
|
+
console.log('aisa-web-market serve [--install-skills --client codex|claude-code|hermes]\naisa-web-market setup [--client codex|claude-code|hermes] [--home directory]\naisa-web-market uninstall --client codex|claude-code|hermes [--home directory]\naisa-web-market status\nsetup detects all supported clients in your home directory when --client is omitted. Set AISA_API_KEY in the server environment. setup prompts for a missing key and saves it in the client MCP configuration.');
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
if (positionals.length > 1)
|
|
14
15
|
throw Error('Unexpected positional argument.');
|
|
15
16
|
if (command === 'status') {
|
|
16
|
-
console.log(JSON.stringify({ credential_configured: Boolean(process.env.AISA_API_KEY?.trim()), authentication: 'bearer', version: '0.1.
|
|
17
|
+
console.log(JSON.stringify({ credential_configured: Boolean(process.env.AISA_API_KEY?.trim()), authentication: 'bearer', version: '0.1.2' }));
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
if (command === 'setup' || command === 'uninstall') {
|
|
21
|
+
if (command === 'uninstall' && !values.client)
|
|
22
|
+
throw Error('--client is required for uninstall.');
|
|
23
|
+
const clients = values.client ? [values.client] : await detectClients(values.home);
|
|
24
|
+
if (!clients.length)
|
|
25
|
+
throw Error('No supported clients detected. Run a client once, or use setup --client codex|claude-code|hermes.');
|
|
20
26
|
if (!values.client)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
console.error(`Detected clients: ${clients.join(', ')}`);
|
|
28
|
+
const resolveKey = sharedSetupKeyResolver();
|
|
29
|
+
for (const client of clients) {
|
|
30
|
+
try {
|
|
31
|
+
console.log(JSON.stringify(await install(client, { home: values.home, remove: command === 'uninstall', resolveKey: command === 'setup' ? resolveKey : undefined })));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`${client}: ${error instanceof Error ? error.message : 'Installation failed.'}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log('Client configuration preserves existing values; serialization may reformat comments. A saved key is passed directly to the MCP process; otherwise ensure the client inherits AISA_API_KEY. Restart or refresh the client to load skills.');
|
|
24
39
|
return;
|
|
25
40
|
}
|
|
26
41
|
if (command !== 'serve')
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function promptKey(): Promise<string>;
|
|
2
|
+
export declare function resolveSetupKey(savedKey?: string): Promise<string | undefined>;
|
|
3
|
+
export declare function sharedSetupKeyResolver(resolve?: typeof resolveSetupKey): (savedKey?: string) => Promise<string | undefined>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
2
|
+
// Read from the terminal without echoing credentials or consuming MCP stdin.
|
|
3
|
+
export async function promptKey() {
|
|
4
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
5
|
+
throw Error('AISA_API_KEY is missing. Run setup in an interactive terminal to save a key, or set AISA_API_KEY before running setup.');
|
|
6
|
+
}
|
|
7
|
+
process.stderr.write('Enter AISA_API_KEY (saved locally in the client MCP configuration; input hidden): ');
|
|
8
|
+
const input = process.stdin;
|
|
9
|
+
const wasRaw = input.isRaw;
|
|
10
|
+
emitKeypressEvents(input);
|
|
11
|
+
input.setRawMode(true);
|
|
12
|
+
input.resume();
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let value = '';
|
|
15
|
+
const finish = (error) => {
|
|
16
|
+
input.removeListener('keypress', onKey);
|
|
17
|
+
input.removeListener('end', onEnd);
|
|
18
|
+
input.removeListener('error', onError);
|
|
19
|
+
input.setRawMode(wasRaw);
|
|
20
|
+
input.pause();
|
|
21
|
+
process.stderr.write('\n');
|
|
22
|
+
if (error)
|
|
23
|
+
reject(error);
|
|
24
|
+
else
|
|
25
|
+
resolve(value.trim());
|
|
26
|
+
};
|
|
27
|
+
const onEnd = () => finish(Error('Key input ended; setup cancelled.'));
|
|
28
|
+
const onError = () => finish(Error('Could not read key; setup cancelled.'));
|
|
29
|
+
const onKey = (text, key) => {
|
|
30
|
+
if (key.ctrl && (key.name === 'c' || key.name === 'd'))
|
|
31
|
+
return finish(Error('Setup cancelled.'));
|
|
32
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
33
|
+
if (!value.trim()) {
|
|
34
|
+
process.stderr.write('\nKey cannot be empty. Enter AISA_API_KEY: ');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
return finish();
|
|
38
|
+
}
|
|
39
|
+
if (key.name === 'backspace') {
|
|
40
|
+
value = Array.from(value).slice(0, -1).join('');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.ctrl && key.name === 'u') {
|
|
44
|
+
value = '';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!key.ctrl && !key.meta && text && !/[\x00-\x1f\x7f]/.test(text))
|
|
48
|
+
value += text;
|
|
49
|
+
};
|
|
50
|
+
input.on('keypress', onKey);
|
|
51
|
+
input.once('end', onEnd);
|
|
52
|
+
input.once('error', onError);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function resolveSetupKey(savedKey) {
|
|
56
|
+
if (savedKey?.trim())
|
|
57
|
+
return savedKey;
|
|
58
|
+
// Preserve the existing environment-based setup; only prompted keys are saved.
|
|
59
|
+
if (process.env.AISA_API_KEY?.trim())
|
|
60
|
+
return undefined;
|
|
61
|
+
return promptKey();
|
|
62
|
+
}
|
|
63
|
+
export function sharedSetupKeyResolver(resolve = resolveSetupKey) {
|
|
64
|
+
let requested;
|
|
65
|
+
return (savedKey) => {
|
|
66
|
+
// Keep each client's existing credentials. Share only the newly requested key.
|
|
67
|
+
if (savedKey?.trim())
|
|
68
|
+
return Promise.resolve(savedKey);
|
|
69
|
+
return requested ??= resolve();
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/install.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type Client = 'codex' | 'claude-code' | 'hermes';
|
|
2
|
+
export declare function detectClients(home?: string): Promise<Client[]>;
|
|
2
3
|
export declare const packageRoot: string;
|
|
3
4
|
export declare function launchEntry(client: Client, root?: string): {
|
|
4
5
|
env_vars?: string[] | undefined;
|
|
@@ -9,6 +10,7 @@ export declare function install(client: Client, options?: {
|
|
|
9
10
|
home?: string;
|
|
10
11
|
remove?: boolean;
|
|
11
12
|
skillsOnly?: boolean;
|
|
13
|
+
resolveKey?: (savedKey?: string) => Promise<string | undefined>;
|
|
12
14
|
}): Promise<{
|
|
13
15
|
client: Client;
|
|
14
16
|
skill: string;
|
package/dist/install.js
CHANGED
|
@@ -5,6 +5,27 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import TOML from '@iarna/toml';
|
|
7
7
|
import YAML from 'yaml';
|
|
8
|
+
export async function detectClients(home = homedir()) {
|
|
9
|
+
const markers = [
|
|
10
|
+
['codex', '.codex', 'directory'],
|
|
11
|
+
['claude-code', '.claude', 'directory'],
|
|
12
|
+
['claude-code', '.claude.json', 'file'],
|
|
13
|
+
['hermes', '.hermes', 'directory'],
|
|
14
|
+
];
|
|
15
|
+
const found = new Set();
|
|
16
|
+
for (const [client, relative, kind] of markers) {
|
|
17
|
+
try {
|
|
18
|
+
const stat = await fs.stat(path.resolve(home, relative));
|
|
19
|
+
if (kind === 'directory' ? stat.isDirectory() : stat.isFile())
|
|
20
|
+
found.add(client);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR')
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...found];
|
|
28
|
+
}
|
|
8
29
|
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
30
|
const skillName = 'aisa-web-market';
|
|
10
31
|
// npm/npx installations live in node_modules. Persist a version-pinned registry
|
|
@@ -12,7 +33,7 @@ const skillName = 'aisa-web-market';
|
|
|
12
33
|
// local Node entry so development changes remain immediately testable.
|
|
13
34
|
export function launchEntry(client, root = packageRoot) {
|
|
14
35
|
const command = root.split(path.sep).includes('node_modules')
|
|
15
|
-
? { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', '@hzlmy2002/web-market@0.1.
|
|
36
|
+
? { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', '@hzlmy2002/web-market@0.1.2', 'serve'] }
|
|
16
37
|
: { command: process.execPath, args: [path.join(root, 'dist/cli.js'), 'serve'] };
|
|
17
38
|
return { ...command, ...(client === 'codex' ? { env_vars: ['AISA_API_KEY'] } : {}) };
|
|
18
39
|
}
|
|
@@ -96,7 +117,7 @@ export async function install(client, options = {}) {
|
|
|
96
117
|
const root = skillRoot(client, home);
|
|
97
118
|
const entry = launchEntry(client);
|
|
98
119
|
const writes = new Map();
|
|
99
|
-
const next = { version: '0.1.
|
|
120
|
+
const next = { version: '0.1.2', files: {}, config: previous.config, configHash: previous.configHash };
|
|
100
121
|
const bundled = await skillFiles(path.join(packageRoot, 'skills', skillName));
|
|
101
122
|
for (const relative of new Set([...Object.keys(bundled), ...Object.keys(previous.files)])) {
|
|
102
123
|
if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..'))
|
|
@@ -118,7 +139,7 @@ export async function install(client, options = {}) {
|
|
|
118
139
|
next.files[relative] = hash(content);
|
|
119
140
|
}
|
|
120
141
|
}
|
|
121
|
-
if (!options.skillsOnly && (!options.remove || previous.config)) {
|
|
142
|
+
if (!options.skillsOnly && (!options.remove || previous.config || previous.configHash)) {
|
|
122
143
|
await noSymlinks(cfg.file);
|
|
123
144
|
const current = await read(cfg.file);
|
|
124
145
|
let config;
|
|
@@ -134,16 +155,25 @@ export async function install(client, options = {}) {
|
|
|
134
155
|
if (typeof config[cfg.key] !== 'object' || Array.isArray(config[cfg.key]))
|
|
135
156
|
throw Error('Invalid MCP configuration section.');
|
|
136
157
|
const existing = config[cfg.key][skillName];
|
|
158
|
+
const owned = previous.configHash ? existing !== undefined && hash(JSON.stringify(existing)) === previous.configHash : JSON.stringify(existing) === JSON.stringify(previous.config);
|
|
137
159
|
if (options.remove) {
|
|
138
|
-
if (existing &&
|
|
160
|
+
if (existing && !owned)
|
|
139
161
|
throw Error('MCP entry has been modified; preserving it.');
|
|
140
162
|
delete config[cfg.key][skillName];
|
|
141
163
|
}
|
|
142
164
|
else {
|
|
143
|
-
if (existing &&
|
|
165
|
+
if (existing && !owned && JSON.stringify(existing) !== JSON.stringify(entry))
|
|
144
166
|
throw Error('An unowned aisa-web-market MCP entry already exists; preserving it.');
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
const savedKey = typeof existing?.env?.AISA_API_KEY === 'string' ? existing.env.AISA_API_KEY : undefined;
|
|
168
|
+
const apiKey = options.resolveKey ? await options.resolveKey(savedKey) : savedKey;
|
|
169
|
+
const configured = { ...entry };
|
|
170
|
+
if (apiKey) {
|
|
171
|
+
configured.env = { AISA_API_KEY: apiKey };
|
|
172
|
+
delete configured.env_vars;
|
|
173
|
+
}
|
|
174
|
+
config[cfg.key][skillName] = configured;
|
|
175
|
+
delete next.config;
|
|
176
|
+
next.configHash = hash(JSON.stringify(configured));
|
|
147
177
|
}
|
|
148
178
|
writes.set(cfg.file, cfg.stringify(config));
|
|
149
179
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ApiClient, safeError } from './api.js';
|
|
|
3
3
|
import { inputs, output } from './schema.js';
|
|
4
4
|
import { execute, descriptions } from './tools.js';
|
|
5
5
|
export function createServer(api = new ApiClient()) {
|
|
6
|
-
const server = new McpServer({ name: 'aisa-web-market', version: '0.1.
|
|
6
|
+
const server = new McpServer({ name: 'aisa-web-market', version: '0.1.2' }, { instructions: 'Website competitive analysis using five AIsa tools. Use the installed aisa-web-market skill for scenario guidance. Compare identical scope and preserve missing-data evidence. API calls are billed. Credentials come from AISA_API_KEY; never request the key in conversation.' });
|
|
7
7
|
for (const name of Object.keys(inputs)) {
|
|
8
8
|
server.registerTool(name, { description: descriptions[name], inputSchema: inputs[name], outputSchema: output.shape, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true } }, async (args, extra) => {
|
|
9
9
|
try {
|