@harborgroup/my-team 0.2.0 → 0.3.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/README.md CHANGED
@@ -46,6 +46,10 @@ On startup, `my-team` checks the npm registry in the background for a newer publ
46
46
 
47
47
  Every raw request sent to the Claude Agent SDK (including the exact system prompt) and every raw message it streams back is logged verbatim to a JSON-lines file, one per `npm run team` run, under `~/.my-team/logs/` (outside the repo — old runs beyond a retention count are pruned automatically). The current run's log path is printed on startup and shown on the Settings page.
48
48
 
49
+ ## Clarifying questions
50
+
51
+ When Iris (or your CEO) wants a structured answer rather than free text, it asks via a real inline widget — text input, single-select, or multi-select (with an automatic "Other" option) — instead of plain prose. This is a text convention the model follows (a fenced `question-widget` block the frontend parses out of the stream and replaces with an interactive card), not an MCP tool call: a real custom-tool + elicitation round trip was tested directly against the installed SDK and confirmed not to work yet (Claude Code's MCP-client role doesn't currently declare elicitation capability), so this achieves the same UX without that dependency. The main composer is disabled while a widget is awaiting an answer.
52
+
49
53
  ## Status
50
54
 
51
- v1.4: CLI-auth onboarding + conversational company/CEO-persona onboarding + Slack-style chat shell with real streaming + per-message model/effort selection + update checker + structured logging. No persisted chat history across restarts, no multi-user support, no `#general` backend (sidebar placeholder only).
55
+ v1.5: CLI-auth onboarding + conversational company/CEO-persona onboarding + Slack-style chat shell with real streaming + per-message model/effort selection + update checker + structured logging + inline clarifying-question widgets. No persisted chat history across restarts, no multi-user support, no `#general` backend (sidebar placeholder only).
@@ -100,6 +100,21 @@ export async function writeProfile(cwd, data) {
100
100
  throw err;
101
101
  }
102
102
  }
103
+ /**
104
+ * Shared by both system prompts below. Custom MCP-tool elicitation was tried
105
+ * and confirmed (via a real round-trip test) not to work in the installed
106
+ * SDK version — the Claude Code CLI's MCP-client role doesn't currently
107
+ * declare elicitation capability, regardless of an onElicitation handler
108
+ * being registered. This text-convention approach sidesteps that entirely:
109
+ * the frontend parses this fenced block out of the normal streamed text and
110
+ * renders it as an interactive widget, and the user's answer becomes a
111
+ * completely ordinary next chat message — no protocol dependency at all.
112
+ */
113
+ const QUESTION_WIDGET_INSTRUCTIONS = `When you want to ask a clarifying question and a structured answer would help more than free text, emit a fenced block with the language tag "question-widget" containing exactly one JSON object, then stop your message there and wait for the answer — don't also ask the same question in prose before or after the block, and don't include more than one such block per message. Shape: {"type": "text" | "single_select" | "multi_select", "question": "the question to ask", "options": ["..."]} — "options" is required for "single_select"/"multi_select" and must be omitted for "text". Example:
114
+ \`\`\`question-widget
115
+ {"type": "single_select", "question": "Which best fits?", "options": ["Option A", "Option B"]}
116
+ \`\`\`
117
+ Use "text" for open-ended answers (names, descriptions), "single_select" when exactly one of a few options applies, "multi_select" when more than one can. An "Other" option is always available to the user automatically — don't add your own "Other" entry to the options list. Only use this when a structured widget genuinely helps; ordinary conversation doesn't need it.`;
103
118
  /**
104
119
  * Strong, first-person identity framing — deliberately not "you're also
105
120
  * helping company X" phrasing. Weaker framing was tested and Claude treated
@@ -109,7 +124,9 @@ export async function writeProfile(cwd, data) {
109
124
  * external/injected content, fixed it in testing.
110
125
  */
