@aion0/forge 0.5.32 → 0.5.34

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/CLAUDE.md CHANGED
@@ -60,6 +60,8 @@ When adding or changing a feature, check if `lib/help-docs/` needs updating. Eac
60
60
  - `08-rules.md` — CLAUDE.md templates
61
61
  - `09-issue-autofix.md` — GitHub issue scanner
62
62
  - `10-troubleshooting.md` — common issues
63
+ - `11-workspace.md` — multi-agent workspace (smiths, daemon, request docs)
64
+ - `12-usage.md` — token usage analytics and cost tracking
63
65
  If a feature change affects user-facing behavior, update the corresponding help doc in the same commit.
64
66
 
65
67
  ### Architecture
package/RELEASE_NOTES.md CHANGED
@@ -1,8 +1,8 @@
1
- # Forge v0.5.32
1
+ # Forge v0.5.34
2
2
 
3
3
  Released: 2026-04-10
4
4
 
5
- ## Changes since v0.5.31
5
+ ## Changes since v0.5.33
6
6
 
7
7
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.31...v0.5.32
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.33...v0.5.34
@@ -2725,7 +2725,7 @@ const MASCOT_STYLES = `
2725
2725
  }
2726
2726
  `;
2727
2727
  type MascotPose = 'idle' | 'work' | 'done' | 'fail' | 'sleep' | 'wake';
2728
- export type MascotTheme = 'off' | 'stick' | 'cat' | 'dog' | 'pig' | 'emoji';
2728
+ export type MascotTheme = 'off' | 'stick' | 'cat' | 'pixel' | 'emoji';
2729
2729
 
