forge-cw 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/console/assets/addon-fit-BlxrFPDK.js +1 -0
  3. package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
  4. package/console/assets/index-Digctq9W.css +32 -0
  5. package/console/assets/index-ItVUTuQf.js +31 -0
  6. package/console/assets/xterm-DlVFs1Kw.js +9 -0
  7. package/console/index.html +13 -0
  8. package/cw/.bundle.json +4 -0
  9. package/cw/agents/presentation-curator.md +119 -0
  10. package/cw/agents/weather-agent.md +67 -0
  11. package/cw/cw +6188 -0
  12. package/cw/cw-shell-integration.sh +132 -0
  13. package/cw/hooks/HOOKS-README.md +471 -0
  14. package/cw/hooks/config/hooks-config.json +24 -0
  15. package/cw/hooks/scripts/auto-compact.py +133 -0
  16. package/cw/hooks/scripts/hooks.py +472 -0
  17. package/cw/hooks/scripts/review-autoclose.py +171 -0
  18. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
  19. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
  20. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
  21. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
  22. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
  23. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
  24. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
  25. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
  26. package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
  27. package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
  28. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
  29. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
  30. package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
  31. package/cw/hooks/sounds/configchange/configchange.wav +0 -0
  32. package/cw/hooks/sounds/notification/notification.mp3 +0 -0
  33. package/cw/hooks/sounds/notification/notification.wav +0 -0
  34. package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
  35. package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
  36. package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
  37. package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
  38. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
  39. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
  40. package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
  41. package/cw/hooks/sounds/precompact/precompact.wav +0 -0
  42. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
  43. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
  44. package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
  45. package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
  46. package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
  47. package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
  48. package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
  49. package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
  50. package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
  51. package/cw/hooks/sounds/setup/Setup.wav +0 -0
  52. package/cw/hooks/sounds/stop/stop.mp3 +0 -0
  53. package/cw/hooks/sounds/stop/stop.wav +0 -0
  54. package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
  55. package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
  56. package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
  57. package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
  58. package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
  59. package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
  60. package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
  61. package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
  62. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
  63. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
  64. package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
  65. package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
  66. package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
  67. package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
  68. package/cw/install.sh +243 -0
  69. package/cw/lib/context/github.sh +51 -0
  70. package/cw/lib/context/linear.sh +49 -0
  71. package/cw/lib/context/notion.sh +38 -0
  72. package/cw/lib/dashboard/activity-hook.py +134 -0
  73. package/cw/lib/dashboard/index.html +1515 -0
  74. package/cw/lib/dashboard/server.py +379 -0
  75. package/cw/lib/dashboard/start.sh +15 -0
  76. package/cw/lib/harnesses/claude.sh +92 -0
  77. package/cw/lib/harnesses/codex.sh +245 -0
  78. package/cw/lib/harnesses/opencode.sh +120 -0
  79. package/cw/lib/harnesses/pi.sh +66 -0
  80. package/cw/lib/iterm2.sh +140 -0
  81. package/cw/templates/CLAUDE.template.md +48 -0
  82. package/dist/index.js +4376 -0
  83. package/package.json +51 -0
