@lhi/n8m 1.0.2 → 1.0.3

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
@@ -27,10 +27,10 @@ No account. No server. Bring your own AI key and your n8n instance.
27
27
 
28
28
  ```bash
29
29
  # Option A: Run without installing (npx)
30
- npx n8m <command>
30
+ npx @lhi/n8m <command>
31
31
 
32
32
  # Option B: Install globally
33
- npm install -g n8m
33
+ npm install -g @lhi/n8m
34
34
  ```
35
35
 
36
36
  ## Setup
@@ -42,16 +42,16 @@ npm install -g n8m
42
42
 
43
43
  ```bash
44
44
  # OpenAI
45
- npx n8m config --ai-provider openai --ai-key sk-...
45
+ npx @lhi/n8m config --ai-provider openai --ai-key sk-...
46
46
 
47
47
  # Anthropic (Claude)
48
- npx n8m config --ai-provider anthropic --ai-key sk-ant-...
48
+ npx @lhi/n8m config --ai-provider anthropic --ai-key sk-ant-...
49
49
 
50
50
  # Google Gemini
51
- npx n8m config --ai-provider gemini --ai-key AIza...
51
+ npx @lhi/n8m config --ai-provider gemini --ai-key AIza...
52
52
 
53
53
  # Any OpenAI-compatible API (Ollama, Groq, Together, LM Studio, etc.)
54
- npx n8m config --ai-base-url http://localhost:11434/v1 --ai-key ollama --ai-model llama3
54
+ npx @lhi/n8m config --ai-base-url http://localhost:11434/v1 --ai-key ollama --ai-model llama3
55
55
  ```
56
56
 
57
57
  You can also use environment variables or a `.env` file — stored config takes
@@ -69,7 +69,7 @@ Default models per provider: `gpt-4o` · `claude-sonnet-4-6` · `gemini-2.5-flas
69
69
  ### 2. Configure your n8n instance
70
70
 
71
71
  ```bash
72
- npx n8m config --n8n-url https://your-n8n.example.com --n8n-key <your-n8n-api-key>
72
+ npx @lhi/n8m config --n8n-url https://your-n8n.example.com --n8n-key <your-n8n-api-key>
73
73
  ```
74
74
 
75
75
  Credentials are saved locally to `~/.n8m/config.json`. You can also use
@@ -408,7 +408,7 @@ Add it to your MCP client config (e.g. Claude Desktop's `claude_desktop_config.j
408
408
  "mcpServers": {
409
409
  "n8m": {
410
410
  "command": "npx",
411
- "args": ["n8m", "mcp"]
411
+ "args": ["@lhi/n8m", "mcp"]
412
412
  }
413
413
  }
414
414
  }
@@ -16,38 +16,32 @@ export const architectNode = async (state) => {
16
16
  collaborationLog: [`Architect: Modification plan — ${plan.description}`],
17
17
  };
18
18
  }
19
- try {
20
- const credentials = state.availableCredentials ?? [];
21
- const spec = await aiService.generateSpec(state.userGoal, credentials);
22
- // Check if the spec requires clarification
23
- const questions = spec.questions;
24
- const needsClarification = questions && questions.length > 0;
25
- // Multi-agent collaboration: generate an alternative strategy in parallel with the primary.
26
- // Both are handed off to separate Engineer agents that run concurrently.
27
- const alternativeSpec = await aiService.generateAlternativeSpec(state.userGoal, spec, credentials);
28
- const alternativeModel = aiService.getAlternativeModel();
29
- const strategies = [
30
- {
31
- ...spec,
32
- strategyName: "Primary Strategy",
33
- aiModel: aiService.getDefaultModel()
34
- },
35
- {
36
- ...alternativeSpec,
37
- strategyName: "Alternative Strategy",
38
- aiModel: alternativeModel
39
- },
40
- ];
41
- const logEntry = `Architect: Generated 2 strategies — "${strategies[0].suggestedName}" (primary) and "${strategies[1].suggestedName}" (alternative)`;
42
- return {
43
- spec,
44
- strategies,
45
- needsClarification,
46
- collaborationLog: [logEntry],
47
- };
48
- }
49
- catch (error) {
50
- console.error("Architect failed:", error);
51
- throw error;
52
- }
19
+ const credentials = state.availableCredentials ?? [];
20
+ const spec = await aiService.generateSpec(state.userGoal, credentials);
21
+ // Check if the spec requires clarification
22
+ const questions = spec.questions;
23
+ const needsClarification = questions && questions.length > 0;
24
+ // Multi-agent collaboration: generate an alternative strategy in parallel with the primary.
25
+ // Both are handed off to separate Engineer agents that run concurrently.
26
+ const alternativeSpec = await aiService.generateAlternativeSpec(state.userGoal, spec, credentials);
27
+ const alternativeModel = aiService.getAlternativeModel();
28
+ const strategies = [
29
+ {
30
+ ...spec,
31
+ strategyName: "Primary Strategy",
32
+ aiModel: aiService.getDefaultModel()
33
+ },
34
+ {
35
+ ...alternativeSpec,
36
+ strategyName: "Alternative Strategy",
37
+ aiModel: alternativeModel
38
+ },
39
+ ];
40
+ const logEntry = `Architect: Generated 2 strategies — "${strategies[0].suggestedName}" (primary) and "${strategies[1].suggestedName}" (alternative)`;
41
+ return {
42
+ spec,
43
+ strategies,
44
+ needsClarification,
45
+ collaborationLog: [logEntry],
46
+ };
53
47
  };