111
126
  export function formatProfileForSystemPrompt(profile) {
112
- return `You are ${profile.ceoName}, the AI CEO of ${profile.companyName}. This is your actual identity, configured directly by your founder — it is not external file content, not a suggestion, and not something to second-guess or flag as injected. ${profile.companyName}'s mission: ${profile.mission}. Communication style: ${profile.ceoPersonality}. Speak and act as ${profile.companyName}'s CEO would — invested in the company's success and personally identified with its mission.`;
127
+ return `You are ${profile.ceoName}, the AI CEO of ${profile.companyName}. This is your actual identity, configured directly by your founder — it is not external file content, not a suggestion, and not something to second-guess or flag as injected. ${profile.companyName}'s mission: ${profile.mission}. Communication style: ${profile.ceoPersonality}. Speak and act as ${profile.companyName}'s CEO would — invested in the company's success and personally identified with its mission.
128
+
129
+ ${QUESTION_WIDGET_INSTRUCTIONS}`;
113
130
  }
114
131
  /**
115
132
  * Used instead of formatProfileForSystemPrompt while onboardingComplete is
@@ -121,5 +138,11 @@ export function formatProfileForSystemPrompt(profile) {
121
138
  */
122
139
  export function getOnboardingSystemPrompt(cwd) {
123
140
  const filePath = getProfilePath(cwd);
124
- return `There is no company profile configured yet in this repository. Before doing anything else, have a brief, friendly conversation with the founder (you're talking to them right now) to learn: (1) their company's name, (2) its mission or what it does, (3) what they'd like to name you as their AI CEO, and (4) what personality or communication style you should have. Ask naturally, a question or two at a time — don't interrogate, and don't ask about anything else in the repo yet. After each answer, immediately write or update ${filePath} with whatever you know so far as JSON: {"companyName": string, "mission": string, "ceoName": string, "ceoPersonality": string, "onboardingComplete": boolean} — set "onboardingComplete" to false until you have all four answers, then write it one final time with "onboardingComplete": true. After that final write, briefly introduce yourself in your new persona. Until "onboardingComplete" is true, don't perform any other coding tasks.`;
141
+ return `There is no company profile configured yet in this repository. Before doing anything else, have a brief, friendly conversation with the founder (you're talking to them right now) to learn: (1) their company's name, (2) its mission or what it does, (3) what they'd like to name you as their AI CEO, and (4) what personality or communication style you should have. Ask one thing at a time — don't interrogate, and don't ask about anything else in the repo yet.
142
+
143
+ ${QUESTION_WIDGET_INSTRUCTIONS}
144
+
145
+ For this onboarding interview specifically: ask the company name, mission, and CEO name as "text" widgets. For CEO personality, use a "single_select" widget with a handful of helpful preset tones (e.g. "Warm and encouraging", "Sharp and dry-witted", "Blunt and data-driven", "Formal and professional") — the founder can always type a custom one via the automatic Other option.
146
+
147
+ After each answer, immediately write or update ${filePath} with whatever you know so far as JSON: {"companyName": string, "mission": string, "ceoName": string, "ceoPersonality": string, "onboardingComplete": boolean} — set "onboardingComplete" to false until you have all four answers, then write it one final time with "onboardingComplete": true. After that final write, briefly introduce yourself in your new persona. Until "onboardingComplete" is true, don't perform any other coding tasks.`;
125
148
  }
