@harborgroup/my-team 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Hudson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # my-team
2
+
3
+ Chat with your local Claude Code subscription from a web dashboard, scoped to whatever repo you're working in. Dev-only — no API key required, no production code path.
4
+
5
+ It drives the official [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk), which spawns your existing `claude` login/subscription. If you're logged in via `claude login`, it just works.
6
+
7
+ ## Setup
8
+
9
+ In any repo:
10
+
11
+ ```sh
12
+ npx @harborgroup/my-team init
13
+ ```
14
+
15
+ This adds `@harborgroup/my-team` as a devDependency and an `npm run team` script.
16
+
17
+ ## Usage
18
+
19
+ ```sh
20
+ npm run team
21
+ ```
22
+
23
+ Opens a local, token-protected dashboard in your browser (bound to `127.0.0.1` only). If Claude Code isn't installed or logged in yet, you'll see onboarding instructions instead.
24
+
25
+ Chat runs with this repo as Claude's working directory, and tool calls (file edits, shell commands) are auto-approved via Claude's own safety classifier (`permissionMode: 'auto'`) rather than prompted per action — there's no terminal in this flow to answer y/n prompts.
26
+
27
+ ## Status
28
+
29
+ v1: onboarding + a single chat session per launch. No persisted chat history, no multi-user support.
package/dist/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { runInit } from './init.js';
3
+ import { startServer } from './server/start.js';
4
+ async function main() {
5
+ if (process.env.NODE_ENV === 'production') {
6
+ console.error('my-team is a dev-only tool and refuses to run with NODE_ENV=production.');
7
+ process.exitCode = 1;
8
+ return;
9
+ }
10
+ const [, , command] = process.argv;
11
+ if (command === 'init') {
12
+ await runInit(process.cwd());
13
+ }
14
+ else {
15
+ await startServer(process.cwd());
16
+ }
17
+ }
18
+ main().catch((err) => {
19
+ console.error(err);
20
+ process.exitCode = 1;
21
+ });
package/dist/init.js ADDED
@@ -0,0 +1,50 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ // dist/init.js -> package root is one level up.
7
+ const OWN_PACKAGE_ROOT = path.join(__dirname, '..');
8
+ const OWN_PACKAGE_JSON = JSON.parse(readFileSync(path.join(OWN_PACKAGE_ROOT, 'package.json'), 'utf-8'));
9
+ function npmInstall(cwd, spec) {
10
+ // On Windows, npm is a .cmd batch file that spawnSync can't execute
11
+ // directly (EINVAL). Routing through cmd.exe /c as the actual child
12
+ // (rather than shell: true) avoids that without the argument-escaping
13
+ // risk Node warns about (DEP0190) for shell: true + an args array.
14
+ const result = process.platform === 'win32'
15
+ ? spawnSync('cmd.exe', ['/d', '/s', '/c', 'npm', 'install', '--save-dev', spec], { cwd, stdio: 'inherit' })
16
+ : spawnSync('npm', ['install', '--save-dev', spec], { cwd, stdio: 'inherit' });
17
+ return result.status === 0;
18
+ }
19
+ function addTeamScript(repoPackageJsonPath) {
20
+ const raw = readFileSync(repoPackageJsonPath, 'utf-8');
21
+ const pkg = JSON.parse(raw);
22
+ pkg.scripts ??= {};
23
+ if (pkg.scripts.team) {
24
+ if (pkg.scripts.team !== 'my-team') {
25
+ console.warn(`package.json already has a "team" script ("${pkg.scripts.team}") — leaving it untouched. Run "npx my-team" directly instead.`);
26
+ }
27
+ return;
28
+ }
29
+ pkg.scripts.team = 'my-team';
30
+ writeFileSync(repoPackageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
31
+ console.log('Added "npm run team" script to package.json.');
32
+ }
33
+ export async function runInit(cwd) {
34
+ const repoPackageJsonPath = path.join(cwd, 'package.json');
35
+ const name = OWN_PACKAGE_JSON.name;
36
+ if (!existsSync(repoPackageJsonPath)) {
37
+ console.error(`No package.json found at ${cwd}. Run "npm init" first, then re-run "npx ${name} init".`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const version = OWN_PACKAGE_JSON.version;
42
+ const installed = npmInstall(cwd, `${name}@${version}`) || npmInstall(cwd, OWN_PACKAGE_ROOT);
43
+ if (!installed) {
44
+ console.error('Failed to install my-team as a devDependency.');
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ addTeamScript(repoPackageJsonPath);
49
+ console.log('Done — run "npm run team" to launch.');
50
+ }
@@ -0,0 +1,45 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { sanitizeEnv } from './env.js';
3
+ /**
4
+ * Live probe, run fresh on every /api/status call rather than caching an
5
+ * "onboarded" flag, so it can't go stale.
6
+ *
7
+ * Iterating a query() call to completion sends a real, billed model turn
8
+ * (confirmed empirically) even for an empty prompt. Aborting as soon as the
9
+ * `system init` message arrives is enough to call accountInfo() without ever
10
+ * dispatching a turn.
11
+ */
12
+ export async function checkAuth(cwd) {
13
+ const controller = new AbortController();
14
+ const q = query({
15
+ prompt: '',
16
+ options: {
17
+ cwd,
18
+ env: sanitizeEnv(process.env),
19
+ permissionMode: 'plan',
20
+ abortController: controller,
21
+ maxTurns: 1,
22
+ },
23
+ });
24
+ try {
25
+ for await (const msg of q) {
26
+ if (msg.type === 'system' && msg.subtype === 'init') {
27
+ const accountInfo = await q.accountInfo();
28
+ controller.abort();
29
+ if (!accountInfo.email && !accountInfo.apiProvider) {
30
+ return { ok: false, reason: 'not-authenticated' };
31
+ }
32
+ return { ok: true, accountInfo };
33
+ }
34
+ }
35
+ return { ok: false, reason: 'not-authenticated' };
36
+ }
37
+ catch (err) {
38
+ controller.abort();
39
+ const message = String(err?.message ?? err);
40
+ if (/native binary not found|ENOENT|not recognized as an internal/i.test(message)) {
41
+ return { ok: false, reason: 'cli-missing' };
42
+ }
43
+ return { ok: false, reason: 'not-authenticated' };
44
+ }
45
+ }
@@ -0,0 +1,60 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { sanitizeEnv } from './env.js';
3
+ /**
4
+ * One ongoing SDK session per running server process. Conversation history
5
+ * lives only in the CLI's own session store (resumed via session_id) — no
6
+ * disk persistence of our own, lost on server restart (acceptable for v1).
7
+ */
8
+ export class ChatSession {
9
+ sessionId;
10
+ turnInFlight = false;
11
+ isBusy() {
12
+ return this.turnInFlight;
13
+ }
14
+ async *sendTurn(message, cwd) {
15
+ if (this.turnInFlight) {
16
+ yield { type: 'error', message: 'A previous turn is still in progress.' };
17
+ return;
18
+ }
19
+ this.turnInFlight = true;
20
+ try {
21
+ const q = query({
22
+ prompt: message,
23
+ options: {
24
+ cwd,
25
+ env: sanitizeEnv(process.env),
26
+ permissionMode: 'auto',
27
+ ...(this.sessionId ? { resume: this.sessionId } : {}),
28
+ },
29
+ });
30
+ for await (const msg of q) {
31
+ if (msg.type === 'system' && msg.subtype === 'init') {
32
+ this.sessionId = msg.session_id;
33
+ yield { type: 'meta', sessionId: msg.session_id };
34
+ }
35
+ else if (msg.type === 'assistant') {
36
+ for (const block of msg.message.content) {
37
+ if (block.type === 'text') {
38
+ yield { type: 'text', text: block.text };
39
+ }
40
+ else if (block.type === 'tool_use') {
41
+ yield { type: 'tool-use', name: block.name };
42
+ }
43
+ }
44
+ }
45
+ else if (msg.type === 'result') {
46
+ if (msg.subtype !== 'success') {
47
+ yield { type: 'error', message: msg.subtype };
48
+ }
49
+ yield { type: 'done' };
50
+ }
51
+ }
52
+ }
53
+ catch (err) {
54
+ yield { type: 'error', message: String(err?.message ?? err) };
55
+ }
56
+ finally {
57
+ this.turnInFlight = false;
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Claude Code refuses to start when it detects it's already running inside
3
+ * another Claude Code session. Strip those markers so `my-team` still works
4
+ * when launched from a terminal that's itself inside a `claude` session.
5
+ */
6
+ export function sanitizeEnv(base) {
7
+ const env = { ...base };
8
+ delete env.CLAUDECODE;
9
+ delete env.CLAUDE_CODE_ENTRYPOINT;
10
+ delete env.CLAUDE_CODE_SESSION;
11
+ return env;
12
+ }
@@ -0,0 +1,94 @@
1
+ import http from 'node:http';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { checkAuth } from './auth-check.js';
6
+ import { ChatSession } from './chat-session.js';
7
+ import { tokensMatch } from './token.js';
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const WEB_DIR = path.join(__dirname, '..', 'web');
10
+ const STATIC_FILES = {
11
+ '/': { file: 'index.html', contentType: 'text/html; charset=utf-8' },
12
+ '/app.js': { file: 'app.js', contentType: 'text/javascript; charset=utf-8' },
13
+ '/style.css': { file: 'style.css', contentType: 'text/css; charset=utf-8' },
14
+ };
15
+ function isAllowedHost(hostHeader, port) {
16
+ if (!hostHeader)
17
+ return false;
18
+ return hostHeader === `127.0.0.1:${port}` || hostHeader === `localhost:${port}`;
19
+ }
20
+ function getToken(req, url) {
21
+ return req.headers['x-my-team-token'] ?? url.searchParams.get('token') ?? undefined;
22
+ }
23
+ async function readJsonBody(req) {
24
+ const chunks = [];
25
+ for await (const chunk of req)
26
+ chunks.push(chunk);
27
+ const raw = Buffer.concat(chunks).toString('utf-8');
28
+ return raw ? JSON.parse(raw) : {};
29
+ }
30
+ export function createServer(options) {
31
+ const { cwd, token, portRef } = options;
32
+ const chatSession = new ChatSession();
33
+ return http.createServer(async (req, res) => {
34
+ if (!isAllowedHost(req.headers.host, portRef.port)) {
35
+ res.writeHead(400, { 'Content-Type': 'text/plain' }).end('Bad Host header');
36
+ return;
37
+ }
38
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${portRef.port}`);
39
+ const staticEntry = req.method === 'GET' ? STATIC_FILES[url.pathname] : undefined;
40
+ if (staticEntry) {
41
+ try {
42
+ const body = await readFile(path.join(WEB_DIR, staticEntry.file));
43
+ res.writeHead(200, { 'Content-Type': staticEntry.contentType }).end(body);
44
+ }
45
+ catch {
46
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not found');
47
+ }
48
+ return;
49
+ }
50
+ if (!url.pathname.startsWith('/api/')) {
51
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not found');
52
+ return;
53
+ }
54
+ if (!tokensMatch(token, getToken(req, url))) {
55
+ res.writeHead(401, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'unauthorized' }));
56
+ return;
57
+ }
58
+ if (req.method === 'GET' && url.pathname === '/api/status') {
59
+ const result = await checkAuth(cwd);
60
+ res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(result));
61
+ return;
62
+ }
63
+ if (req.method === 'POST' && url.pathname === '/api/chat') {
64
+ if (chatSession.isBusy()) {
65
+ res.writeHead(409, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'busy' }));
66
+ return;
67
+ }
68
+ let message;
69
+ try {
70
+ const body = await readJsonBody(req);
71
+ message = String(body.message ?? '');
72
+ }
73
+ catch {
74
+ res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'invalid body' }));
75
+ return;
76
+ }
77
+ if (!message.trim()) {
78
+ res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'empty message' }));
79
+ return;
80
+ }
81
+ res.writeHead(200, {
82
+ 'Content-Type': 'text/event-stream',
83
+ 'Cache-Control': 'no-cache',
84
+ Connection: 'keep-alive',
85
+ });
86
+ for await (const event of chatSession.sendTurn(message, cwd)) {
87
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
88
+ }
89
+ res.end();
90
+ return;
91
+ }
92
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not found');
93
+ });
94
+ }
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createServer } from './http-server.js';
3
+ import { generateToken } from './token.js';
4
+ function openBrowser(url) {
5
+ const platform = process.platform;
6
+ try {
7
+ if (platform === 'win32') {
8
+ spawn('cmd', ['/c', 'start', '""', url], { shell: false, stdio: 'ignore', detached: true }).unref();
9
+ }
10
+ else if (platform === 'darwin') {
11
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
12
+ }
13
+ else {
14
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
15
+ }
16
+ }
17
+ catch {
18
+ // Fall through — the URL is printed to the terminal regardless.
19
+ }
20
+ }
21
+ export async function startServer(cwd) {
22
+ const token = generateToken();
23
+ const portRef = { port: 0 };
24
+ await new Promise((resolve) => {
25
+ const server = createServer({ cwd, token, portRef });
26
+ server.listen(0, '127.0.0.1', () => {
27
+ const address = server.address();
28
+ portRef.port = typeof address === 'object' && address ? address.port : 0;
29
+ const url = `http://127.0.0.1:${portRef.port}/?token=${token}`;
30
+ console.log(`my-team is running for ${cwd}`);
31
+ console.log(`Open: ${url}`);
32
+ openBrowser(url);
33
+ resolve();
34
+ });
35
+ });
36
+ }
@@ -0,0 +1,9 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ export function generateToken() {
3
+ return randomBytes(32).toString('hex');
4
+ }
5
+ export function tokensMatch(expected, provided) {
6
+ if (!provided || provided.length !== expected.length)
7
+ return false;
8
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
9
+ }
@@ -0,0 +1,118 @@
1
+ const token = new URLSearchParams(location.search).get('token') ?? '';
2
+
3
+ const el = {
4
+ loading: document.getElementById('loading'),
5
+ onboarding: document.getElementById('onboarding'),
6
+ cliMissing: document.getElementById('onboarding-cli-missing'),
7
+ notAuthenticated: document.getElementById('onboarding-not-authenticated'),
8
+ checkAgain: document.getElementById('check-again'),
9
+ dashboard: document.getElementById('dashboard'),
10
+ accountLine: document.getElementById('account-line'),
11
+ messages: document.getElementById('messages'),
12
+ form: document.getElementById('chat-form'),
13
+ input: document.getElementById('chat-input'),
14
+ };
15
+
16
+ function api(path, options = {}) {
17
+ return fetch(path, {
18
+ ...options,
19
+ headers: { ...(options.headers ?? {}), 'X-My-Team-Token': token },
20
+ });
21
+ }
22
+
23
+ function show(section) {
24
+ el.loading.hidden = true;
25
+ el.onboarding.hidden = section !== 'onboarding';
26
+ el.dashboard.hidden = section !== 'dashboard';
27
+ }
28
+
29
+ async function checkStatus() {
30
+ el.loading.hidden = false;
31
+ el.onboarding.hidden = true;
32
+ el.dashboard.hidden = true;
33
+ const res = await api('/api/status');
34
+ const result = await res.json();
35
+
36
+ if (result.ok) {
37
+ const info = result.accountInfo;
38
+ el.accountLine.textContent = `Logged in as ${info.email ?? 'unknown'}${info.subscriptionType ? ` (${info.subscriptionType})` : ''}`;
39
+ show('dashboard');
40
+ return;
41
+ }
42
+
43
+ el.cliMissing.hidden = result.reason !== 'cli-missing';
44
+ el.notAuthenticated.hidden = result.reason !== 'not-authenticated';
45
+ show('onboarding');
46
+ }
47
+
48
+ el.checkAgain.addEventListener('click', checkStatus);
49
+
50
+ function appendMessage(className, text) {
51
+ const div = document.createElement('div');
52
+ div.className = `msg ${className}`;
53
+ div.textContent = text;
54
+ el.messages.appendChild(div);
55
+ el.messages.scrollTop = el.messages.scrollHeight;
56
+ return div;
57
+ }
58
+
59
+ async function sendMessage(message) {
60
+ appendMessage('user', message);
61
+ const assistantEl = appendMessage('assistant', '');
62
+ let assistantText = '';
63
+
64
+ const res = await api('/api/chat', {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ message }),
68
+ });
69
+
70
+ if (!res.ok || !res.body) {
71
+ appendMessage('error', `Request failed (${res.status})`);
72
+ return;
73
+ }
74
+
75
+ const reader = res.body.getReader();
76
+ const decoder = new TextDecoder();
77
+ let buffer = '';
78
+
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+ buffer += decoder.decode(value, { stream: true });
83
+
84
+ let sepIndex;
85
+ while ((sepIndex = buffer.indexOf('\n\n')) !== -1) {
86
+ const frame = buffer.slice(0, sepIndex);
87
+ buffer = buffer.slice(sepIndex + 2);
88
+ const line = frame.split('\n').find((l) => l.startsWith('data: '));
89
+ if (!line) continue;
90
+ const event = JSON.parse(line.slice('data: '.length));
91
+
92
+ if (event.type === 'text') {
93
+ assistantText += event.text;
94
+ assistantEl.textContent = assistantText;
95
+ } else if (event.type === 'tool-use') {
96
+ appendMessage('tool', `\u{1F527} using tool: ${event.name}`);
97
+ } else if (event.type === 'error') {
98
+ appendMessage('error', event.message);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ el.form.addEventListener('submit', async (e) => {
105
+ e.preventDefault();
106
+ const message = el.input.value.trim();
107
+ if (!message) return;
108
+ el.input.value = '';
109
+ el.input.disabled = true;
110
+ try {
111
+ await sendMessage(message);
112
+ } finally {
113
+ el.input.disabled = false;
114
+ el.input.focus();
115
+ }
116
+ });
117
+
118
+ checkStatus();
@@ -0,0 +1,44 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>my-team</title>
6
+ <link rel="stylesheet" href="/style.css" />
7
+ </head>
8
+ <body>
9
+ <main id="app">
10
+ <p id="loading">Checking Claude Code status&hellip;</p>
11
+
12
+ <section id="onboarding" hidden>
13
+ <h1>Set up Claude Code</h1>
14
+ <div id="onboarding-cli-missing" hidden>
15
+ <p>The Claude Code CLI binary couldn't be found or run.</p>
16
+ <p>Try installing it globally, then reload this page:</p>
17
+ <pre>npm install -g @anthropic-ai/claude-code</pre>
18
+ </div>
19
+ <div id="onboarding-not-authenticated" hidden>
20
+ <p>Claude Code isn't logged in yet. In a terminal, run:</p>
21
+ <pre>claude login</pre>
22
+ <p>Then come back here.</p>
23
+ </div>
24
+ <button id="check-again">Check again</button>
25
+ </section>
26
+
27
+ <section id="dashboard" hidden>
28
+ <header>
29
+ <span id="account-line"></span>
30
+ </header>
31
+ <div class="banner">
32
+ Tool actions (file edits, shell commands) are auto-approved by Claude's own safety classifier &mdash;
33
+ not reviewed by you turn-by-turn.
34
+ </div>
35
+ <div id="messages"></div>
36
+ <form id="chat-form">
37
+ <input id="chat-input" type="text" placeholder="Ask Claude about this repo&hellip;" autocomplete="off" />
38
+ <button type="submit">Send</button>
39
+ </form>
40
+ </section>
41
+ </main>
42
+ <script src="/app.js"></script>
43
+ </body>
44
+ </html>
@@ -0,0 +1,130 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --fg: #1a1a1a;
4
+ --bg: #ffffff;
5
+ --muted: #666;
6
+ --border: #ddd;
7
+ --accent: #2563eb;
8
+ --banner-bg: #fff7e6;
9
+ --banner-fg: #7a4a00;
10
+ }
11
+
12
+ @media (prefers-color-scheme: dark) {
13
+ :root {
14
+ --fg: #eee;
15
+ --bg: #1a1a1a;
16
+ --muted: #999;
17
+ --border: #333;
18
+ --accent: #60a5fa;
19
+ --banner-bg: #3a2e0f;
20
+ --banner-fg: #f0c979;
21
+ }
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ body {
29
+ margin: 0;
30
+ font-family: system-ui, -apple-system, sans-serif;
31
+ color: var(--fg);
32
+ background: var(--bg);
33
+ }
34
+
35
+ #app {
36
+ max-width: 720px;
37
+ margin: 0 auto;
38
+ padding: 2rem 1rem;
39
+ height: 100vh;
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+
44
+ pre {
45
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
46
+ padding: 0.75rem 1rem;
47
+ border-radius: 6px;
48
+ overflow-x: auto;
49
+ }
50
+
51
+ .banner {
52
+ background: var(--banner-bg);
53
+ color: var(--banner-fg);
54
+ padding: 0.6rem 0.9rem;
55
+ border-radius: 6px;
56
+ font-size: 0.85rem;
57
+ margin-bottom: 1rem;
58
+ }
59
+
60
+ #dashboard {
61
+ display: flex;
62
+ flex-direction: column;
63
+ flex: 1;
64
+ min-height: 0;
65
+ }
66
+
67
+ header {
68
+ margin-bottom: 0.5rem;
69
+ color: var(--muted);
70
+ font-size: 0.9rem;
71
+ }
72
+
73
+ #messages {
74
+ flex: 1;
75
+ overflow-y: auto;
76
+ border: 1px solid var(--border);
77
+ border-radius: 8px;
78
+ padding: 1rem;
79
+ margin-bottom: 1rem;
80
+ }
81
+
82
+ .msg {
83
+ margin-bottom: 0.75rem;
84
+ white-space: pre-wrap;
85
+ line-height: 1.4;
86
+ }
87
+
88
+ .msg.user {
89
+ font-weight: 600;
90
+ }
91
+
92
+ .msg.tool {
93
+ color: var(--muted);
94
+ font-style: italic;
95
+ font-size: 0.85rem;
96
+ }
97
+
98
+ .msg.error {
99
+ color: #d33;
100
+ }
101
+
102
+ #chat-form {
103
+ display: flex;
104
+ gap: 0.5rem;
105
+ }
106
+
107
+ #chat-input {
108
+ flex: 1;
109
+ padding: 0.6rem 0.8rem;
110
+ border-radius: 6px;
111
+ border: 1px solid var(--border);
112
+ background: var(--bg);
113
+ color: var(--fg);
114
+ font-size: 1rem;
115
+ }
116
+
117
+ button {
118
+ padding: 0.6rem 1.2rem;
119
+ border-radius: 6px;
120
+ border: none;
121
+ background: var(--accent);
122
+ color: white;
123
+ font-size: 1rem;
124
+ cursor: pointer;
125
+ }
126
+
127
+ button:disabled {
128
+ opacity: 0.5;
129
+ cursor: default;
130
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@harborgroup/my-team",
3
+ "version": "0.1.0",
4
+ "description": "Chat with your local Claude Code subscription from a web dashboard, scoped to the current repo. Dev-only.",
5
+ "type": "module",
6
+ "bin": {
7
+ "my-team": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json && node scripts/copy-web-assets.mjs",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@anthropic-ai/claude-agent-sdk": "^0.3.201",
24
+ "@anthropic-ai/sdk": "^0.110.0",
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "zod": "^4.4.3"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^26.1.0",
30
+ "typescript": "^6.0.3"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/anthonyhudson-hg/my-team.git"
36
+ },
37
+ "homepage": "https://github.com/anthonyhudson-hg/my-team#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/anthonyhudson-hg/my-team/issues"
40
+ }
41
+ }