@@ -29,18 +29,41 @@ export default class Config extends Command {
29
29
  return;
30
30
  }
31
31
  // Update config
32
- if (flags['n8n-url'])
32
+ if (flags['n8n-url']) {
33
+ try {
34
+ const parsed = new URL(flags['n8n-url']);
35
+ if (!['http:', 'https:'].includes(parsed.protocol))
36
+ throw new Error('bad protocol');
37
+ }
38
+ catch {
39
+ this.error(`Invalid n8n URL: "${flags['n8n-url']}". Must be a valid http/https URL (e.g. https://your-n8n.example.com).`);
40
+ }
33
41
  config.n8nUrl = flags['n8n-url'];
42
+ }
34
43
  if (flags['n8n-key'])
35
44
  config.n8nKey = flags['n8n-key'];
36
45
  if (flags['ai-key'])
37
46
  config.aiKey = flags['ai-key'];
38
- if (flags['ai-provider'])
39
- config.aiProvider = flags['ai-provider'];
47
+ if (flags['ai-provider']) {
48
+ const KNOWN_PROVIDERS = ['openai', 'anthropic', 'gemini'];
49
+ if (!KNOWN_PROVIDERS.includes(flags['ai-provider'].toLowerCase())) {
50
+ this.error(`Unknown AI provider: "${flags['ai-provider']}". Must be one of: ${KNOWN_PROVIDERS.join(', ')}.`);
51
+ }
52
+ config.aiProvider = flags['ai-provider'].toLowerCase();
53
+ }
40
54
  if (flags['ai-model'])
41
55
  config.aiModel = flags['ai-model'];
42
- if (flags['ai-base-url'])
56
+ if (flags['ai-base-url']) {
57
+ try {
58
+ const parsed = new URL(flags['ai-base-url']);
59
+ if (!['http:', 'https:'].includes(parsed.protocol))
60
+ throw new Error('bad protocol');
61
+ }
62
+ catch {
63
+ this.error(`Invalid AI base URL: "${flags['ai-base-url']}". Must be a valid http/https URL (e.g. http://localhost:11434/v1).`);
64
+ }
43
65
  config.aiBaseUrl = flags['ai-base-url'];
66
+ }
44
67
  await ConfigManager.save(config);
45
68
  this.log(theme.done('Configuration updated successfully'));
46
69
  }
