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.
- package/LICENSE +21 -0
- package/console/assets/addon-fit-BlxrFPDK.js +1 -0
- package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
- package/console/assets/index-Digctq9W.css +32 -0
- package/console/assets/index-ItVUTuQf.js +31 -0
- package/console/assets/xterm-DlVFs1Kw.js +9 -0
- package/console/index.html +13 -0
- package/cw/.bundle.json +4 -0
- package/cw/agents/presentation-curator.md +119 -0
- package/cw/agents/weather-agent.md +67 -0
- package/cw/cw +6188 -0
- package/cw/cw-shell-integration.sh +132 -0
- package/cw/hooks/HOOKS-README.md +471 -0
- package/cw/hooks/config/hooks-config.json +24 -0
- package/cw/hooks/scripts/auto-compact.py +133 -0
- package/cw/hooks/scripts/hooks.py +472 -0
- package/cw/hooks/scripts/review-autoclose.py +171 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
- package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
- package/cw/hooks/sounds/configchange/configchange.wav +0 -0
- package/cw/hooks/sounds/notification/notification.mp3 +0 -0
- package/cw/hooks/sounds/notification/notification.wav +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
- package/cw/hooks/sounds/precompact/precompact.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
- package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
- package/cw/hooks/sounds/setup/Setup.wav +0 -0
- package/cw/hooks/sounds/stop/stop.mp3 +0 -0
- package/cw/hooks/sounds/stop/stop.wav +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
- package/cw/install.sh +243 -0
- package/cw/lib/context/github.sh +51 -0
- package/cw/lib/context/linear.sh +49 -0
- package/cw/lib/context/notion.sh +38 -0
- package/cw/lib/dashboard/activity-hook.py +134 -0
- package/cw/lib/dashboard/index.html +1515 -0
- package/cw/lib/dashboard/server.py +379 -0
- package/cw/lib/dashboard/start.sh +15 -0
- package/cw/lib/harnesses/claude.sh +92 -0
- package/cw/lib/harnesses/codex.sh +245 -0
- package/cw/lib/harnesses/opencode.sh +120 -0
- package/cw/lib/harnesses/pi.sh +66 -0
- package/cw/lib/iterm2.sh +140 -0
- package/cw/templates/CLAUDE.template.md +48 -0
- package/dist/index.js +4376 -0
- 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()
|