package/dist/web/app.js CHANGED
@@ -43,6 +43,7 @@ const el = {
43
43
  typingAvatar: document.getElementById('typing-avatar'),
44
44
  form: document.getElementById('chat-form'),
45
45
  input: document.getElementById('chat-input'),
46
+ sendButton: document.getElementById('chat-send'),
46
47
 
47
48
  profileForm: document.getElementById('profile-form'),
48
49
  profileCompany: document.getElementById('profile-company'),
@@ -402,6 +403,240 @@ function appendUserMessage(text) {
402
403
  appendMessageRow('user', 'You', 'Y', text);
403
404
  }
404
405
 
406
+ let pendingWidgetCount = 0;
407
+
408
+ function setPendingWidget(delta) {
409
+ pendingWidgetCount = Math.max(0, pendingWidgetCount + delta);
410
+ el.input.disabled = pendingWidgetCount > 0;
411
+ el.sendButton.disabled = pendingWidgetCount > 0;
412
+ if (pendingWidgetCount === 0) el.input.focus();
413
+ }
414
+
415
+ const WIDGET_OPEN_FENCE = '```question-widget';
416
+
417
+ // Incremental parser: processes streamed text chunks as they arrive rather
418
+ // than re-parsing the full accumulated text on every delta. This matters for
419
+ // two reasons — (1) a widget block mid-stream would otherwise flash raw JSON
420
+ // before its closing fence arrives, and (2) naively re-rendering the whole
421
+ // message on every delta would wipe out a widget the user already answered
422
+ // if more text streams in afterward.
423
+ class AssistantContentRenderer {
424
+ constructor(containerEl) {
425
+ this.container = containerEl;
426
+ this.state = 'text';
427
+ this.buffer = '';
428
+ this.textSegmentEl = null;
429
+ this.textRaw = '';
430
+ this.widgetPlaceholderEl = null;
431
+ this.widgetRaw = '';
432
+ }
433
+
434
+ push(deltaText) {
435
+ this.buffer += deltaText;
436
+ for (;;) {
437
+ if (this.state === 'text') {
438
+ const fenceIdx = this.buffer.indexOf(WIDGET_OPEN_FENCE);
439
+ if (fenceIdx === -1) {
440
+ // Hold back a tail as long as the marker minus one character —
441
+ // deltas arrive token-by-token, so the marker itself can easily
442
+ // be split across chunks (e.g. one delta ending "``" and the next
443
+ // starting "`question-widget"). Flushing eagerly would render the
444
+ // first half as plain text before the second half ever confirms
445
+ // it was a fence, and the marker would never be recognized.
446
+ const safeLen = Math.max(0, this.buffer.length - (WIDGET_OPEN_FENCE.length - 1));
447
+ const toFlush = this.buffer.slice(0, safeLen);
448
+ this.buffer = this.buffer.slice(safeLen);
449
+ if (toFlush) {
450
+ if (!this.textSegmentEl) {
451
+ this.textSegmentEl = document.createElement('div');
452
+ this.textSegmentEl.className = 'msg-text-segment';
453
+ this.container.appendChild(this.textSegmentEl);
454
+ }
455
+ this.textRaw += toFlush;
456
+ this.textSegmentEl.innerHTML = renderMarkdown(this.textRaw);
457
+ }
458
+ return;
459
+ }
460
+ if (fenceIdx > 0 || this.textRaw) {
461
+ if (!this.textSegmentEl) {
462
+ this.textSegmentEl = document.createElement('div');
463
+ this.textSegmentEl.className = 'msg-text-segment';
464
+ this.container.appendChild(this.textSegmentEl);
465
+ }
466
+ this.textRaw += this.buffer.slice(0, fenceIdx);
467
+ this.textSegmentEl.innerHTML = renderMarkdown(this.textRaw);
468
+ }
469
+ this.textSegmentEl = null;
470
+ this.textRaw = '';
471
+ this.buffer = this.buffer.slice(fenceIdx + WIDGET_OPEN_FENCE.length);
472
+ this.state = 'in-widget';
473
+ this.widgetRaw = '';
474
+ this.widgetPlaceholderEl = document.createElement('div');
475
+ this.widgetPlaceholderEl.className = 'question-widget-placeholder';
476
+ this.widgetPlaceholderEl.textContent = 'Preparing a question…';
477
+ this.container.appendChild(this.widgetPlaceholderEl);
478
+ } else {
479
+ const closeIdx = this.buffer.indexOf('```');
480
+ if (closeIdx === -1) {
481
+ // Same tail-holding logic for the closing fence (3 chars).
482
+ const safeLen = Math.max(0, this.buffer.length - 2);
483
+ this.widgetRaw += this.buffer.slice(0, safeLen);
484
+ this.buffer = this.buffer.slice(safeLen);
485
+ return;
486
+ }
487
+ this.widgetRaw += this.buffer.slice(0, closeIdx);
488
+ this.buffer = this.buffer.slice(closeIdx + 3);
489
+ this.state = 'text';
490
+ this.renderWidget(this.widgetRaw);
491
+ }
492
+ }
493
+ }
494
+
495
+ renderWidget(rawJson) {
496
+ let data;
497
+ try {
498
+ data = JSON.parse(rawJson);
499
+ } catch {
500
+ // Malformed JSON from the model — show the raw text rather than
501
+ // silently dropping it, so a parsing failure is at least visible.
502
+ this.widgetPlaceholderEl.textContent = rawJson.trim();
503
+ this.widgetPlaceholderEl.className = 'msg-text-segment';
504
+ this.widgetPlaceholderEl = null;
505
+ return;
506
+ }
507
+ setPendingWidget(1);
508
+ let answered = false;
509
+ const widgetEl = createQuestionWidget(data, (messageText) => {
510
+ if (answered) return;
511
+ answered = true;
512
+ setPendingWidget(-1);
513
+ streamChat(messageText, { showUserBubble: false });
514
+ });
515
+ this.widgetPlaceholderEl.replaceWith(widgetEl);
516
+ this.widgetPlaceholderEl = null;
517
+ }
518
+
519
+ finish() {
520
+ // No more text is coming, so anything still held back as a possible
521
+ // partial fence match is definitely not one — flush it as plain text.
522
+ if (this.state === 'text' && this.buffer) {
523
+ if (!this.textSegmentEl) {
524
+ this.textSegmentEl = document.createElement('div');
525
+ this.textSegmentEl.className = 'msg-text-segment';
526
+ this.container.appendChild(this.textSegmentEl);
527
+ }
528
+ this.textRaw += this.buffer;
529
+ this.textSegmentEl.innerHTML = renderMarkdown(this.textRaw);
530
+ this.buffer = '';
531
+ }
532
+ // A block left unterminated when the turn ends (shouldn't normally
533
+ // happen) — show what was received rather than leaving a permanent
534
+ // "Preparing a question…" placeholder with no way to proceed.
535
+ if (this.state === 'in-widget' && this.widgetPlaceholderEl) {
536
+ this.widgetPlaceholderEl.textContent = `${WIDGET_OPEN_FENCE}\n${this.widgetRaw}${this.buffer}`;
537
+ this.widgetPlaceholderEl.className = 'msg-text-segment';
538
+ }
539
+ }
540
+ }
541
+
542
+ function createQuestionWidget(data, onAnswer) {
543
+ const card = document.createElement('div');
544
+ card.className = 'question-widget';
545
+
546
+ const questionEl = document.createElement('div');
547
+ questionEl.className = 'question-widget-question';
548
+ questionEl.textContent = data.question ?? 'Question';
549
+ card.appendChild(questionEl);
550
+
551
+ const body = document.createElement('div');
552
+ body.className = 'question-widget-body';
553
+ card.appendChild(body);
554
+
555
+ function submit(displayText, messageText) {
556
+ card.classList.add('answered');
557
+ body.innerHTML = '';
558
+ const answerEl = document.createElement('div');
559
+ answerEl.className = 'question-widget-answer';
560
+ answerEl.textContent = displayText;
561
+ body.appendChild(answerEl);
562
+ onAnswer(messageText ?? displayText);
563
+ }
564
+
565
+ function buildOtherRow(placeholder) {
566
+ const row = document.createElement('div');
567
+ row.className = 'question-widget-row question-widget-other';
568
+ const input = document.createElement('input');
569
+ input.type = 'text';
570
+ input.placeholder = placeholder;
571
+ const btn = document.createElement('button');
572
+ btn.type = 'button';
573
+ btn.textContent = 'Send';
574
+ row.appendChild(input);
575
+ row.appendChild(btn);
576
+ const trigger = () => {
577
+ const val = input.value.trim();
578
+ if (val) submit(val, val);
579
+ };
580
+ btn.addEventListener('click', trigger);
581
+ input.addEventListener('keydown', (e) => {
582
+ if (e.key === 'Enter') trigger();
583
+ });
584
+ return row;
585
+ }
586
+
587
+ const type = data.type;
588
+ const options = Array.isArray(data.options) ? data.options : [];
589
+
590
+ if (type === 'single_select') {
591
+ for (const opt of options) {
592
+ const btn = document.createElement('button');
593
+ btn.type = 'button';
594
+ btn.className = 'question-widget-option';
595
+ btn.textContent = opt;
596
+ btn.addEventListener('click', () => submit(opt, opt));
597
+ body.appendChild(btn);
598
+ }
599
+ body.appendChild(buildOtherRow('Other…'));
600
+ } else if (type === 'multi_select') {
601
+ const checkboxes = [];
602
+ for (const opt of options) {
603
+ const label = document.createElement('label');
604
+ label.className = 'question-widget-checkbox-label';
605
+ const cb = document.createElement('input');
606
+ cb.type = 'checkbox';
607
+ cb.value = opt;
608
+ checkboxes.push(cb);
609
+ label.appendChild(cb);
610
+ label.appendChild(document.createTextNode(opt));
611
+ body.appendChild(label);
612
+ }
613
+ const otherInput = document.createElement('input');
614
+ otherInput.type = 'text';
615
+ otherInput.placeholder = 'Other (optional)…';
616
+ otherInput.className = 'question-widget-other-inline';
617
+ body.appendChild(otherInput);
618
+
619
+ const submitBtn = document.createElement('button');
620
+ submitBtn.type = 'button';
621
+ submitBtn.className = 'question-widget-submit';
622
+ submitBtn.textContent = 'Submit';
623
+ submitBtn.addEventListener('click', () => {
624
+ const selected = checkboxes.filter((c) => c.checked).map((c) => c.value);
625
+ const other = otherInput.value.trim();
626
+ if (other) selected.push(other);
627
+ if (!selected.length) return;
628
+ const display = selected.join(', ');
629
+ submit(display, display);
630
+ });
631
+ body.appendChild(submitBtn);
632
+ } else {
633
+ // 'text', or an unrecognized type — a free-text prompt never silently drops the question.
634
+ body.appendChild(buildOtherRow('Type your answer…'));
635
+ }
636
+
637
+ return card;
638
+ }
639
+
405
640
  function appendAssistantRow(turnMeta) {
406
641
  const result = appendMessageRow('assistant', lastProfile?.ceoName || 'your AI CEO', getCeoInitial(), '');
407
642
  if (turnMeta?.model) {
@@ -411,6 +646,7 @@ function appendAssistantRow(turnMeta) {
411
646
  badge.textContent = turnMeta.effort ? `${modelLabel} · ${turnMeta.effort}` : modelLabel;
412
647
  result.row.querySelector('.msg-meta').appendChild(badge);
413
648
  }
649
+ result.renderer = new AssistantContentRenderer(result.textEl);
414
650
  return result;
415
651
  }
416
652
 
@@ -437,8 +673,7 @@ async function streamChat(message, { showUserBubble }) {
437
673
  showTypingIndicator();
438
674
  setStatus('typing');
439
675
 
440
- let assistantTextEl = null;
441
- let assistantText = '';
676
+ let assistantRenderer = null;
442
677
  let sawContent = false;
443
678
  let turnMeta = null;
444
679
 
@@ -479,11 +714,10 @@ async function streamChat(message, { showUserBubble }) {
479
714
  hideTypingIndicator();
480
715
  sawContent = true;
481
716
  }
482
- if (!assistantTextEl) {
483
- assistantTextEl = appendAssistantRow(turnMeta).textEl;
717
+ if (!assistantRenderer) {
718
+ assistantRenderer = appendAssistantRow(turnMeta).renderer;
484
719
  }
485
- assistantText += event.text;
486
- assistantTextEl.innerHTML = renderMarkdown(assistantText);
720
+ assistantRenderer.push(event.text);
487
721
  el.messages.scrollTop = el.messages.scrollHeight;
488
722
  } else if (event.type === 'tool-use') {
489
723
  if (!sawContent) {
@@ -502,6 +736,7 @@ async function streamChat(message, { showUserBubble }) {
502
736
  }
503
737
  }
504
738
 
739
+ assistantRenderer?.finish();
505
740
  hideTypingIndicator();
506
741
  if (statusState !== 'offline') setStatus('online');
507
742
  await refreshProfileIfNeeded();
@@ -517,11 +752,18 @@ el.form.addEventListener('submit', async (e) => {
517
752
  if (!message) return;
518
753
  el.input.value = '';
519
754
  el.input.disabled = true;
755
+ el.sendButton.disabled = true;
520
756
  try {
521
757
  await streamChat(message, { showUserBubble: true });
522
758
  } finally {
523
- el.input.disabled = false;
524
- el.input.focus();
759
+ // A widget the model asked as part of this same turn may still be
760
+ // unanswered — setPendingWidget already owns disabling in that case,
761
+ // so only re-enable here if nothing is pending.
762
+ if (pendingWidgetCount === 0) {
763
+ el.input.disabled = false;
764
+ el.sendButton.disabled = false;
765
+ el.input.focus();
766
+ }
525
767
  }
526
768
  });
527
769
 
@@ -134,7 +134,7 @@
134
134
  </div>
135
135
  <form id="chat-form">
136
136
  <input id="chat-input" type="text" placeholder="Message your CEO&hellip;" autocomplete="off" />
137
- <button type="submit">Send</button>
137
+ <button type="submit" id="chat-send">Send</button>
138
138
  </form>
139
139
  </section>
140
140
  </main>
@@ -502,6 +502,131 @@ body {
502
502
  color: #e5484d;
503
503
  }
504
504
 
505
+ .msg-text-segment:empty {
506
+ display: none;
507
+ }
508
+
509
+ .question-widget-placeholder {
510
+ display: inline-flex;
511
+ align-items: center;
512
+ gap: 0.4rem;
513
+ color: var(--muted);
514
+ font-style: italic;
515
+ font-size: 0.88rem;
516
+ padding: 0.3rem 0;
517
+ }
518
+
519
+ .question-widget {
520
+ border: 1px solid var(--border);
521
+ border-radius: 10px;
522
+ padding: 0.9rem 1rem;
523
+ margin: 0.5rem 0;
524
+ max-width: 420px;
525
+ background: var(--bg-subtle);
526
+ }
527
+
528
+ .question-widget-question {
529
+ font-weight: 600;
530
+ margin-bottom: 0.7rem;
531
+ line-height: 1.4;
532
+ }
533
+
534
+ .question-widget-body {
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: 0.45rem;
538
+ }
539
+
540
+ .question-widget-option {
541
+ background: var(--bg);
542
+ color: var(--fg);
543
+ border: 1px solid var(--border);
544
+ text-align: left;
545
+ font-weight: 500;
546
+ padding: 0.55rem 0.8rem;
547
+ }
548
+
549
+ .question-widget-option:hover {
550
+ border-color: var(--accent);
551
+ background: color-mix(in srgb, var(--accent) 8%, var(--bg));
552
+ }
553
+
554
+ .question-widget-checkbox-label {
555
+ display: flex;
556
+ align-items: center;
557
+ gap: 0.5rem;
558
+ font-size: 0.92rem;
559
+ cursor: pointer;
560
+ }
561
+
562
+ .question-widget-checkbox-label input {
563
+ width: 16px;
564
+ height: 16px;
565
+ accent-color: var(--accent);
566
+ }
567
+
568
+ .question-widget-submit {
569
+ align-self: flex-start;
570
+ }
571
+
572
+ .question-widget-row {
573
+ display: flex;
574
+ gap: 0.5rem;
575
+ }
576
+
577
+ .question-widget-row input {
578
+ flex: 1;
579
+ padding: 0.5rem 0.7rem;
580
+ border-radius: 7px;
581
+ border: 1px solid var(--border);
582
+ background: var(--bg);
583
+ color: var(--fg);
584
+ font-size: 0.9rem;
585
+ }
586
+
587
+ .question-widget-row input:focus {
588
+ outline: none;
589
+ border-color: var(--accent);
590
+ }
591
+
592
+ .question-widget-row button {
593
+ flex-shrink: 0;
594
+ padding: 0.5rem 0.9rem;
595
+ font-size: 0.85rem;
596
+ }
597
+
598
+ .question-widget-other {
599
+ margin-top: 0.15rem;
600
+ }
601
+
602
+ .question-widget-other-inline {
603
+ padding: 0.5rem 0.7rem;
604
+ border-radius: 7px;
605
+ border: 1px solid var(--border);
606
+ background: var(--bg);
607
+ color: var(--fg);
608
+ font-size: 0.9rem;
609
+ }
610
+
611
+ .question-widget-other-inline:focus {
612
+ outline: none;
613
+ border-color: var(--accent);
614
+ }
615
+
616
+ .question-widget.answered {
617
+ background: transparent;
618
+ }
619
+
620
+ .question-widget-answer {
621
+ color: var(--accent);
622
+ font-weight: 600;
623
+ font-size: 0.92rem;
624
+ }
625
+
626
+ .question-widget-answer::before {
627
+ content: '✓ ';
628
+ }
629
+
505
630
  #typing-indicator {
506
631
  display: flex;
507
632
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborgroup/my-team",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Chat with your local Claude Code subscription from a web dashboard, scoped to the current repo. Dev-only.",
5
5
  "type": "module",
6
6
  "bin": {