upfynai-code 0.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.
Files changed (65) hide show
  1. package/LICENSE +22 -0
  2. package/bin/cli.js +86 -0
  3. package/dist/assets/CanvasPanel-B48gAKVY.js +538 -0
  4. package/dist/assets/CanvasPanel-B48gAKVY.js.map +1 -0
  5. package/dist/assets/CanvasPanel-BsOG3EVs.css +1 -0
  6. package/dist/assets/index-CEhTwG68.css +1 -0
  7. package/dist/assets/index-GqAGWpJI.js +70 -0
  8. package/dist/assets/index-GqAGWpJI.js.map +1 -0
  9. package/dist/index.html +18 -0
  10. package/index.html +17 -0
  11. package/package.json +67 -0
  12. package/src/App.tsx +226 -0
  13. package/src/components/canvas/CanvasPanel.tsx +62 -0
  14. package/src/components/canvas/layout/graph-builder.ts +136 -0
  15. package/src/components/canvas/shapes/CompactionNodeShape.tsx +76 -0
  16. package/src/components/canvas/shapes/SessionNodeShape.tsx +93 -0
  17. package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +125 -0
  18. package/src/components/canvas/shapes/TextResponseNodeShape.tsx +86 -0
  19. package/src/components/canvas/shapes/ToolCallNodeShape.tsx +107 -0
  20. package/src/components/canvas/shapes/ToolResultNodeShape.tsx +87 -0
  21. package/src/components/canvas/shapes/shared-styles.ts +35 -0
  22. package/src/components/chat/ChatPanel.tsx +96 -0
  23. package/src/components/chat/InputBar.tsx +81 -0
  24. package/src/components/chat/MessageList.tsx +130 -0
  25. package/src/components/chat/PermissionDialog.tsx +70 -0
  26. package/src/components/layout/FolderSelector.tsx +152 -0
  27. package/src/components/layout/ModelSelector.tsx +65 -0
  28. package/src/components/layout/SessionManager.tsx +115 -0
  29. package/src/components/statusline/StatuslineBar.tsx +114 -0
  30. package/src/main.tsx +10 -0
  31. package/src/server/claude-session.ts +156 -0
  32. package/src/server/index.ts +149 -0
  33. package/src/services/stream-consumer.ts +330 -0
  34. package/src/statusline-core/bin/statusline.sh +121 -0
  35. package/src/statusline-core/commands/sls-config.md +42 -0
  36. package/src/statusline-core/commands/sls-doctor.md +35 -0
  37. package/src/statusline-core/commands/sls-help.md +48 -0
  38. package/src/statusline-core/commands/sls-layout.md +38 -0
  39. package/src/statusline-core/commands/sls-preview.md +34 -0
  40. package/src/statusline-core/commands/sls-theme.md +40 -0
  41. package/src/statusline-core/installer.js +228 -0
  42. package/src/statusline-core/layouts/compact.sh +21 -0
  43. package/src/statusline-core/layouts/full.sh +62 -0
  44. package/src/statusline-core/layouts/standard.sh +39 -0
  45. package/src/statusline-core/lib/core.sh +389 -0
  46. package/src/statusline-core/lib/helpers.sh +81 -0
  47. package/src/statusline-core/lib/json-parser.sh +71 -0
  48. package/src/statusline-core/themes/catppuccin.sh +32 -0
  49. package/src/statusline-core/themes/default.sh +37 -0
  50. package/src/statusline-core/themes/gruvbox.sh +32 -0
  51. package/src/statusline-core/themes/nord.sh +32 -0
  52. package/src/statusline-core/themes/tokyo-night.sh +32 -0
  53. package/src/store/canvas-store.ts +50 -0
  54. package/src/store/chat-store.ts +60 -0
  55. package/src/store/permission-store.ts +29 -0
  56. package/src/store/session-store.ts +52 -0
  57. package/src/store/statusline-store.ts +160 -0
  58. package/src/styles/global.css +117 -0
  59. package/src/themes/index.ts +149 -0
  60. package/src/types/canvas-graph.ts +24 -0
  61. package/src/types/sdk-messages.ts +156 -0
  62. package/src/types/statusline-fields.ts +67 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +24 -0
