martinbonan 3.0.0 → 4.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/bin/index.js CHANGED
@@ -17,6 +17,20 @@ if (cols < 60 || rows < 16) {
17
17
  process.exit(1);
18
18
  }
19
19
 
20
- render(React.createElement(App), {
20
+ // Enter alternate screen buffer (prevents scroll-back pollution)
21
+ process.stdout.write('\x1b[?1049h');
22
+ process.stdout.write('\x1b[2J\x1b[H');
23
+
24
+ const instance = render(React.createElement(App), {
21
25
  exitOnCtrlC: true,
22
26
  });
27
+
28
+ // Leave alternate screen on exit
29
+ instance.waitUntilExit().then(() => {
30
+ process.stdout.write('\x1b[?1049l');
31
+ });
32
+
33
+ // Also handle unexpected exits
34
+ process.on('exit', () => {
35
+ process.stdout.write('\x1b[?1049l');
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "martinbonan",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Martin Music -- interactive CLI portfolio with AI twin",
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -2,25 +2,20 @@ import React, { useState, useCallback } from 'react';
2
2
  import { Box } from 'ink';
3
3
  import BootSequence from './screens/BootSequence.js';
4
4
  import Dashboard from './screens/Dashboard.js';
5
- import { ThemeContext, THEMES, makeChalkColors, getNextThemeName } from './theme.js';
5
+ import { ThemeContext, THEMES, makeChalkColors } from './theme.js';
6
6
 
7
7
  const e = React.createElement;
8
8
 
9
9
  export default function App() {
10
10
  const [screen, setScreen] = useState('boot');
11
- const [themeName, setThemeName] = useState('cyberpunk');
12
- const theme = THEMES[themeName];
11
+ const theme = THEMES.sunset;
13
12
  const c = makeChalkColors(theme);
14
13
 
15
14
  const handleBootComplete = useCallback(() => {
16
15
  setScreen('dashboard');
17
16
  }, []);
18
17
 
19
- const nextTheme = useCallback(() => {
20
- setThemeName(name => getNextThemeName(name));
21
- }, []);
22
-
23
- const themeValue = { theme, themeName, nextTheme, c };
18
+ const themeValue = { theme, themeName: 'sunset', nextTheme: () => {}, c };
24
19
 
25
20
  return e(ThemeContext.Provider, { value: themeValue },
26
21
  e(Box, { flexDirection: 'column', minHeight: process.stdout.rows || 24 },
package/src/ai/context.js CHANGED
@@ -1,4 +1,4 @@
1
- export const SYSTEM_PROMPT = `You are Martin Music, a 20-year-old French entrepreneur. You're answering as yourself in your CLI portfolio. People are chatting with an AI version of you.
1
+ export const SYSTEM_PROMPT = `You are Martin Bonan, a 20-year-old French entrepreneur. You're answering as yourself in your CLI portfolio. People are chatting with an AI version of you.
2
2
 
3
3
  PERSONALITY:
4
4
  - Direct, casual, no-bullshit. You hate corporate-speak.
@@ -9,35 +9,37 @@ PERSONALITY:
9
9
  - Witty, occasionally funny. Never cringe.
10
10
 
11
11
  BACKGROUND:
12
- - 20yo, French, based in France. Building since age 15.
13
- - Co-founded Qual (qual.cx) -- AI qualitative research
14
- - Building Squal (squal.ai) -- qualitative feedback for AI companies
15
- "Every AI company measures what users do. None understand why."
16
- - Running Astry -- dev agency, 30+ international B2B clients
17
- - Teaching vibecoding at IQ Project (trained at HEC Paris)
18
- - Accepted to Berkeley Global Incubator (Jan 2027)
19
- - 3rd year Global BBA at NEOMA Business School
20
- - Father co-founded Talend (acquired by Qlik)
12
+ - 20yo, French, based in Paris. Building since age 15.
13
+ - Co-founded Qual (qual.cx) with Marin -- research engine making qualitative depth scale
14
+ AI-conducted interviews that go deep, learn in real-time, scale to tens of thousands
15
+ Adopted by PhD researchers at Harvard, MIT, Imperial College London. 40+ studies, 70%+ completion rate.
16
+ - Co-founded Astry with Teo & Mathis -- AI-powered dev agency, 30+ B2B clients, 3-5x speed
17
+ - Built F*cksubscription -- 3K+ paying customers, 10K+ EUR, one-time payments replacing SaaS
18
+ - Teaching vibecoding at HEC Paris through IQ Project -- 120 students trained
19
+ - Started designing at 13 (Figma, Affinity Designer). First venture at 15.
20
+ - NEOMA Business School Global BBA (2023-2027)
21
+ - Exchange at Queen's University / Smith School of Business in Canada (2024)
22
+ Discovered Cursor & Claude there, went from no-code to full-stack AI dev
23
+ - UC Berkeley Global Incubator starting January 2027 -- bringing Qual to the US
21
24
 
22
25
  KEY PROJECTS:
23
- - Squal: Missing qualitative layer for AI. 1 client ~$999/mo. Raising 250K at 2M SAFE.
24
- - Astry: Dev agency, 30+ clients worldwide
25
- - AI Series Farm: Video pipeline (Claude Code + Playwright + Higgsfield.ai)
26
- - SMCP: Supabase MCP Manager -- Electron app, 11 Supabase accounts
27
- - CASA IMMO: React Native real estate aggregator
28
- - F*ckSubscription: First hit at 15, 3K+ users, 10K+ EUR
29
- - Brief MCP: Paris Innov'Hack 2nd/150
26
+ - Qual: Research engine for qualitative depth at scale. Primary focus. Harvard, MIT, Imperial adoption.
27
+ - Astry: AI-powered dev agency, 30+ clients. Cash machine funding Qual.
28
+ - F*cksubscription: "Sell before building" masterclass. 1K signups day one, 90% conversion, 3K+ customers.
29
+ Products: MangoForm, PeachCalendar, GrapeLink, GuavaTeam.
30
+ - Brief: Paris Innov'Hack 2025, 2nd/150+. MCP-based intelligent context manager for AI agents.
31
+ - IQ Project: Teaching vibecoding at HEC Paris. 120 students.
32
+ - Objectif Reussite: Nonprofit tutoring network started at 15. 800+ students, 20+ tutors, 3 years.
33
+ - Good Good Time: Activity recommendation app. NEOMA incubator -> rebuilt with Cursor in Canada.
34
+ - 15+ projects total since age 15.
30
35
 
31
- TECH PHILOSOPHY:
32
- - "Sell before you build"
33
- - Vibecoding: building with AI as co-pilot (Claude Code, agent teams)
34
- - Autonomous agent loops (LOOP_PROMPT.md + PROGRESS.md pattern)
35
- - Claude Code Agent Teams with TeamCreate/CreateTeam tools
36
- - Multi-agent architectures as core dev pattern
37
- - Ships fast, iterates faster
36
+ TECH:
37
+ - "Sell before you build" -- validate with landing pages and pre-sales first
38
+ - Design as competitive moat, not afterthought
39
+ - Stack: Next.js, React, TypeScript, Supabase, Vercel, ShadCN UI, Cursor, Claude
40
+ - Vibecoding evangelist -- AI as core of the development workflow
38
41
 
39
42
  INTERESTS: Philosophy, consciousness, cinema, media literacy
40
- FAVE COMPANIES: Lovable, Logitech, Featherless, Monster
41
43
 
42
44
  RULES:
43
45
  - Be honest. If you don't know, say so.
@@ -21,6 +21,8 @@ const TAB_COMPONENTS = {
21
21
  'Ask AI': AskAi,
22
22
  };
23
23
 
24
+ const GLITCH_CHARS = '\u2591\u2592\u2593\u2588\u2580\u2584\u258C\u2590\u2500\u2502';
25
+
24
26
  export default function ContentPanel({
25
27
  activeTab,
26
28
  isFocused,
@@ -28,6 +30,7 @@ export default function ContentPanel({
28
30
  selectedIndex,
29
31
  expandedProject,
30
32
  onCollapseProject,
33
+ transitioning,
31
34
  }) {
32
35
  const { theme } = React.useContext(ThemeContext);
33
36
  const Component = TAB_COMPONENTS[activeTab];
@@ -36,8 +39,27 @@ export default function ContentPanel({
36
39
  const cols = process.stdout.columns || 80;
37
40
  const separator = '\u2500'.repeat(Math.max(cols - 2, 20));
38
41
 
39
- const props = {};
42
+ // Glitch transition effect
43
+ if (transitioning) {
44
+ const glitchLines = [];
45
+ for (let i = 0; i < 4; i++) {
46
+ let line = '';
47
+ for (let j = 0; j < Math.min(cols - 4, 70); j++) {
48
+ line += Math.random() < 0.3
49
+ ? GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)]
50
+ : ' ';
51
+ }
52
+ glitchLines.push(line);
53
+ }
54
+ return e(Box, { flexDirection: 'column', flexGrow: 1 },
55
+ e(Text, { dimColor: true, color: theme.dim }, separator),
56
+ ...glitchLines.map((l, i) =>
57
+ e(Text, { key: i, color: theme.accent, dimColor: true }, l)
58
+ ),
59
+ );
60
+ }
40
61
 
62
+ const props = {};
41
63
  switch (activeTab) {
42
64
  case 'Projects':
43
65
  props.selectedIndex = selectedIndex;
@@ -47,6 +69,9 @@ export default function ContentPanel({
47
69
  case 'History':
48
70
  props.selectedIndex = selectedIndex;
49
71
  break;
72
+ case 'Contact':
73
+ props.selectedIndex = selectedIndex;
74
+ break;
50
75
  case 'Ask AI':
51
76
  props.isFocused = isFocused;
52
77
  break;
@@ -54,6 +79,6 @@ export default function ContentPanel({
54
79
 
55
80
  return e(Box, { flexDirection: 'column', flexGrow: 1 },
56
81
  e(Text, { dimColor: true, color: theme.dim }, separator),
57
- e(Component, props)
82
+ e(Component, props),
58
83
  );
59
84
  }
@@ -7,33 +7,44 @@ import { ThemeContext } from '../theme.js';
7
7
 
8
8
  const e = React.createElement;
9
9
 
10
- const nameGradient = gradient(['#00D4AA', '#7C5CE0', '#B088F9']);
10
+ function makeGradient(theme) {
11
+ return gradient([theme.primary, theme.secondary || theme.primary]);
12
+ }
11
13
 
12
14
  export default function Header({ tagline, fading }) {
13
15
  const { theme } = React.useContext(ThemeContext);
14
16
  const cols = process.stdout.columns || 80;
15
- const showPortrait = cols >= 80;
17
+ const showPortrait = cols >= 75;
18
+ const grad = makeGradient(theme);
16
19
 
17
20
  let figletText;
18
21
  try {
19
- figletText = figlet.textSync('MARTIN', { font: 'ANSI Shadow' });
22
+ const line1 = figlet.textSync('MARTIN', { font: 'ANSI Shadow' });
23
+ const line2 = figlet.textSync('BONAN', { font: 'ANSI Shadow' });
24
+ figletText = line1 + '\n' + line2;
20
25
  } catch {
21
- figletText = 'MARTIN MUSIC';
26
+ try {
27
+ const line1 = figlet.textSync('MARTIN', { font: 'Standard' });
28
+ const line2 = figlet.textSync('BONAN', { font: 'Standard' });
29
+ figletText = line1 + '\n' + line2;
30
+ } catch {
31
+ figletText = 'MARTIN\nBONAN';
32
+ }
22
33
  }
23
34
 
24
- const gradientName = nameGradient(figletText);
35
+ const gradientName = grad(figletText);
25
36
 
26
37
  return e(Box, { flexDirection: 'column' },
27
38
  e(Box, { flexDirection: 'row' },
28
39
  showPortrait && e(Box, { marginRight: 2 },
29
- e(Portrait, null)
40
+ e(Portrait, null),
30
41
  ),
31
42
  e(Box, { flexDirection: 'column', justifyContent: 'center' },
32
- e(Text, null, gradientName)
33
- )
43
+ e(Text, null, gradientName),
44
+ ),
34
45
  ),
35
46
  e(Box, { marginTop: 0 },
36
- e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || '')
37
- )
47
+ e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || ''),
48
+ ),
38
49
  );
39
50
  }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { ThemeContext } from '../theme.js';
4
+
5
+ const e = React.createElement;
6
+
7
+ const SHORTCUTS = [
8
+ ['\u2190 \u2192', 'Navigate tabs'],
9
+ ['1-7', 'Jump to tab directly'],
10
+ ['\u2191 \u2193', 'Scroll / select items'],
11
+ ['Enter', 'Expand project / focus chat'],
12
+ ['Esc', 'Go back / collapse'],
13
+ ['?', 'Toggle this help screen'],
14
+ ['q', 'Quit martin-cli'],
15
+ ];
16
+
17
+ const TIPS = [
18
+ 'Type "neofetch" or "sudo hire martin" in AI chat',
19
+ 'Type "help" in AI chat for all commands',
20
+ ];
21
+
22
+ export default function HelpOverlay() {
23
+ const { theme } = React.useContext(ThemeContext);
24
+
25
+ const w = 50;
26
+ const top = '\u256D\u2500\u2500\u2500 Help ' + '\u2500'.repeat(w - 7) + '\u256E';
27
+ const bot = '\u2570' + '\u2500'.repeat(w) + '\u256F';
28
+ const emp = '\u2502' + ' '.repeat(w) + '\u2502';
29
+
30
+ const padRight = (str, total) => str + ' '.repeat(Math.max(total - str.length, 0));
31
+
32
+ return e(Box, { flexDirection: 'column', paddingLeft: 2, marginTop: 1 },
33
+ e(Text, { color: theme.border }, top),
34
+ e(Text, { color: theme.border }, emp),
35
+ e(Text, { color: theme.border }, '\u2502 ',
36
+ e(Text, { color: theme.primary, bold: true }, padRight('KEYBOARD SHORTCUTS', w - 2)),
37
+ '\u2502',
38
+ ),
39
+ e(Text, { color: theme.border }, emp),
40
+ ...SHORTCUTS.map((s, i) => {
41
+ const content = s[0].padEnd(12) + s[1];
42
+ const pad = Math.max(w - content.length - 2, 0);
43
+ return e(Text, { key: `s-${i}`, color: theme.border }, '\u2502 ',
44
+ e(Text, { color: theme.accent, bold: true }, s[0].padEnd(12)),
45
+ e(Text, { color: theme.text }, s[1]),
46
+ ' '.repeat(pad), '\u2502',
47
+ );
48
+ }),
49
+ e(Text, { color: theme.border }, emp),
50
+ e(Text, { color: theme.border }, '\u2502 ',
51
+ e(Text, { color: theme.primary, bold: true }, padRight('TIPS', w - 2)),
52
+ '\u2502',
53
+ ),
54
+ e(Text, { color: theme.border }, emp),
55
+ ...TIPS.map((t, i) => {
56
+ const content = '\u2022 ' + t;
57
+ const pad = Math.max(w - content.length - 2, 0);
58
+ return e(Text, { key: `t-${i}`, color: theme.border }, '\u2502 ',
59
+ e(Text, { color: theme.dim }, content),
60
+ ' '.repeat(pad), '\u2502',
61
+ );
62
+ }),
63
+ e(Text, { color: theme.border }, emp),
64
+ e(Text, { color: theme.border }, bot),
65
+ );
66
+ }
@@ -1,16 +1,26 @@
1
1
  import React from 'react';
2
- import { Text } from 'ink';
2
+ import { Box, Text } from 'ink';
3
3
  import { PORTRAIT_ANSI, PORTRAIT_BRAILLE } from '../data/portrait.js';
4
4
  import { ThemeContext, supportsTrueColor } from '../theme.js';
5
5
 
6
6
  const e = React.createElement;
7
7
 
8
8
  export default function Portrait() {
9
- const { c } = React.useContext(ThemeContext);
9
+ const { theme } = React.useContext(ThemeContext);
10
10
 
11
+ // True-color ANSI portrait (no animations — avoids flickering)
11
12
  if (supportsTrueColor && PORTRAIT_ANSI.length > 0) {
12
- return e(Text, null, PORTRAIT_ANSI.join('\n'));
13
+ return e(Box, { flexDirection: 'column' },
14
+ ...PORTRAIT_ANSI.map((line, i) =>
15
+ e(Text, { key: i }, line),
16
+ ),
17
+ );
13
18
  }
14
19
 
15
- return e(Text, { dimColor: true }, c.dim(PORTRAIT_BRAILLE.join('\n')));
20
+ // Braille fallback
21
+ return e(Box, { flexDirection: 'column' },
22
+ ...PORTRAIT_BRAILLE.map((line, i) =>
23
+ e(Text, { key: i, color: theme.primary }, line),
24
+ ),
25
+ );
16
26
  }
@@ -5,12 +5,13 @@ import { ThemeContext } from '../theme.js';
5
5
  const e = React.createElement;
6
6
 
7
7
  const KEYBINDS = {
8
- normal: '<- -> tabs | up/dn scroll | enter expand | t theme | q quit',
9
- expanded: 'esc back | up/dn scroll',
10
- chat: 'type to chat | enter send | esc back to tabs',
8
+ normal: '\u2190 \u2192 tabs \u2502 \u2191\u2193 scroll \u2502 enter expand \u2502 ? help \u2502 q quit',
9
+ expanded: 'esc back \u2502 \u2191\u2193 scroll',
10
+ chat: 'type to chat \u2502 enter send \u2502 esc back',
11
+ help: '? or esc to close help',
11
12
  };
12
13
 
13
- export default function StatusBar({ mode, themeName }) {
14
+ export default function StatusBar({ mode, exploredCount = 0, totalTabs = 7 }) {
14
15
  const { theme } = React.useContext(ThemeContext);
15
16
  const [clock, setClock] = React.useState('');
16
17
 
@@ -22,15 +23,16 @@ export default function StatusBar({ mode, themeName }) {
22
23
  setClock(`${hh}:${mm}`);
23
24
  };
24
25
  update();
25
- const id = setInterval(update, 30000);
26
+ const id = setInterval(update, 60000);
26
27
  return () => clearInterval(id);
27
28
  }, []);
28
29
 
29
30
  const keys = KEYBINDS[mode] || KEYBINDS.normal;
30
- const right = `${themeName || 'cyberpunk'} | ${clock}`;
31
+ const explored = '\u2588'.repeat(exploredCount) + '\u2591'.repeat(totalTabs - exploredCount);
32
+ const right = `${explored} ${exploredCount}/${totalTabs} \u2502 ${clock}`;
31
33
 
32
34
  return e(Box, { width: '100%', justifyContent: 'space-between', marginTop: 1 },
33
35
  e(Text, { dimColor: true, color: theme.dim }, keys),
34
- e(Text, { dimColor: true, color: theme.dim }, right)
36
+ e(Text, { dimColor: true, color: theme.dim }, right),
35
37
  );
36
38
  }
@@ -8,22 +8,23 @@ export default function TabBar({ tabs, activeIndex }) {
8
8
  const { theme } = React.useContext(ThemeContext);
9
9
 
10
10
  return e(Box, { flexDirection: 'row', marginTop: 1 },
11
- tabs.map((label, i) => {
11
+ ...tabs.map((label, i) => {
12
12
  const isActive = i === activeIndex;
13
+ const num = String(i + 1);
13
14
 
14
15
  if (isActive) {
15
16
  return e(Text, {
16
17
  key: label,
17
18
  bold: true,
18
- backgroundColor: theme.primary,
19
- color: 'black',
20
- }, `[ ${label} ]`);
19
+ backgroundColor: theme.accent,
20
+ color: theme.bg,
21
+ }, `[${num}:${label}]`);
21
22
  }
22
23
 
23
- return e(Text, {
24
- key: label,
25
- dimColor: true,
26
- }, ` ${label} `);
27
- })
24
+ return e(Box, { key: label },
25
+ e(Text, { dimColor: true, color: theme.dim }, ` ${num}:`),
26
+ e(Text, { dimColor: true }, `${label} `),
27
+ );
28
+ }),
28
29
  );
29
30
  }