@@ -0,0 +1,38 @@
1
+ # notion needs an internal integration token from the env or ~/.cw/tokens.env
2
+ context_credential_notion() {
3
+ [[ -n "${NOTION_TOKEN:-}" ]] && return 0
4
+ [[ -f "$CW_HOME/tokens.env" ]] && grep -q '^NOTION_TOKEN=' "$CW_HOME/tokens.env"
5
+ }
6
+
7
+ context_fetch_notion() {
8
+ local url="$1"
9
+ local page; page=$(printf '%s' "$url" | grep -oE '[a-f0-9]{32}' | head -1)
10
+ [[ -n "$page" ]] || return 1
11
+ local tok="${NOTION_TOKEN:-}"
12
+ [[ -z "$tok" && -f "$CW_HOME/tokens.env" ]] && \
13
+ tok=$(sed -n 's/^NOTION_TOKEN=//p' "$CW_HOME/tokens.env" | head -1)
14
+ [[ -n "$tok" ]] || return 1
15
+ local api="${CW_NOTION_API:-https://api.notion.com}"
16
+ CW_NOTION_TOKEN="$tok" CW_NOTION_PAGE="$page" CW_NOTION_URL="$api" python3 - <<'PY'
17
+ import json, os, sys, urllib.request
18
+ req = urllib.request.Request(
19
+ f"{os.environ['CW_NOTION_URL']}/v1/blocks/{os.environ['CW_NOTION_PAGE']}/children?page_size=100",
20
+ headers={"Authorization": f"Bearer {os.environ['CW_NOTION_TOKEN']}",
21
+ "Notion-Version": "2022-06-28"},
22
+ )
23
+ try:
24
+ with urllib.request.urlopen(req, timeout=15) as r:
25
+ d = json.load(r)
26
+ except Exception as e:
27
+ print(f"_Notion fetch failed: {e}_", file=sys.stderr)
28
+ sys.exit(1)
29
+ prefix = {"heading_1": "# ", "heading_2": "## ", "heading_3": "### ",
30
+ "bulleted_list_item": "- ", "numbered_list_item": "1. "}
31
+ for b in d.get("results", []):
32
+ t = b.get("type")
33
+ rich = (b.get(t) or {}).get("rich_text") or []
34
+ text = "".join(x.get("plain_text", "") for x in rich)
35
+ if text:
36
+ print(prefix.get(t, "") + text)
37
+ PY
38
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """CW Activity Feed Hook — emits lightweight events to ~/.cw/activity.jsonl.
3
+
4
+ This script is called by Claude Code hooks (PreToolUse, PostToolUse, etc.)
5
+ and writes a single JSON line per event for the retro dashboard to display.
6
+
7
+ CW injects env vars before launching Claude:
8
+ CW_PROJECT, CW_TASK, CW_TASK_TYPE, CW_ACCOUNT
9
+ """
10
+
11
+ import sys
12
+ import json
13
+ import os
14
+ import time
15
+ from pathlib import Path
16
+
17
+ CW_HOME = os.environ.get("CW_HOME", os.path.expanduser("~/.cw"))
18
+ ACTIVITY_FILE = os.path.join(CW_HOME, "activity.jsonl")
19
+ MAX_SIZE = 500_000 # ~500KB before rotation
20
+ MAX_LINES = 2000
21
+
22
+
23
+ def main():
24
+ try:
25
+ raw = sys.stdin.read().strip()
26
+ if not raw:
27
+ sys.exit(0)
28
+ data = json.loads(raw)
29
+ except Exception:
30
+ sys.exit(0)
31
+
32
+ event = data.get("hook_event_name", "")
33
+ session_id = data.get("session_id", "")
34
+ tool = data.get("tool_name", "")
35
+ cwd = data.get("cwd", "")
36
+
37
+ # CW context from environment (set by cw work/review before launching claude)
38
+ project = os.environ.get("CW_PROJECT", "")
39
+ task = os.environ.get("CW_TASK", "")
40
+ task_type = os.environ.get("CW_TASK_TYPE", "")
41
+ account = os.environ.get("CW_ACCOUNT", "")
42
+
43
+ # Fallback: infer from cwd if env vars not set
44
+ if not project and cwd:
45
+ if "/.tasks/" in cwd:
46
+ parts = cwd.split("/.tasks/")
47
+ project = os.path.basename(parts[0])
48
+ task = parts[1].split("/")[0] if len(parts) > 1 else ""
49
+ task_type = "task"
50
+ elif "/.reviews/" in cwd:
51
+ parts = cwd.split("/.reviews/")
52
+ project = os.path.basename(parts[0])
53
+ task = parts[1].split("/")[0] if len(parts) > 1 else ""
54
+ task_type = "review"
55
+ else:
56
+ project = os.path.basename(cwd)
57
+
58
+ # Build summary (keep it short)
59
+ summary = ""
60
+ ti = data.get("tool_input", {})
61
+ if isinstance(ti, str):
62
+ try:
63
+ ti = json.loads(ti)
64
+ except Exception:
65
+ ti = {}
66
+
67
+ if tool == "Bash":
68
+ cmd = ti.get("command", "")
69
+ summary = cmd[:100]
70
+ elif tool in ("Read", "Write"):
71
+ fp = ti.get("file_path", "")
72
+ summary = os.path.basename(fp) if fp else ""
73
+ elif tool == "Edit":
74
+ fp = ti.get("file_path", "")
75
+ summary = os.path.basename(fp) if fp else ""
76
+ elif tool in ("Glob",):
77
+ summary = ti.get("pattern", "")[:60]
78
+ elif tool in ("Grep",):
79
+ summary = ti.get("pattern", ti.get("query", ""))[:60]
80
+ elif tool == "Agent":
81
+ summary = ti.get("description", "")[:60]
82
+ elif tool == "WebFetch":
83
+ summary = ti.get("url", "")[:80]
84
+ elif tool == "WebSearch":
85
+ summary = ti.get("query", "")[:60]
86
+ elif event == "SubagentStart":
87
+ summary = data.get("agent_type", "")
88
+ elif event == "SubagentStop":
89
+ summary = data.get("agent_type", "")
90
+ elif event == "Stop":
91
+ summary = "response complete"
92
+ elif event == "SessionStart":
93
+ summary = data.get("source", "")
94
+ elif event == "SessionEnd":
95
+ summary = data.get("reason", "")
96
+ elif event == "TeammateIdle":
97
+ summary = f"teammate idle: {data.get('teammate_name', '')}"
98
+ elif event == "TaskCompleted":
99
+ summary = f"task done: {data.get('task_description', '')[:60]}"
100
+
101
+ entry = {
102
+ "ts": time.time(),
103
+ "event": event,
104
+ "session": session_id[:12] if session_id else "",
105
+ "tool": tool,
106
+ "project": project,
107
+ "task": task,
108
+ "type": task_type,
109
+ "account": account,
110
+ "summary": summary,
111
+ }
112
+
113
+ try:
114
+ os.makedirs(os.path.dirname(ACTIVITY_FILE), exist_ok=True)
115
+ with open(ACTIVITY_FILE, "a") as f:
116
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
117
+
118
+ # Rotate if too large
119
+ try:
120
+ if os.path.getsize(ACTIVITY_FILE) > MAX_SIZE:
121
+ with open(ACTIVITY_FILE) as f:
122
+ lines = f.readlines()
123
+ with open(ACTIVITY_FILE, "w") as f:
124
+ f.writelines(lines[-MAX_LINES:])
125
+ except Exception:
126
+ pass
127
+ except Exception:
128
+ pass
129
+
130
+ sys.exit(0)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()