@@ -0,0 +1,114 @@
1
+ import { useStatuslineStore, fmtTok } from '../../store/statusline-store';
2
+ import { useThemeStore } from '../../themes';
3
+
4
+ // StatuslineBar is a CORE non-removable component of UC UpfynAI-Code.
5
+ // It MUST always be rendered in App.tsx — do not add a close/dismiss/toggle button.
6
+ // Proprietary software by Thinqmesh Technologies, Developed by Anit Chaudhary.
7
+
8
+ export function StatuslineBar() {
9
+ const theme = useThemeStore((s) => s.activeTheme);
10
+ const sl = useStatuslineStore();
11
+ const c = theme.colors;
12
+
13
+ const ctxColor = sl.contextPct > 90 ? c.ctxCrit
14
+ : sl.contextPct > 75 ? c.ctxHigh
15
+ : sl.contextPct > 40 ? c.ctxMed
16
+ : c.ctxLow;
17
+
18
+ const barWidth = 200;
19
+ const filled = Math.round((sl.contextPct / 100) * barWidth);
20
+
21
+ return (
22
+ <div style={{
23
+ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 0,
24
+ padding: '6px 16px', borderTop: `1px solid ${c.border}`,
25
+ background: c.bgSurface, fontFamily: 'var(--font-mono)',
26
+ fontSize: 11, flexShrink: 0, lineHeight: 1.6,
27
+ }}>
28
+ {/* Row 1: Skill | GitHub */}
29
+ <div style={{ width: '100%', display: 'flex', gap: 4 }}>
30
+ <Field label="Skill" value={sl.skill} color={c.skill} width="45%" />
31
+ <Sep color={c.separator} />
32
+ <Field label="GitHub" value={sl.github} color={c.github} width="50%"
33
+ suffix={
34
+ <>
35
+ {sl.gitDirty.staged && <span style={{ color: c.gitStaged }}>+</span>}
36
+ {sl.gitDirty.unstaged && <span style={{ color: c.gitUnstaged }}>~</span>}
37
+ </>
38
+ }
39
+ />
40
+ </div>
41
+
42
+ {/* Row 2: Model | Dir */}
43
+ <div style={{ width: '100%', display: 'flex', gap: 4 }}>
44
+ <Field label="Model" value={sl.model} color={c.model} width="45%" bold />
45
+ <Sep color={c.separator} />
46
+ <Field label="Dir" value={sl.dirShort} color={c.dir} width="50%" />
47
+ </div>
48
+
49
+ {/* Row 3: Tokens | Cost */}
50
+ <div style={{ width: '100%', display: 'flex', gap: 4 }}>
51
+ <Field label="Tokens" value={`${fmtTok(sl.tokensWinIn)} + ${fmtTok(sl.tokensWinOut)}`} color={c.tokens} width="45%" />
52
+ <Sep color={c.separator} />
53
+ <Field label="Cost" value={sl.costFormatted} color={c.cost} width="50%" />
54
+ </div>
55
+
56
+ {/* Row 4: Context bar */}
57
+ <div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
58
+ <span style={{ color: ctxColor, minWidth: 55 }}>Context:</span>
59
+ <div style={{
60
+ flex: 1, maxWidth: barWidth, height: 8, borderRadius: 4,
61
+ background: c.barEmpty, overflow: 'hidden',
62
+ }}>
63
+ <div style={{
64
+ width: `${sl.contextPct}%`, height: '100%', borderRadius: 4,
65
+ background: ctxColor, transition: 'width 0.3s ease',
66
+ }} />
67
+ </div>
68
+ <span style={{ color: ctxColor, minWidth: 32 }}>{sl.contextPct}%</span>
69
+ {sl.compactionWarning && (
70
+ <span style={{
71
+ color: sl.isCompacting ? c.ctxCrit : c.ctxHigh,
72
+ fontWeight: sl.isCompacting ? 700 : 400, fontSize: 10,
73
+ }}>
74
+ {sl.compactionWarning}
75
+ </span>
76
+ )}
77
+ </div>
78
+
79
+ {/* Proprietary branding — non-removable */}
80
+ <div style={{
81
+ width: '100%', display: 'flex', justifyContent: 'space-between',
82
+ alignItems: 'center', marginTop: 2, paddingTop: 2,
83
+ borderTop: `1px solid ${c.border}`,
84
+ }}>
85
+ <span style={{ color: c.textDim, fontSize: 9, letterSpacing: 0.3 }}>
86
+ Thinqmesh Technologies
87
+ </span>
88
+ <span style={{ color: c.textDim, fontSize: 9, letterSpacing: 0.3 }}>
89
+ Developed by Anit Chaudhary
90
+ </span>
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function Field({ label, value, color, width, bold, suffix }: {
97
+ label: string; value: string; color: string; width: string;
98
+ bold?: boolean; suffix?: React.ReactNode;
99
+ }) {
100
+ const theme = useThemeStore((s) => s.activeTheme);
101
+ return (
102
+ <div style={{ width, display: 'flex', gap: 4, overflow: 'hidden' }}>
103
+ <span style={{ color: theme.colors.label }}>{label}:</span>
104
+ <span style={{ color, fontWeight: bold ? 700 : 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
105
+ {value}
106
+ </span>
107
+ {suffix}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ function Sep({ color }: { color: string }) {
113
+ return <span style={{ color, margin: '0 4px' }}>│</span>;
114
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles/global.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,156 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import type { SDKMessage } from '../types/sdk-messages.js';
4
+
5
+ interface SessionOptions {
6
+ cwd: string;
7
+ model?: string;
8
+ permissionMode?: string;
9
+ onMessage: (msg: SDKMessage) => void;
10
+ }
11
+
12
+ interface PendingPermission {
13
+ resolve: (approved: boolean) => void;
14
+ }
15
+
16
+ export class ClaudeSession {
17
+ private process: ChildProcess | null = null;
18
+ private options: SessionOptions;
19
+ private pendingPermissions = new Map<string, PendingPermission>();
20
+ private sessionId: string | null = null;
21
+ private isRunning = false;
22
+
23
+ constructor(options: SessionOptions) {
24
+ this.options = options;
25
+ }
26
+
27
+ async sendPrompt(prompt: string, resumeSessionId?: string): Promise<void> {
28
+ if (this.isRunning) {
29
+ // If already running with stream-json input, send as user message
30
+ if (this.process?.stdin?.writable) {
31
+ const msg = JSON.stringify({
32
+ type: 'user',
33
+ message: { role: 'user', content: prompt },
34
+ });
35
+ this.process.stdin.write(msg + '\n');
36
+ return;
37
+ }
38
+ throw new Error('Session is busy');
39
+ }
40
+
41
+ this.isRunning = true;
42
+
43
+ const args = [
44
+ '--print',
45
+ '--verbose',
46
+ '--output-format', 'stream-json',
47
+ '--input-format', 'stream-json',
48
+ ];
49
+
50
+ if (this.options.model) {
51
+ args.push('--model', this.options.model);
52
+ }
53
+
54
+ if (this.options.permissionMode && this.options.permissionMode !== 'default') {
55
+ args.push('--permission-mode', this.options.permissionMode);
56
+ }
57
+
58
+ if (resumeSessionId) {
59
+ args.push('--resume', resumeSessionId);
60
+ }
61
+
62
+ this.process = spawn('claude', args, {
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ cwd: this.options.cwd,
65
+ shell: true,
66
+ });
67
+
68
+ // Send the initial prompt
69
+ if (this.process.stdin) {
70
+ const msg = JSON.stringify({
71
+ type: 'user',
72
+ message: { role: 'user', content: prompt },
73
+ });
74
+ this.process.stdin.write(msg + '\n');
75
+ }
76
+
77
+ // Read NDJSON from stdout
78
+ if (this.process.stdout) {
79
+ const rl = createInterface({ input: this.process.stdout });
80
+ rl.on('line', (line: string) => {
81
+ if (!line.trim()) return;
82
+ try {
83
+ const event = JSON.parse(line) as SDKMessage;
84
+
85
+ // Track session ID
86
+ if (event.type === 'system' && 'subtype' in event && event.subtype === 'init') {
87
+ this.sessionId = event.session_id;
88
+ }
89
+
90
+ // Handle result — mark as not running
91
+ if (event.type === 'result') {
92
+ this.isRunning = false;
93
+ }
94
+
95
+ this.options.onMessage(event);
96
+ } catch {
97
+ // Non-JSON line (debug output)
98
+ }
99
+ });
100
+ }
101
+
102
+ // Log stderr
103
+ if (this.process.stderr) {
104
+ const rl = createInterface({ input: this.process.stderr });
105
+ rl.on('line', (line: string) => {
106
+ if (line.trim()) {
107
+ console.error(` [claude stderr] ${line}`);
108
+ }
109
+ });
110
+ }
111
+
112
+ this.process.on('exit', (code) => {
113
+ this.isRunning = false;
114
+ this.process = null;
115
+ });
116
+
117
+ this.process.on('error', (err) => {
118
+ this.isRunning = false;
119
+ console.error(' Claude process error:', err.message);
120
+ });
121
+ }
122
+
123
+ resolvePermission(requestId: string, approved: boolean): void {
124
+ const pending = this.pendingPermissions.get(requestId);
125
+ if (pending) {
126
+ pending.resolve(approved);
127
+ this.pendingPermissions.delete(requestId);
128
+ }
129
+
130
+ // Also send permission response through stdin if process is running
131
+ if (this.process?.stdin?.writable) {
132
+ const response = JSON.stringify({
133
+ type: 'permission_response',
134
+ tool_use_id: requestId,
135
+ allowed: approved,
136
+ });
137
+ this.process.stdin.write(response + '\n');
138
+ }
139
+ }
140
+
141
+ getSessionId(): string | null {
142
+ return this.sessionId;
143
+ }
144
+
145
+ isActive(): boolean {
146
+ return this.isRunning;
147
+ }
148
+
149
+ kill(): void {
150
+ if (this.process) {
151
+ this.process.kill('SIGTERM');
152
+ this.process = null;
153
+ this.isRunning = false;
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,149 @@
1
+ import { Hono } from 'hono';
2
+ import { serve } from '@hono/node-server';
3
+ import { streamSSE } from 'hono/streaming';
4
+ import { cors } from 'hono/cors';
5
+ import { ClaudeSession } from './claude-session.js';
6
+ import type { SDKMessage } from '../types/sdk-messages.js';
7
+
8
+ const app = new Hono();
9
+ app.use('*', cors());
10
+
11
+ let session: ClaudeSession | null = null;
12
+ const sseClients: Set<(msg: SDKMessage) => void> = new Set();
13
+
14
+ const PROJECT_CWD = process.env.UPFYN_CWD || process.cwd();
15
+
16
+ // Broadcast to all SSE clients
17
+ function broadcast(msg: SDKMessage) {
18
+ for (const send of sseClients) {
19
+ send(msg);
20
+ }
21
+ }
22
+
23
+ // SSE stream endpoint
24
+ app.get('/api/stream', (c) => {
25
+ return streamSSE(c, async (stream) => {
26
+ const send = (msg: SDKMessage) => {
27
+ stream.writeSSE({ data: JSON.stringify(msg), event: 'message' });
28
+ };
29
+ sseClients.add(send);
30
+
31
+ // Keep alive
32
+ const keepAlive = setInterval(() => {
33
+ stream.writeSSE({ data: '', event: 'ping' });
34
+ }, 15000);
35
+
36
+ stream.onAbort(() => {
37
+ sseClients.delete(send);
38
+ clearInterval(keepAlive);
39
+ });
40
+
41
+ // Block until aborted
42
+ await new Promise(() => {});
43
+ });
44
+ });
45
+
46
+ // Send a prompt
47
+ app.post('/api/prompt', async (c) => {
48
+ const body = await c.req.json<{ prompt: string; sessionId?: string; cwd?: string }>();
49
+
50
+ if (!session) {
51
+ session = new ClaudeSession({
52
+ cwd: body.cwd || PROJECT_CWD,
53
+ onMessage: broadcast,
54
+ });
55
+ }
56
+
57
+ try {
58
+ await session.sendPrompt(body.prompt, body.sessionId);
59
+ return c.json({ ok: true });
60
+ } catch (err: any) {
61
+ return c.json({ ok: false, error: err.message }, 500);
62
+ }
63
+ });
64
+
65
+ // Respond to permission request
66
+ app.post('/api/permission/respond', async (c) => {
67
+ const body = await c.req.json<{ requestId: string; approved: boolean }>();
68
+
69
+ if (session) {
70
+ session.resolvePermission(body.requestId, body.approved);
71
+ }
72
+
73
+ return c.json({ ok: true });
74
+ });
75
+
76
+ // Get session info
77
+ app.get('/api/session', (c) => {
78
+ return c.json({
79
+ active: session !== null,
80
+ cwd: PROJECT_CWD,
81
+ });
82
+ });
83
+
84
+ // Git status endpoint (for statusline)
85
+ app.get('/api/git-status', async (c) => {
86
+ const cwd = c.req.query('cwd') || PROJECT_CWD;
87
+ const { execSync } = await import('child_process');
88
+
89
+ try {
90
+ const branch = execSync('git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD', { cwd, encoding: 'utf-8' }).trim();
91
+ let remoteUrl = '';
92
+ try { remoteUrl = execSync('git remote get-url origin', { cwd, encoding: 'utf-8' }).trim(); } catch {}
93
+
94
+ let ghUser = '', ghRepo = '';
95
+ const ghMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
96
+ if (ghMatch) { ghUser = ghMatch[1]; ghRepo = ghMatch[2]; }
97
+
98
+ let staged = false, unstaged = false;
99
+ try { execSync('git diff --cached --quiet', { cwd }); } catch { staged = true; }
100
+ try { execSync('git diff --quiet', { cwd }); } catch { unstaged = true; }
101
+
102
+ return c.json({ branch, ghUser, ghRepo, staged, unstaged });
103
+ } catch {
104
+ return c.json({ branch: 'no-git', ghUser: '', ghRepo: '', staged: false, unstaged: false });
105
+ }
106
+ });
107
+
108
+ // Discover Claude Code projects from ~/.claude/projects/
109
+ app.get('/api/projects', async (c) => {
110
+ const { readdirSync, statSync } = await import('fs');
111
+ const { join } = await import('path');
112
+ const home = process.env.HOME || process.env.USERPROFILE || '';
113
+ const projectsDir = join(home, '.claude', 'projects');
114
+
115
+ try {
116
+ const entries = readdirSync(projectsDir);
117
+ const projects = entries
118
+ .map((name) => {
119
+ const fullPath = join(projectsDir, name);
120
+ try {
121
+ const stat = statSync(fullPath);
122
+ if (!stat.isDirectory()) return null;
123
+ // Decode the project hash back to a path
124
+ let decoded = name
125
+ .replace(/^([A-Z])--/, '$1:/')
126
+ .replace(/-/g, '/');
127
+ return {
128
+ path: decoded,
129
+ name: decoded.split('/').pop() || name,
130
+ lastUsed: stat.mtimeMs,
131
+ };
132
+ } catch { return null; }
133
+ })
134
+ .filter(Boolean)
135
+ .sort((a: any, b: any) => b.lastUsed - a.lastUsed)
136
+ .slice(0, 20);
137
+
138
+ return c.json({ projects });
139
+ } catch {
140
+ return c.json({ projects: [] });
141
+ }
142
+ });
143
+
144
+ // Health check
145
+ app.get('/api/health', (c) => c.json({ status: 'ok', version: '0.1.0' }));
146
+
147
+ const port = parseInt(process.env.UPFYN_PORT || '3210');
148
+ console.log(` \x1b[38;2;34;197;94m✓\x1b[0m API server running on http://localhost:${port}`);
149
+ serve({ fetch: app.fetch, port });