@@ -72,12 +72,14 @@ export default class Create extends Command {
72
72
  if (description && description.startsWith('```') && description.endsWith('```')) {
73
73
  description = description.slice(3, -3).trim();
74
74
  }
75
- if (!description) {
75
+ if (!description || !description.trim()) {
76
76
  this.error('Description is required.');
77
77
  }
78
+ description = description.trim();
78
79
  // 2. AGENTIC EXECUTION
79
80
  const threadId = randomUUID();
80
- this.log(theme.info(`\nInitializing Agentic Workflow for: "${description}" (Session: ${threadId})`));
81
+ this.log('\n' + theme.session(threadId));
82
+ this.log(theme.stageStart('Architect', 'Analyzing requirements...'));
81
83
  // Fetch available credentials for AI guidance (gracefully skipped if n8n not configured)
82
84
  let availableCredentials = [];
83
85
  {
@@ -100,18 +102,13 @@ export default class Create extends Command {
100
102
  const nodeName = Object.keys(event)[0];
101
103
  const stateUpdate = event[nodeName];
102
104
  if (nodeName === 'architect') {
103
- this.log(theme.agent(`🏗️ Architect: Blueprint designed.`));
104
- if (stateUpdate.strategies && stateUpdate.strategies.length > 0) {
105
- this.log(theme.header('\nPROPOSED STRATEGIES:'));
106
- stateUpdate.strategies.forEach((s, i) => {
107
- this.log(`${i === 0 ? theme.success(' [Primary]') : theme.info(' [Alternative]')} ${theme.value(s.suggestedName)}`);
108
- this.log(` Description: ${s.description}`);
109
- if (s.nodes && s.nodes.length > 0) {
110
- this.log(` Proposed Nodes: ${s.nodes.map((n) => n.type.split('.').pop()).join(', ')}`);
111
- }
112
- this.log('');
113
- });
114
- }
105
+ const count = stateUpdate.strategies?.length ?? 1;
106
+ this.log(theme.stagePass('Blueprint ready', `${count} approach${count !== 1 ? 'es' : ''}`));
107
+ const topNodes = stateUpdate.strategies?.[0]?.nodes
108
+ ?.map((n) => n.type?.split('.').pop())
109
+ .join(' ');
110
+ if (topNodes)
111
+ this.log(theme.treeItem(topNodes));
115
112
  }
116
113
  }
117
114
  // Handle interrupt/pause loop
@@ -122,12 +119,14 @@ export default class Create extends Command {
122
119
  const isRepair = (snapshot.values.validationErrors || []).length > 0;
123
120
  if (isRepair) {
124
121
  // Repair iteration — auto-continue without asking the user
122
+ this.log(theme.stageStart('Engineer', 'Applying fixes...'));
125
123
  const repairStream = await graph.stream(null, { configurable: { thread_id: threadId } });
126
124
  for await (const event of repairStream) {
127
125
  const n = Object.keys(event)[0];
128
126
  const u = event[n];
129
127
  if (n === 'engineer') {
130
- this.log(theme.agent(`⚙️ Engineer: Applying fixes...`));
128
+ const lineCount = u.workflowJson ? JSON.stringify(u.workflowJson, null, 2).split('\n').length : 0;
129
+ this.log(theme.stagePass('Fixes applied', `${lineCount} lines`));
131
130
  if (u.workflowJson)
132
131
  lastWorkflowJson = u.workflowJson;
133
132
  }
@@ -186,7 +185,7 @@ export default class Create extends Command {
186
185
  choices,
187
186
  }]);
188
187
  if (choice.type === 'exit') {
189
- this.log(theme.info(`\nSession saved. Resume later with: n8m resume ${threadId}`));
188
+ this.log(theme.muted(`\n Session saved. Resume later with: n8m resume ${threadId}`));
190
189
  return;
191
190
  }
192
191
  let chosenSpec = choice.strategy ?? spec;
@@ -235,12 +234,14 @@ export default class Create extends Command {
235
234
  this.log(theme.agent(`Building "${chosenSpec?.suggestedName}"...`));
236
235
  }
237
236
  await graph.updateState({ configurable: { thread_id: threadId } }, stateUpdate);
237
+ this.log(theme.stageStart('Engineer', 'Building workflow JSON...'));
238
238
  const buildStream = await graph.stream(null, { configurable: { thread_id: threadId } });
239
239
  for await (const event of buildStream) {
240
240
  const n = Object.keys(event)[0];
241
241
  const u = event[n];
242
242
  if (n === 'engineer') {
243
- this.log(theme.agent(`⚙️ Engineer: Building workflow...`));
243
+ const lineCount = u.workflowJson ? JSON.stringify(u.workflowJson, null, 2).split('\n').length : 0;
244
+ this.log(theme.stagePass('Workflow generated', `${lineCount} lines`));
244
245
  if (u.workflowJson)
245
246
  lastWorkflowJson = u.workflowJson;
246
247
  }
@@ -248,7 +249,7 @@ export default class Create extends Command {
248
249
  lastWorkflowJson = u.workflowJson;
249
250
  }
250
251
  else if (n === 'reviewer' && u.validationStatus === 'failed') {
251
- this.log(theme.warn(` Reviewer flagged issues — Engineer will revise...`));
252
+ this.log(theme.stageFail('Reviewer flagged issues — Engineer will revise'));
252
253
  }
253
254
  }
254
255
  }
@@ -261,25 +262,25 @@ export default class Create extends Command {
261
262
  default: true,
262
263
  }]);
263
264
  if (!proceed) {
264
- this.log(theme.info(`\nSession saved. Resume later with: n8m resume ${threadId}`));
265
+ this.log(theme.muted(`\n Session saved. Resume later with: n8m resume ${threadId}`));
265
266
  return;
266
267
  }
268
+ this.log(theme.stageStart('QA', 'Validating structure...'));
267
269
  const qaStream = await graph.stream(null, { configurable: { thread_id: threadId } });
268
270
  for await (const event of qaStream) {
269
271
  const n = Object.keys(event)[0];
270
272
  const u = event[n];
271
273
  if (n === 'qa') {
272
274
  if (u.validationStatus === 'passed') {
273
- this.log(theme.success(`🧪 QA: Validation Passed!`));
275
+ this.log(theme.stagePass('All checks passed'));
274
276
  if (u.workflowJson)
275
277
  lastWorkflowJson = u.workflowJson;
276
278
  }
277
279
  else {
278
- this.log(theme.fail(`🧪 QA: Validation Failed.`));
280
+ this.log(theme.stageFail('Validation failed'));
279
281
  if (u.validationErrors?.length) {
280
- u.validationErrors.forEach(e => this.log(theme.error(` - ${e}`)));
282
+ u.validationErrors.forEach(e => this.log(theme.treeItem(` ${e}`)));
281
283
  }
282
- this.log(theme.warn(` Looping back to Engineer for repairs...`));
283
284
  }
284
285
  }
285
286
  else if (n === 'supervisor' && u.workflowJson) {
@@ -297,7 +298,12 @@ export default class Create extends Command {
297
298
  }
298
299
  }
299
300
  catch (error) {
300
- this.error(`Agent ran into an unrecoverable error: ${error.message}`);
301
+ const msg = error.message ?? '';
302
+ const status = error.status;
303
+ if (status === 401 || /authentication_error|invalid.*api.?key|incorrect api key/i.test(msg)) {
304
+ this.error(`Authentication failed: invalid API key.\nRun: npx @lhi/n8m config --ai-key <your-key>`);
305
+ }
306
+ this.error(`Agent ran into an unrecoverable error: ${msg}`);
301
307
  }
302
308
  if (!lastWorkflowJson) {
303
309
  this.error('Agent finished but no workflow JSON was produced.');
@@ -318,13 +324,14 @@ export default class Create extends Command {
318
324
  const targetFile = path.join(targetDir, 'workflow.json');
319
325
  await fs.mkdir(targetDir, { recursive: true });
320
326
  await fs.writeFile(targetFile, JSON.stringify(workflow, null, 2));
321
- this.log(theme.success(`\nWorkflow organized at: ${targetDir}`));
322
327
  // Auto-Generate Documentation
323
- this.log(theme.agent("Generating initial documentation..."));
324
328
  const mermaid = docService.generateMermaid(workflow);
325
329
  const readmeContent = await docService.generateReadme(workflow);
326
330
  const fullDoc = `# ${projectTitle}\n\n## Visual Flow\n\n\`\`\`mermaid\n${mermaid}\`\`\`\n\n${readmeContent}`;
327
331
  await fs.writeFile(path.join(targetDir, 'README.md'), fullDoc);
332
+ this.log(theme.savedDir(path.relative(process.cwd(), targetDir)));
333
+ this.log(theme.treeItem('├── workflow.json'));
334
+ this.log(theme.treeItem('└── README.md'));
328
335
  savedResources.push({ path: targetFile, name: projectTitle, original: workflow });
329
336
  }
330
337
  // 4. DEPLOY PROMPT
@@ -150,9 +150,10 @@ export default class Modify extends Command {
150
150
  if (!instruction) {
151
151
  instruction = await promptMultiline('Describe the modifications you want to apply (use ``` for multiline): ');
152
152
  }
153
- if (!instruction) {
153
+ if (!instruction || !instruction.trim()) {
154
154
  this.error('Modification instructions are required.');
155
155
  }
156
+ instruction = instruction.trim();
156
157
  // 4. AGENTIC EXECUTION
157
158
  const threadId = randomUUID();
158
159
  this.log(theme.info(`\nInitializing Agentic Modification for: "${workflowName}"`));
@@ -304,7 +305,12 @@ export default class Modify extends Command {
304
305
  }
305
306
  }
306
307
  catch (error) {
307
- this.error(`Agent encountered an error: ${error.message}`);
308
+ const msg = error.message ?? '';
309
+ const status = error.status;
310
+ if (status === 401 || /authentication_error|invalid.*api.?key|incorrect api key/i.test(msg)) {
311
+ this.error(`Authentication failed: invalid API key.\nRun: npx @lhi/n8m config --ai-key <your-key>`);
312
+ }
313
+ this.error(`Agent encountered an error: ${msg}`);
308
314
  }
309
315
  // 5. POST-MODIFICATION ACTIONS
310
316
  const modifiedWorkflow = lastWorkflowJson.workflows ? lastWorkflowJson.workflows[0] : lastWorkflowJson;
@@ -128,7 +128,15 @@ export class AIService {
128
128
  });
129
129
  if (!response.ok) {
130
130
  const errorText = await response.text();
131
- throw new Error(`Anthropic API Error: ${response.status} - ${errorText}`);
131
+ let cleanMessage = `Anthropic API Error: ${response.status}`;
132
+ try {
133
+ const parsed = JSON.parse(errorText);
134
+ cleanMessage = parsed?.error?.message ?? cleanMessage;
135
+ }
136
+ catch { /* not JSON */ }
137
+ const err = new Error(cleanMessage);
138
+ err.status = response.status;
139
+ throw err;
132
140
  }
133
141
  const result = await response.json();
134
142
  return result.content?.[0]?.text || '';
@@ -9,6 +9,7 @@ export interface N8mConfig {
9
9
  export declare class ConfigManager {
10
10
  private static configDir;
11
11
  private static configFile;
12
+ private static loadFile;
12
13
  static load(): Promise<N8mConfig>;
13
14
  static save(config: Partial<N8mConfig>): Promise<void>;
14
15
  static clear(): Promise<void>;
@@ -8,7 +8,7 @@ dotenv.config({ quiet: true });
8
8
  export class ConfigManager {
9
9
  static configDir = path.join(os.homedir(), '.n8m');
10
10
  static configFile = path.join(os.homedir(), '.n8m', 'config.json');
11
- static async load() {
11
+ static async loadFile() {
12
12
  try {
13
13
  const data = await fs.readFile(this.configFile, 'utf-8');
14
14
  return JSON.parse(data);
@@ -17,9 +17,21 @@ export class ConfigManager {
17
17
  return {};
18
18
  }
19
19
  }
20
+ static async load() {
21
+ const fileConfig = await this.loadFile();
22
+ // Env vars (including .env in cwd) take priority over file config.
23
+ return {
24
+ n8nUrl: process.env.N8N_API_URL || fileConfig.n8nUrl,
25
+ n8nKey: process.env.N8N_API_KEY || fileConfig.n8nKey,
26
+ aiKey: process.env.AI_API_KEY || fileConfig.aiKey,
27
+ aiProvider: process.env.AI_PROVIDER || fileConfig.aiProvider,
28
+ aiModel: process.env.AI_MODEL || fileConfig.aiModel,
29
+ aiBaseUrl: process.env.AI_BASE_URL || fileConfig.aiBaseUrl,
30
+ };
31
+ }
20
32
  static async save(config) {
21
33
  await fs.mkdir(this.configDir, { recursive: true });
22
- const existing = await this.load();
34
+ const existing = await this.loadFile();
23
35
  const merged = { ...existing, ...config };
24
36
  await fs.writeFile(this.configFile, JSON.stringify(merged, null, 2));
25
37
  }
@@ -9,6 +9,12 @@ export declare const theme: {
9
9
  warn: (text: string) => string;
10
10
  fail: (text: string) => string;
11
11
  agent: (text: string) => string;
12
+ stageStart: (name: string, desc: string) => string;
13
+ stagePass: (text: string, sub?: string) => string;
14
+ stageFail: (text: string) => string;
15
+ savedDir: (dirPath: string) => string;
16
+ treeItem: (line: string) => string;
17
+ session: (id: string) => string;
12
18
  brand: () => string;
13
19
  tag: (text: string) => string;
14
20
  primary: import("chalk").ChalkInstance;
@@ -64,6 +64,19 @@ export const theme = {
64
64
  fail: (text) => c.error('✘ ') + c.foreground(text),
65
65
  // AI/Agentic
66
66
  agent: (text) => c.ai('✧ ') + c.ai.italic(text),
67
+ // Site-terminal stage style (matches n8m.run terminal demo)
68
+ stageStart: (name, desc) => {
69
+ const padded = name.padEnd(12);
70
+ return chalk.hex('#fb923c').bold(` ${padded}`) + chalk.hex('#64748b')(desc);
71
+ },
72
+ stagePass: (text, sub) => {
73
+ const line = chalk.hex('#4ade80')(' ✓') + ' ' + chalk.hex('#f1f5f9')(text);
74
+ return sub ? line + chalk.hex('#64748b')(` · ${sub}`) : line;
75
+ },
76
+ stageFail: (text) => chalk.hex('#f87171')(' ✗') + ' ' + chalk.hex('#f1f5f9')(text),
77
+ savedDir: (dirPath) => chalk.hex('#4ade80').bold('\n Saved') + ' ' + chalk.hex('#94a3b8')(dirPath),
78
+ treeItem: (line) => chalk.hex('#94a3b8')(` ${line}`),
79
+ session: (id) => chalk.hex('#3d5070')(` Session: ${id}`),
67
80
  // Brand/Banner
68
81
  brand: () => {
69
82
  const bannerPath = join(rootPath, 'banner.txt');
package/docs/index.html CHANGED
@@ -837,19 +837,87 @@
837
837
  /* ============================================================
838
838
  RESPONSIVE
839
839
  ============================================================ */
840
+
841
+ /* ---- tap targets ---- */
842
+ a, button { -webkit-tap-highlight-color: transparent; }
843
+ .btn, .install-box, .cta-install, .badge, .nav-links a { touch-action: manipulation; }
844
+
845
+ /* ---- tablet (≤960px) ---- */
840
846
  @media (max-width: 960px) {
841
847
  .hero-wrap { flex-direction: column; padding-top: 5.5rem; }
842
848
  .hero-r { max-width: 100%; }
849
+ .hero-sub { max-width: 100%; }
843
850
  .pipeline { flex-direction: column; }
844
851
  .parr { transform: rotate(90deg); padding: .4rem 0; justify-content: center; }
845
852
  .mcp-split, .rm-grid, .sponsor-split { grid-template-columns: 1fr; gap: 2rem; }
846
853
  }
847
854
 
855
+ /* ---- mobile (≤640px) ---- */
848
856
  @media (max-width: 640px) {
849
857
  h1 { font-size: 2.2rem; }
858
+ h2 { font-size: 1.65rem; }
850
859
  .cmds-grid, .feats-grid { grid-template-columns: 1fr; }
851
- .nav-links a:not(.nav-cta) { display: none; }
860
+ /* keep GitHub + npm links visible; hide interior nav links */
861
+ .nav-links a:not(.nav-cta):not([href*="github"]):not([href*="npmjs"]) { display: none; }
852
862
  section { padding: 3.5rem 1.25rem; }
863
+
864
+ /* install command — scroll rather than overflow */
865
+ .install-box {
866
+ display: flex;
867
+ width: 100%;
868
+ overflow-x: auto;
869
+ white-space: nowrap;
870
+ }
871
+
872
+ /* code blocks that can overflow */
873
+ .config-box, .json-box, .cmd-ex { overflow-x: auto; }
874
+
875
+ /* CTA install box */
876
+ .cta-install {
877
+ width: 100%;
878
+ max-width: 100%;
879
+ overflow-x: auto;
880
+ font-size: .85rem;
881
+ justify-content: flex-start;
882
+ }
883
+
884
+ /* hero buttons — tighter gap, slightly smaller */
885
+ .hero-btns { gap: .5rem; }
886
+ .btn { padding: .55rem 1rem; font-size: .82rem; }
887
+
888
+ /* terminal — smaller font saves vertical space */
889
+ .term-body { font-size: .7rem; padding: .9rem 1rem 1rem; min-height: 240px; }
890
+
891
+ /* footer — stacked */
892
+ footer { flex-direction: column; align-items: flex-start; gap: .65rem; }
893
+ .footer-links { flex-wrap: wrap; gap: .65rem; }
894
+ }
895
+
896
+ /* ---- small phone (≤430px) ---- */
897
+ @media (max-width: 430px) {
898
+ h1 { font-size: 1.85rem; }
899
+ h2 { font-size: 1.4rem; }
900
+ .hero-wrap { padding-top: 4.75rem; padding-left: 1rem; padding-right: 1rem; }
901
+ section { padding: 3rem 1rem; }
902
+ nav { padding: .55rem 1rem; }
903
+
904
+ /* badge — tighter on very narrow screens */
905
+ .badge { font-size: .65rem; padding: .25rem .65rem; }
906
+
907
+ /* hero buttons stack full-width */
908
+ .hero-btns { flex-direction: column; }
909
+ .hero-btns .btn { width: 100%; justify-content: center; }
910
+
911
+ /* provider pills — slightly smaller */
912
+ .prov-pill { font-size: .75rem; }
913
+
914
+ /* CTA section buttons stack */
915
+ .cta-btns { flex-direction: column; align-items: stretch; }
916
+ .cta-btns .btn { width: 100%; justify-content: center; }
917
+
918
+ /* sponsor section buttons */
919
+ #sponsors .btn { width: 100%; justify-content: center; }
920
+ #sponsors div[style*="display:flex"] { flex-direction: column; }
853
921
  }
854
922
 
855
923
  /* ============================================================
@@ -920,10 +988,10 @@
920
988
  No account. No server. Bring your own AI key.
921
989
  </p>
922
990
 
923
- <div class="install-box" onclick="copy('npx n8m create &quot;your workflow&quot;','copyBtn')">
991
+ <div class="install-box" onclick="copy('npx @lhi/n8m create &quot;your workflow&quot;','copyBtn')">
924
992
  <span class="ip">$</span>
925
993
  <span class="ic">npx</span>
926
- <span style="color:var(--text-3);">&thinsp;n8m</span>
994
+ <span style="color:var(--text-3);">&thinsp;@lhi/n8m</span>
927
995
  <span class="ec" style="color:var(--brand-hi);">&thinsp;create</span>
928
996
  <span class="is">&thinsp;"your workflow"</span>
929
997
  <button class="copy-btn" id="copyBtn" aria-label="Copy install command">copy</button>
@@ -1417,10 +1485,10 @@
1417
1485
  <h2>Ready to stop clicking?</h2>
1418
1486
  <p class="sec-sub">No account. No server. No lock-in. Just your n8n instance and an AI key.</p>
1419
1487
 
1420
- <div class="cta-install" onclick="copy('npx n8m create &quot;describe your workflow&quot;','copyBtnCta')">
1488
+ <div class="cta-install" onclick="copy('npx @lhi/n8m create &quot;describe your workflow&quot;','copyBtnCta')">
1421
1489
  <span style="color:var(--text-5);">$</span>
1422
1490
  <span class="ic">npx</span>
1423
- <span style="color:var(--text-3);">&thinsp;n8m</span>
1491
+ <span style="color:var(--text-3);">&thinsp;@lhi/n8m</span>
1424
1492
  <span class="ec" style="color:var(--brand-hi);">&thinsp;create</span>
1425
1493
  <span class="is">&thinsp;"describe your workflow"</span>
1426
1494
  <button class="copy-btn" id="copyBtnCta" aria-label="Copy command">copy</button>
@@ -215,7 +215,7 @@
215
215
  n8n compatible
216
216
  </div>
217
217
  <div class="tag" style="color:#4ade80;border-color:rgba(34,197,94,.25);">
218
- npx n8m create "..."
218
+ npx @lhi/n8m create "..."
219
219
  </div>
220
220
  </div>
221
221
 
@@ -585,5 +585,5 @@
585
585
  ]
586
586
  }
587
587
  },
588
- "version": "1.0.2"
588
+ "version": "1.0.3"
589
589
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/n8m",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Agentic n8n CLI wrapper - A Skill Bridge for n8n workflow automation",
5
5
  "author": "Lem Canady",
6
6
  "keywords": [
@@ -129,6 +129,7 @@
129
129
  "start": "npm run build && ./bin/run.js",
130
130
  "dev": "tsc -b -w",
131
131
  "generate-patterns": "tsx scripts/generate-patterns.ts",
132
- "version": "oclif readme && git add README.md"
132
+ "version": "oclif readme && git add README.md",
133
+ "test:security": "mocha '__tests__/security/**/*.spec.js'"
133
134
  }
134
135
  }