@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 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. Set AISA_API_KEY in the environment of the process launching your MCP client, then run one of:
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.0 setup --client codex
11
- npx -y @hzlmy2002/web-market@0.1.0 setup --client claude-code
12
- npx -y @hzlmy2002/web-market@0.1.0 setup --client hermes
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.0 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. Credentials must still reach the MCP process; the installer never stores your key. Use `uninstall --client ...` to remove an unchanged managed installation.
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
- Set `AISA_API_KEY` in the environment of the process launching your MCP client. It is an AIsa API key; never put it in a Skill or commit it. Desktop clients started outside your shell may need the variable configured explicitly in their MCP settings. The installer does not read or persist the key.
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\nSet AISA_API_KEY in the server environment. setup never stores the token.');
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.0' }));
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
- throw Error('--client is required.');
22
- console.log(JSON.stringify(await install(values.client, { home: values.home, remove: command === 'uninstall' })));
23
- console.log('Client configuration preserves existing values; serialization may reformat comments. Ensure AISA_API_KEY reaches the client process. Restart or refresh the client to load skills.');
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.0', 'serve'] }
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.0', files: {}, config: previous.config };
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 && JSON.stringify(existing) !== JSON.stringify(previous.config))
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 && JSON.stringify(existing) !== JSON.stringify(previous.config) && JSON.stringify(existing) !== JSON.stringify(entry))
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
- config[cfg.key][skillName] = entry;
146
- next.config = entry;
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.0' }, { 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.' });
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzlmy2002/web-market",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Focused website competitive analysis: five MCP tools and a portable skill",
5
5
  "type": "module",
6
6
  "bin": {