2730
2730
  function StickCat({ pose, color, accentColor }: { pose: MascotPose; color: string; accentColor: string }) {
2731
2731
  const strokeProps = { stroke: color, strokeWidth: 1.5, strokeLinecap: 'round' as const, fill: 'none' };
@@ -2840,163 +2840,68 @@ function StickCat({ pose, color, accentColor }: { pose: MascotPose; color: strin
2840
2840
  );
2841
2841
  }
2842
2842
 
2843
- function StickDog({ pose, color, accentColor }: { pose: MascotPose; color: string; accentColor: string }) {
2844
- // Side-profile dog — elongated snout forward, triangular perked ear, visible tail
2845
- // Designed to read clearly at small sizes with distinct dog silhouette
2846
2843
 
2847
- if (pose === 'sleep') {
2848
- return (
2849
- <svg width="40" height="40" viewBox="0 0 40 40">
2850
- {/* body lying down */}
2851
- <ellipse cx="22" cy="32" rx="12" ry="4" stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.15" />
2852
- {/* head resting on paws — side profile */}
2853
- <path d="M 10 32 Q 6 30 4 32 Q 2 33 3 35 L 10 35 Z" stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.2" strokeLinejoin="round" />
2854
- {/* long snout */}
2855
- <path d="M 3 34 L 1 35 L 3 36" stroke={color} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
2856
- {/* floppy ear */}
2857
- <path d="M 8 30 Q 6 33 9 34" stroke={color} strokeWidth="2" fill={color} fillOpacity="0.35" strokeLinecap="round" />
2858
- {/* closed eye */}
2859
- <path d="M 6 33 Q 7 32.5 8 33" stroke={color} strokeWidth="0.8" fill="none" strokeLinecap="round" />
2860
- {/* nose */}
2861
- <ellipse cx="1.5" cy="35" rx="0.9" ry="0.7" fill={color} />
2862
- {/* curled tail */}
2863
- <path d="M 33 32 Q 38 30 36 26 Q 35 25 36 24" stroke={color} strokeWidth="2" fill="none" strokeLinecap="round" />
2864
- <text x="18" y="18" fill={accentColor} fontSize="7" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite' }}>z</text>
2865
- <text x="24" y="12" fill={accentColor} fontSize="5" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite 0.7s' }}>z</text>
2866
- </svg>
2867
- );
2868
- }
2869
-
2870
- if (pose === 'fail') {
2871
- return (
2872
- <svg width="40" height="40" viewBox="0 0 40 40">
2873
- {/* belly up */}
2874
- <ellipse cx="22" cy="32" rx="12" ry="4" stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.15" />
2875
- {/* head upside down */}
2876
- <path d="M 10 32 Q 6 34 4 32 Q 2 31 3 29 L 10 29 Z" stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.2" strokeLinejoin="round" />
2877
- <path d="M 3 30 L 1 29 L 3 28" stroke={color} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
2878
- {/* X eyes */}
2879
- <line x1="5" y1="30" x2="6.5" y2="31.5" stroke={accentColor} strokeWidth="1.2" strokeLinecap="round" />
2880
- <line x1="6.5" y1="30" x2="5" y2="31.5" stroke={accentColor} strokeWidth="1.2" strokeLinecap="round" />
2881
- {/* tongue hanging out sideways */}
2882
- <path d="M 2 30 Q 1 27 2 25" stroke="#ff6b9d" strokeWidth="1.5" fill="none" strokeLinecap="round" />
2883
- {/* all 4 legs sticking up */}
2884
- <line x1="14" y1="28" x2="13" y2="20" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2885
- <line x1="18" y1="28" x2="18" y2="18" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2886
- <line x1="26" y1="28" x2="26" y2="18" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2887
- <line x1="30" y1="28" x2="31" y2="20" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2888
- {/* limp tail */}
2889
- <path d="M 33 32 L 37 34" stroke={color} strokeWidth="2" fill="none" strokeLinecap="round" />
2890
- </svg>
2891
- );
2892
- }
2893
-
2894
- // Standing side-profile dog
2895
- const standingDog = (tailAnim: string, bounce: string = '') => (
2896
- <g style={bounce ? { transform: bounce } : {}}>
2897
- {/* body — side profile, clearly elongated horizontal */}
2898
- <path d="M 11 22 L 28 22 Q 32 22 32 26 L 32 30 Q 32 32 30 32 L 9 32 Q 7 32 7 30 L 7 27 Q 7 23 11 22 Z"
2899
- stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.15" strokeLinejoin="round" />
2900
- {/* head — side profile with long snout pointing LEFT */}
2901
- <path d="M 11 22 Q 8 22 6 20 Q 4 20 2 22 Q 0 23 1 25 Q 1 27 3 27 L 8 27 Q 10 27 11 25 Z"
2902
- stroke={color} strokeWidth="1.8" fill={color} fillOpacity="0.2" strokeLinejoin="round" />
2903
- {/* triangular perked ear (pointing up-back) */}
2904
- <path d="M 8 22 L 10 15 L 12 20 Z" stroke={color} strokeWidth="1.5" fill={color} fillOpacity="0.4" strokeLinejoin="round" />
2905
- {/* big black nose at tip */}
2906
- <ellipse cx="1" cy="24" rx="1.3" ry="1" fill={color} />
2907
- {/* eye */}
2908
- <circle cx="7" cy="23" r="1" fill={accentColor} />
2909
- <circle cx="6.7" cy="22.7" r="0.3" fill="#fff" />
2910
- {/* mouth line */}
2911
- <path d="M 1 25.5 Q 3 27 6 26" stroke={color} strokeWidth="0.8" fill="none" strokeLinecap="round" />
2912
- {/* tongue hanging out */}
2913
- <path d="M 2.5 26.5 Q 3 28.5 2 29" stroke="#ff6b9d" strokeWidth="1.2" fill="#ff6b9d" strokeLinecap="round" />
2914
- {/* curled tail (pointing up-right) — wags */}
2915
- <g style={{ transformOrigin: '32px 26px', animation: tailAnim }}>
2916
- <path d="M 32 26 Q 37 24 36 19 Q 36 17 38 17" stroke={color} strokeWidth="2.2" fill="none" strokeLinecap="round" />
2844
+ function PixelPerson({ pose, color, accentColor }: { pose: MascotPose; color: string; accentColor: string }) {
2845
+ // Retro 8-bit pixel character (RPG hero style)
2846
+ const skin = '#f4c69d';
2847
+ const hair = color;
2848
+ const shirt = accentColor;
2849
+ const pants = '#3b5998';
2850
+ const shoes = '#1a1a1a';
2851
+ const eye = '#000';
2852
+
2853
+ // Standing character 32x40, pixel 2
2854
+ const body = (armAnim?: string, legAnim?: string) => (
2855
+ <g shapeRendering="crispEdges">
2856
+ {/* hair top */}
2857
+ <rect x="10" y="6" width="12" height="2" fill={hair} />
2858
+ <rect x="8" y="8" width="16" height="2" fill={hair} />
2859
+ {/* head */}
2860
+ <rect x="10" y="10" width="12" height="6" fill={skin} />
2861
+ {/* hair sides */}
2862
+ <rect x="8" y="10" width="2" height="4" fill={hair} />
2863
+ <rect x="22" y="10" width="2" height="4" fill={hair} />
2864
+ {/* eyes */}
2865
+ <rect x="12" y="12" width="2" height="2" fill={eye} />
2866
+ <rect x="18" y="12" width="2" height="2" fill={eye} />
2867
+ {/* mouth */}
2868
+ <rect x="14" y="15" width="4" height="1" fill={eye} />
2869
+ {/* neck */}
2870
+ <rect x="14" y="16" width="4" height="1" fill={skin} />
2871
+ {/* body/shirt */}
2872
+ <rect x="10" y="17" width="12" height="8" fill={shirt} />
2873
+ <rect x="12" y="19" width="8" height="1" fill="#fff" opacity="0.3" />
2874
+ {/* arms */}
2875
+ <g style={armAnim ? { animation: armAnim, transformOrigin: '16px 18px' } : {}}>
2876
+ <rect x="8" y="17" width="2" height="7" fill={shirt} />
2877
+ <rect x="22" y="17" width="2" height="7" fill={shirt} />
2878
+ <rect x="8" y="24" width="2" height="2" fill={skin} />
2879
+ <rect x="22" y="24" width="2" height="2" fill={skin} />
2880
+ </g>
2881
+ {/* pants */}
2882
+ <g style={legAnim ? { animation: legAnim, transformOrigin: '16px 28px' } : {}}>
2883
+ <rect x="11" y="25" width="4" height="8" fill={pants} />
2884
+ <rect x="17" y="25" width="4" height="8" fill={pants} />
2885
+ {/* shoes */}
2886
+ <rect x="10" y="33" width="5" height="2" fill={shoes} />
2887
+ <rect x="17" y="33" width="5" height="2" fill={shoes} />
2917
2888
  </g>
2918
- {/* 4 legs — visible in side profile (2 front + 2 back, front pair visible) */}
2919
- <line x1="10" y1="32" x2="10" y2="38" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2920
- <line x1="14" y1="32" x2="14" y2="38" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2921
- <line x1="26" y1="32" x2="26" y2="38" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2922
- <line x1="30" y1="32" x2="30" y2="38" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
2923
- {/* paws */}
2924
- <rect x="8.5" y="37.5" width="3" height="1.5" fill={color} rx="0.5" />
2925
- <rect x="12.5" y="37.5" width="3" height="1.5" fill={color} rx="0.5" />
2926
- <rect x="24.5" y="37.5" width="3" height="1.5" fill={color} rx="0.5" />
2927
- <rect x="28.5" y="37.5" width="3" height="1.5" fill={color} rx="0.5" />
2928
- {/* collar */}
2929
- <line x1="9" y1="27" x2="12" y2="27" stroke={accentColor} strokeWidth="1.5" strokeLinecap="round" />
2930
- <circle cx="10.5" cy="28" r="0.9" fill={accentColor} />
2931
2889
  </g>
2932
2890
  );
2933
2891
 
2934
- if (pose === 'done') {
2935
- return (
2936
- <svg width="40" height="40" viewBox="0 0 40 40">
2937
- {standingDog('stick-arm-wave 0.3s ease-in-out infinite', 'translateY(-3px)')}
2938
- <text x="2" y="10" fill="#ffd700" fontSize="7" style={{ animation: 'stick-spark-burst 1.2s ease-out forwards' }}>✦</text>
2939
- <text x="34" y="12" fill="#ffd700" fontSize="9" style={{ animation: 'stick-spark-burst 1.2s ease-out forwards 0.3s' }}>✦</text>
2940
- <text x="18" y="6" fill="#ffd700" fontSize="6" style={{ animation: 'stick-spark-burst 1.2s ease-out forwards 0.5s' }}>✦</text>
2941
- </svg>
2942
- );
2943
- }
2944
-
2945
- if (pose === 'work') {
2946
- return (
2947
- <svg width="40" height="40" viewBox="0 0 40 40">
2948
- {standingDog('stick-arm-wave 0.25s ease-in-out infinite')}
2949
- {/* bone in mouth */}
2950
- <g transform="translate(-6, 0)">
2951
- <circle cx="0" cy="25" r="1.5" fill={accentColor} stroke={color} strokeWidth="0.6" />
2952
- <rect x="0" y="24.2" width="5" height="1.6" fill={accentColor} stroke={color} strokeWidth="0.6" rx="0.4" />
2953
- <circle cx="5" cy="25" r="1.5" fill={accentColor} stroke={color} strokeWidth="0.6" />
2954
- </g>
2955
- </svg>
2956
- );
2957
- }
2958
-
2959
- if (pose === 'wake') {
2960
- return (
2961
- <svg width="40" height="40" viewBox="0 0 40 40">
2962
- {standingDog('stick-arm-wave 1.5s ease-in-out infinite')}
2963
- {/* yawn — open mouth replaces tongue */}
2964
- <ellipse cx="2" cy="26" rx="1.3" ry="1.8" fill={color} opacity="0.5" />
2965
- </svg>
2966
- );
2967
- }
2968
-
2969
- // idle — standing, happy tail wag
2970
- return (
2971
- <svg width="40" height="40" viewBox="0 0 40 40">
2972
- {standingDog('stick-arm-wave 0.6s ease-in-out infinite')}
2973
- </svg>
2974
- );
2975
- }
2976
-
2977
- function StickPig({ pose, color, accentColor }: { pose: MascotPose; color: string; accentColor: string }) {
2978
- const pink = '#ff9ecb';
2979
- const pinkFill = '#ff9ecb';
2980
-
2981
2892
  if (pose === 'sleep') {
2982
2893
  return (
2983
2894
  <svg width="32" height="40" viewBox="0 0 32 40">
2984
- <ellipse cx="16" cy="30" rx="12" ry="7" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.25" />
2985
- <circle cx="8" cy="28" r="4" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.3" />
2986
- {/* pig snout disc */}
2987
- <ellipse cx="4.5" cy="29" rx="2.5" ry="1.8" stroke={pink} strokeWidth="1.5" fill={pinkFill} fillOpacity="0.4" />
2988
- <circle cx="4" cy="29" r="0.5" fill={color} />
2989
- <circle cx="5" cy="29" r="0.5" fill={color} />
2990
- {/* pointy triangular ears */}
2991
- <path d="M 5 24 L 6 22 L 8 25 Z" fill={pinkFill} stroke={pink} strokeWidth="1" />
2992
- <path d="M 9 24 L 10 22 L 11 25 Z" fill={pinkFill} stroke={pink} strokeWidth="1" />
2993
- {/* closed eyes */}
2994
- <path d="M 7 27 Q 7.5 26.5 8 27" stroke={color} strokeWidth="0.8" fill="none" strokeLinecap="round" />
2995
- <path d="M 9 27 Q 9.5 26.5 10 27" stroke={color} strokeWidth="0.8" fill="none" strokeLinecap="round" />
2996
- {/* curly tail */}
2997
- <path d="M 28 28 Q 30 26 28 24 Q 26 24 28 22" stroke={pink} strokeWidth="2" fill="none" strokeLinecap="round" />
2998
- <text x="16" y="16" fill={accentColor} fontSize="7" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite' }}>z</text>
2999
- <text x="21" y="10" fill={accentColor} fontSize="5" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite 0.7s' }}>z</text>
2895
+ <g shapeRendering="crispEdges">
2896
+ {/* lying down horizontally */}
2897
+ <rect x="4" y="22" width="4" height="4" fill={hair} />
2898
+ <rect x="8" y="22" width="6" height="4" fill={skin} />
2899
+ <rect x="10" y="24" width="1" height="1" fill={eye} />
2900
+ <rect x="14" y="22" width="12" height="4" fill={shirt} />
2901
+ <rect x="26" y="22" width="4" height="4" fill={pants} />
2902
+ </g>
2903
+ <text x="18" y="16" fill={accentColor} fontSize="6" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite' }}>z</text>
2904
+ <text x="22" y="10" fill={accentColor} fontSize="4" fontWeight="bold" style={{ animation: 'stick-zzz 2s ease-out infinite 0.7s' }}>z</text>
3000
2905
  </svg>
3001
2906
  );
3002
2907
  }
@@ -3004,66 +2909,21 @@ function StickPig({ pose, color, accentColor }: { pose: MascotPose; color: strin
3004
2909
  if (pose === 'fail') {
3005
2910
  return (
3006
2911
  <svg width="32" height="40" viewBox="0 0 32 40">
3007
- <ellipse cx="18" cy="29" rx="11" ry="5" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.25" />
3008
- <circle cx="8" cy="27" r="4.5" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.3" />
3009
- <ellipse cx="7" cy="23" rx="2.5" ry="2" stroke={pink} strokeWidth="1.5" fill={pinkFill} fillOpacity="0.5" />
3010
- <circle cx="6.3" cy="23" r="0.5" fill={color} />
3011
- <circle cx="7.7" cy="23" r="0.5" fill={color} />
3012
- <path d="M 4 28 Q 0 30 2 34" fill={pinkFill} stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
3013
- <path d="M 12 28 Q 16 30 14 34" fill={pinkFill} stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
3014
- <line x1="6" y1="26" x2="7.5" y2="27.5" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
3015
- <line x1="7.5" y1="26" x2="6" y2="27.5" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
3016
- <line x1="9" y1="26" x2="10.5" y2="27.5" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
3017
- <line x1="10.5" y1="26" x2="9" y2="27.5" stroke={color} strokeWidth="1.2" strokeLinecap="round" />
3018
- <line x1="14" y1="25" x2="13" y2="17" stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
3019
- <line x1="18" y1="25" x2="18" y2="16" stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
3020
- <line x1="22" y1="25" x2="22" y2="17" stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
3021
- <line x1="26" y1="25" x2="27" y2="18" stroke={pink} strokeWidth="1.8" strokeLinecap="round" />
2912
+ {/* knocked out rotated */}
2913
+ <g transform="rotate(-80 16 28)">
2914
+ {body()}
2915
+ </g>
2916
+ <text x="18" y="14" fill={accentColor} fontSize="5">×_×</text>
3022
2917
  </svg>
3023
2918
  );
3024
2919
  }
3025
2920
 
3026
- const pigBody = (tailAnim: string, bounce: string = '') => (
3027
- <g style={bounce ? { transform: bounce } : {}}>
3028
- {/* round pig body */}
3029
- <ellipse cx="18" cy="27" rx="11" ry="6.5" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.25" />
3030
- {/* round head */}
3031
- <circle cx="9" cy="18" r="6" stroke={pink} strokeWidth="1.8" fill={pinkFill} fillOpacity="0.3" />
3032
- {/* pig snout — flat disc with nostrils */}
3033
- <ellipse cx="5" cy="20" rx="3" ry="2.2" stroke={pink} strokeWidth="1.5" fill={pinkFill} fillOpacity="0.5" />
3034
- <circle cx="4" cy="20" r="0.6" fill={color} />
3035
- <circle cx="6" cy="20" r="0.6" fill={color} />
3036
- {/* triangular pointed ears */}
3037
- <path d="M 6 13 L 7 10 L 9 14 Z" fill={pinkFill} stroke={pink} strokeWidth="1.2" strokeLinejoin="round" />
3038
- <path d="M 11 13 L 12 10 L 14 14 Z" fill={pinkFill} stroke={pink} strokeWidth="1.2" strokeLinejoin="round" />
3039
- {/* eyes */}
3040
- <circle cx="8" cy="17" r="1" fill={color} />
3041
- <circle cx="12" cy="17" r="1" fill={color} />
3042
- <circle cx="7.7" cy="16.7" r="0.3" fill="#fff" />
3043
- <circle cx="11.7" cy="16.7" r="0.3" fill="#fff" />
3044
- {/* smile */}
3045
- <path d="M 7 22 Q 9 23 11 22" stroke={color} strokeWidth="0.8" fill="none" strokeLinecap="round" />
3046
- {/* curly tail — wagging */}
3047
- <g style={{ transformOrigin: '29px 25px', animation: tailAnim }}>
3048
- <path d="M 29 25 Q 32 23 30 21 Q 28 21 30 19 Q 31 18 32 19" stroke={pink} strokeWidth="2" fill="none" strokeLinecap="round" />
3049
- </g>
3050
- {/* trotter legs */}
3051
- <line x1="12" y1="32" x2="12" y2="38" stroke={pink} strokeWidth="2" strokeLinecap="round" />
3052
- <line x1="16" y1="33" x2="16" y2="38" stroke={pink} strokeWidth="2" strokeLinecap="round" />
3053
- <line x1="20" y1="33" x2="20" y2="38" stroke={pink} strokeWidth="2" strokeLinecap="round" />
3054
- <line x1="24" y1="32" x2="24" y2="38" stroke={pink} strokeWidth="2" strokeLinecap="round" />
3055
- {/* hooves */}
3056
- <rect x="10.5" y="37.5" width="3" height="1.8" fill={color} rx="0.3" />
3057
- <rect x="14.5" y="37.5" width="3" height="1.8" fill={color} rx="0.3" />
3058
- <rect x="18.5" y="37.5" width="3" height="1.8" fill={color} rx="0.3" />
3059
- <rect x="22.5" y="37.5" width="3" height="1.8" fill={color} rx="0.3" />
3060
- </g>
3061
- );
3062
-
3063
2921
  if (pose === 'done') {
3064
2922
  return (
3065
2923
  <svg width="32" height="40" viewBox="0 0 32 40">
3066
- {pigBody('stick-arm-wave 0.4s ease-in-out infinite', 'translateY(-2px)')}
2924
+ <g style={{ transform: 'translateY(-3px)' }}>
2925
+ {body('stick-arm-wave 0.3s ease-in-out infinite')}
2926
+ </g>
3067
2927
  <text x="2" y="8" fill="#ffd700" fontSize="6" style={{ animation: 'stick-spark-burst 1.2s ease-out forwards' }}>✦</text>
3068
2928
  <text x="26" y="10" fill="#ffd700" fontSize="8" style={{ animation: 'stick-spark-burst 1.2s ease-out forwards 0.3s' }}>✦</text>
3069
2929
  </svg>
@@ -3073,7 +2933,7 @@ function StickPig({ pose, color, accentColor }: { pose: MascotPose; color: strin
3073
2933
  if (pose === 'work') {
3074
2934
  return (
3075
2935
  <svg width="32" height="40" viewBox="0 0 32 40">
3076
- {pigBody('stick-arm-wave 0.3s ease-in-out infinite')}
2936
+ {body('stick-arm-hammer 0.4s ease-in-out infinite')}
3077
2937
  </svg>
3078
2938
  );
3079
2939
  }
@@ -3081,19 +2941,20 @@ function StickPig({ pose, color, accentColor }: { pose: MascotPose; color: strin
3081
2941
  if (pose === 'wake') {
3082
2942
  return (
3083
2943
  <svg width="32" height="40" viewBox="0 0 32 40">
3084
- {pigBody('stick-arm-wave 1.5s ease-in-out infinite')}
3085
- <ellipse cx="9" cy="20" rx="1.3" ry="1.5" fill={color} opacity="0.4" />
2944
+ {body('stick-arm-wave 1.8s ease-in-out infinite')}
3086
2945
  </svg>
3087
2946
  );
3088
2947
  }
3089
2948
 
2949
+ // idle
3090
2950
  return (
3091
2951
  <svg width="32" height="40" viewBox="0 0 32 40">
3092
- {pigBody('stick-arm-wave 0.7s ease-in-out infinite')}
2952
+ {body('stick-arm-wave 2.2s ease-in-out infinite')}
3093
2953
  </svg>
3094
2954
  );
3095
2955
  }
3096
2956
 
2957
+
3097
2958
  function EmojiMascot({ pose, seed }: { pose: MascotPose; seed: number }) {
3098
2959
  const characters = ['🦊', '🐱', '🐼', '🦉', '🐸', '🦝', '🐙', '🦖', '🐰', '🦄', '🐺', '🧙‍♂️', '🧝‍♀️', '🦸‍♂️', '🥷', '🐲'];
3099
2960
  const character = characters[seed % characters.length];
@@ -3265,8 +3126,7 @@ function WorkerMascot({ taskStatus, smithStatus, seed, accentColor, theme }: { t
3265
3126
  let figure: React.ReactNode;
3266
3127
  if (theme === 'stick') figure = <StickFigure pose={pose} color={color} accentColor={accentColor} />;
3267
3128
  else if (theme === 'cat') figure = <StickCat pose={pose} color={color} accentColor={accentColor} />;
3268
- else if (theme === 'dog') figure = <StickDog pose={pose} color={color} accentColor={accentColor} />;
3269
- else if (theme === 'pig') figure = <StickPig pose={pose} color={color} accentColor={accentColor} />;
3129
+ else if (theme === 'pixel') figure = <PixelPerson pose={pose} color={color} accentColor={accentColor} />;
3270
3130
  else figure = <EmojiMascot pose={pose} seed={seed} />;
3271
3131
 
3272
3132
  return (
@@ -3466,7 +3326,11 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
3466
3326
  const [showBusPanel, setShowBusPanel] = useState(false);
3467
3327
  const [mascotTheme, setMascotTheme] = useState<MascotTheme>(() => {
3468
3328
  if (typeof window === 'undefined') return 'off';
3469
- return (localStorage.getItem('forge.mascotTheme') as MascotTheme) || 'off';
3329
+ const saved = localStorage.getItem('forge.mascotTheme');
3330
+ // Migrate legacy values
3331
+ if (saved === 'dog' || saved === 'lobster') return 'pixel';
3332
+ if (saved === 'pig') return 'pixel';
3333
+ return (saved as MascotTheme) || 'off';
3470
3334
  });
3471
3335
  const updateMascotTheme = (t: MascotTheme) => {
3472
3336
  setMascotTheme(t);
@@ -3874,8 +3738,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
3874
3738
  title="Mascot theme">
3875
3739
  <option value="stick">🏃 Stick</option>
3876
3740
  <option value="cat">🐱 Cat</option>
3877
- <option value="dog">🐶 Dog</option>
3878
- <option value="pig">🐷 Pig</option>
3741
+ <option value="pixel">👾 Pixel</option>
3879
3742
  <option value="emoji">🎭 Emoji</option>
3880
3743
  <option value="off">⊘ Off</option>
3881
3744
  </select>
@@ -2,6 +2,22 @@
2
2
 
3
3
  Forge is a self-hosted Vibe Coding platform for Claude Code. It provides a browser-based terminal, multi-agent workspace orchestration, AI task management, remote access, and mobile control via Telegram.
4
4
 
5
+ ## Main Features
6
+
7
+ | Feature | Description |
8
+ |---|---|
9
+ | **Browser Terminal** | xterm.js + tmux persistence, split panes, mouse on/off toggle, auto-reconnect on disconnect |
10
+ | **Projects** | File tree, Git operations, code search, per-project tabs, favorites |
11
+ | **Workspace (Smiths)** | Multi-agent orchestration with DAG dependencies, message bus, request documents |
12
+ | **Tasks** | Background AI task queue with hook-based completion notifications |
13
+ | **Pipelines** | YAML-driven DAG workflows with scheduling and routing |
14
+ | **Skills Marketplace** | Install/update Claude Code skills and slash commands per project |
15
+ | **Usage Analytics** | Token/cost tracking by project, model, source with charts and heatmaps |
16
+ | **Telegram Bot** | Mobile control — submit tasks, receive notifications |
17
+ | **Remote Access** | One-click Cloudflare tunnel for remote browsing |
18
+ | **GitHub Issue Auto-fix** | Scan issues, auto-fix, create PRs |
19
+ | **Memory (optional)** | Code graph + knowledge via `@aion0/temper` MCP server |
20
+
5
21
  ## Quick Start
6
22
 
7
23
  ```bash
@@ -40,6 +40,26 @@ Add project directories in Settings → **Project Roots** (e.g. `~/Projects`). F
40
40
 
41
41
  Click ★ next to a project to favorite it. Favorites appear at the top of the sidebar.
42
42
 
43
+ ## Sidebar & Navigation
44
+
45
+ - **Collapse sidebar** (▶/◀ button): Narrow strip shows opened tabs at top (marked with green dot) and all other projects as initials below. Hover to show close (×) button on open tabs.
46
+ - **Sidebar state is persisted** across browser refreshes via `localStorage`.
47
+ - **Open projects** are marked with a green dot in the expanded sidebar; hover to reveal a close button.
48
+ - **Close confirmation** prompts before closing a project tab to prevent accidental closes.
49
+ - Up to **20 project tabs** stay mounted simultaneously (LRU eviction beyond that) — switching between recent projects is instant, terminal state is preserved.
50
+
51
+ ## Tree Views — Collapse All
52
+
53
+ All hierarchical tree views have a `⇱` "Collapse all" button to quickly fold every folder:
54
+
55
+ | Location | Button position |
56
+ |---|---|
57
+ | Project **Code** tab file tree | Right of "Files" label |
58
+ | **Docs viewer** tree | Right of search box |
59
+ | **Skills** panel file tree | Top of file tree |
60
+ | **Workspace** sidebar | Header next to `+` |
61
+ | **Session** list | Next to Batch button |
62
+
43
63
  ## Terminal
44
64
 
45
65
  ### Opening a Terminal
@@ -39,6 +39,18 @@ rm ~/.forge/data/terminal-state.json
39
39
  # Restart server — tabs will be empty but tmux sessions survive
40
40
  ```
41
41
 
42
+ ### Terminal input is laggy
43
+ Usually caused by high system load. Check:
44
+ - System memory — if heavily swapping, kill some processes
45
+ - Clean up old tmux sessions: `tmux list-sessions` then `tmux kill-session -t <name>`
46
+ - Reduce polling: open tabs are limited to 20 by LRU eviction
47
+ - Workspace terminals auto-reconnect on disconnect; no need to manually reopen
48
+
49
+ ### "Connection error" in workspace terminal
50
+ The WebSocket dropped (system suspend, network blip). Forge auto-reconnects after 2s and re-attaches to the same tmux session. If it keeps happening:
51
+ - Check `~/.forge/data/forge.log` for terminal-standalone errors
52
+ - Restart: `forge server restart`
53
+
42
54
  ### gh CLI not authenticated (Issue Scanner)
43
55
  ```bash
44
56
  gh auth login
@@ -207,6 +207,24 @@ Agents use these MCP tools (via forge-mcp-server):
207
207
  | `mark_message_done` | Mark a processed message as done |
208
208
  | `check_outbox` | Check delivery status of sent messages |
209
209
 
210
+ ### Request vs Inbox — When to use which
211
+
212
+ Every preset smith's role prompt includes a decision rule for this:
213
+
214
+ **Use `create_request`** when:
215
+ - Delegating substantive work (implement feature, write tests, do review)
216
+ - Work has concrete deliverables and acceptance criteria
217
+ - Work should flow through a pipeline (engineer → qa → reviewer)
218
+ - The task needs to be tracked, claimed, and its status visible
219
+
220
+ **Use `send_message`** when:
221
+ - Asking a clarifying question
222
+ - Quick status update or coordination
223
+ - Reporting a bug back after review fails
224
+ - No concrete deliverable
225
+
226
+ **When unsure, prefer `create_request`** — a tracked artifact beats losing context in chat.
227
+
210
228
  ### Other
211
229
 
212
230
  | Tool | Description |
@@ -317,6 +335,31 @@ Click **⌨️** on any smith to open a terminal session:
317
335
  - Forge Skills available: `/forge-send`, `/forge-inbox`, `/forge-status`, `/forge-workspace-sync`
318
336
  - Session Picker: choose new session, continue existing, or browse all Claude sessions
319
337
  - Close terminal → smith returns to auto mode, pending messages resume
338
+ - **Auto-reconnect**: If the WebSocket drops (e.g. system suspend, network blip), the terminal automatically reconnects after 2s and re-attaches to the same tmux session — conversation context preserved
339
+ - **Mouse ON/OFF toggle** (🖱️ button in header): Toggle tmux mouse mode globally for all sessions
340
+ - **ON**: trackpad scroll, `Shift+drag` to select text
341
+ - **OFF**: drag to select text directly, `Ctrl+B [` to enter scroll mode
342
+ - Click to apply instantly (no restart needed)
343
+
344
+ ### Terminal Layout: Float vs Dock
345
+
346
+ The workspace toolbar has a layout switcher: `⧉ Float` or `▤ Dock`.
347
+
348
+ | Layout | Behavior |
349
+ |---|---|
350
+ | **Float** (default) | Each terminal is a draggable/resizable floating window positioned near its smith node |
351
+ | **Dock** | All open terminals arranged in a fixed grid at the bottom of the workspace |
352
+
353
+ Dock mode features:
354
+ - Grid columns selector (1/2/3/4) — auto-expands based on open terminal count
355
+ - 1 terminal with 4 columns → fills full width
356
+ - 2 → half-half, 3 → thirds, 4 → quarters, 5+ → wraps to second row
357
+ - Drag the top border to resize dock height
358
+ - Layout preference persisted to localStorage
359
+
360
+ ### Smith Node Positions
361
+
362
+ Drag smith nodes to reorganize the graph. Positions are **persisted to workspace state** and restored on reload. Auto-save debounces writes (500ms after drag stops).
320
363
 
321
364
  ## Watch (Autonomous Monitoring)
322
365
 
@@ -356,7 +399,7 @@ Agents can monitor file/git/command changes without message-driven triggers.
356
399
 
357
400
  Each smith can display an animated companion character next to its node.
358
401
 
359
- **Themes**: Stick figure, Cat, Dog, Pig, Emoji, Off (default)
402
+ **Themes**: Stick figure, Cat, Pixel (8-bit RPG hero), Emoji, Off (default)
360
403
 
361
404
  - Theme picker in workspace header
362
405
  - Animates based on smith state (idle/running/done/failed/sleeping)
@@ -368,7 +411,7 @@ Each smith can display an animated companion character next to its node.
368
411
  | Action | Description |
369
412
  |--------|-------------|
370
413
  | **Start Daemon** | Launch all smiths, begin consuming messages |
371
- | **Stop Daemon** | Stop all smiths, kill workers |
414
+ | **Stop Daemon** | Stop all smiths, kill workers. Preserves user's terminal conversation context (no `/clear` is sent). Tmux sessions attached to by a user are kept alive. |
372
415
  | **Run All** | Trigger all runnable agents once |
373
416
  | **Run** | Trigger specific agent |
374
417
  | **Pause/Resume** | Pause/resume message consumption for one agent |
@@ -0,0 +1,103 @@
1
+ # Usage Analytics
2
+
3
+ Forge tracks Claude API token usage and estimated costs across all your projects.
4
+
5
+ ## Access
6
+
7
+ Click **Usage** in the Dashboard top navigation.
8
+
9
+ ## Data Source
10
+
11
+ Forge scans these sources on-demand (via **Scan Now** button) or automatically on startup:
12
+
13
+ | Source | Location | What it tracks |
14
+ |---|---|---|
15
+ | `claude-code` | `~/.claude/projects/*/` | Interactive Claude Code sessions |
16
+ | `forge-task` | `~/.forge/data/tasks.db` | Background tasks submitted through Forge |
17
+ | `api-direct` | SDK/API calls logged by Forge | Direct API calls (rare) |
18
+
19
+ Stored in `~/.forge/data/usage.db` (SQLite). Each row records: session_id, source, project, model, day, input/output/cache tokens, cost_usd, message_count.
20
+
21
+ ## Time Range Filter
22
+
23
+ Buttons in the header: **7d / 30d / 90d / All** — filter all charts and tables.
24
+
25
+ ## Summary Cards
26
+
27
+ | Card | Shows |
28
+ |---|---|
29
+ | **Total Cost** | Sum of `cost_usd` in the selected range + trend (↑/↓ vs previous half) |
30
+ | **Daily Avg** | Total cost divided by days with activity |
31
+ | **Tokens** | Total tokens (input + output + cache read), broken down below |
32
+ | **Cache Hit** | `cacheRead / (input + cacheRead) × 100%` + cached tokens count |
33
+
34
+ ## Visualizations
35
+
36
+ ### Token Mix (stacked bar)
37
+ A single horizontal bar showing the proportion of:
38
+ - 🔵 Input tokens
39
+ - 🟢 Output tokens
40
+ - 🟣 Cache read tokens
41
+ - 🟠 Cache create tokens
42
+
43
+ Hover for tooltip.
44
+
45
+ ### Cost Trend (line chart)
46
+ Line chart of daily cost over the selected range. Y-axis auto-scales to max cost. X-axis labels shown for ~7 date points.
47
+
48
+ ### Activity Heatmap
49
+ GitHub-style 90-day grid: rows are weekdays (S/M/T/W/T/F/S), columns are weeks. Darker blue = higher cost. Hover a cell for exact date and cost.
50
+
51
+ ### Avg by Weekday
52
+ Bar chart showing the average daily cost per weekday. Weekends highlighted in orange, weekdays in blue.
53
+
54
+ ### By Model / By Source (donut charts)
55
+ Two side-by-side donut charts splitting total cost by model (Opus, Sonnet, Haiku) and by source (claude-code, forge-task, api-direct). Shows percentage + absolute cost per slice.
56
+
57
+ ### By Project (bar list)
58
+ Top 20 projects ranked by cost. Each row shows project name, relative bar, cost, and session count.
59
+
60
+ ### Model Details (table)
61
+ Per-model breakdown:
62
+ - Input / Output tokens
63
+ - Cost
64
+ - Message count
65
+ - Avg cost per message
66
+
67
+ ### Summary Stats (bottom 3 cards)
68
+ - **Avg per session** — total cost / session count
69
+ - **Avg per message** — total cost / message count
70
+ - **Sessions per day** — session count / days with activity
71
+
72
+ ## Cost Calculation
73
+
74
+ Estimates based on public Anthropic API pricing:
75
+
76
+ | Model | Input | Output |
77
+ |---|---|---|
78
+ | Claude Opus 4 | $15/M | $75/M |
79
+ | Claude Sonnet 4 | $3/M | $15/M |
80
+ | Claude Haiku 4 | ~$0.80/M | ~$4/M |
81
+
82
+ Cache reads are ~90% cheaper than regular inputs (~$0.30/M for Opus).
83
+
84
+ > Actual cost may differ if you're on Claude Max/Pro subscription (fixed monthly), or using Bedrock/Vertex with different pricing.
85
+
86
+ ## Actions
87
+
88
+ - **Scan Now** — Re-scans all JSONL session files and tasks.db, updates the database
89
+ - **Time range** — 7/30/90/All days
90
+
91
+ ## Troubleshooting
92
+
93
+ ### Usage shows $0
94
+ - No data yet — click **Scan Now**
95
+ - Check `~/.claude/projects/` has JSONL session files
96
+ - Check `~/.forge/data/tasks.db` exists and has rows
97
+ - Check `~/.forge/data/usage.db` has data: `sqlite3 ~/.forge/data/usage.db 'SELECT COUNT(*) FROM token_usage'`
98
+
99
+ ### Wrong project name
100
+ Usage scanner derives project name from directory path. Rename via scan refresh after moving projects.
101
+
102
+ ### Missing recent sessions
103
+ Sessions in progress aren't tracked until the file is flushed. Click **Scan Now** to force a refresh.
@@ -42,6 +42,7 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
42
42
  | `09-issue-autofix.md` | GitHub issue auto-fix pipeline |
43
43
  | `10-troubleshooting.md` | Common issues and solutions |
44
44
  | `11-workspace.md` | Workspace (Forge Smiths) — multi-agent orchestration, daemon, message bus, profiles |
45
+ | `12-usage.md` | Token usage analytics — charts, heatmap, cost estimation, by model/project/source |
45
46
 
46
47
  ## Matching questions to docs
47
48
 
@@ -60,3 +61,6 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
60
61
  - Watch/monitor/detect/file changes/autonomous → `11-workspace.md`
61
62
  - Agent profile/env/model/cliType → `01-settings.md` + `11-workspace.md`
62
63
  - Agent log/logs/history/clear logs → `11-workspace.md`
64
+ - Usage/cost/tokens/spending/billing/analytics → `12-usage.md`
65
+ - Terminal dock/float/mouse toggle/reconnect → `07-projects.md` + `11-workspace.md`
66
+ - Sidebar collapse/project tabs/favorites → `07-projects.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {