create-arete-workspace 0.2.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 (180) hide show
  1. package/README.md +77 -0
  2. package/bin/arete.js +156 -0
  3. package/bin/create.js +111 -0
  4. package/lib/install-openclaw.js +50 -0
  5. package/lib/scaffold.js +213 -0
  6. package/lib/setup-wizard.js +88 -0
  7. package/lib/updater.js +130 -0
  8. package/package.json +34 -0
  9. package/packages/gatsaeng-os/README.md +36 -0
  10. package/packages/gatsaeng-os/components.json +23 -0
  11. package/packages/gatsaeng-os/eslint.config.mjs +18 -0
  12. package/packages/gatsaeng-os/next.config.ts +7 -0
  13. package/packages/gatsaeng-os/package.json +59 -0
  14. package/packages/gatsaeng-os/postcss.config.mjs +7 -0
  15. package/packages/gatsaeng-os/public/file.svg +1 -0
  16. package/packages/gatsaeng-os/public/globe.svg +1 -0
  17. package/packages/gatsaeng-os/public/next.svg +1 -0
  18. package/packages/gatsaeng-os/public/vercel.svg +1 -0
  19. package/packages/gatsaeng-os/public/window.svg +1 -0
  20. package/packages/gatsaeng-os/python/api_server.py +248 -0
  21. package/packages/gatsaeng-os/python/briefing.py +145 -0
  22. package/packages/gatsaeng-os/python/config.py +55 -0
  23. package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
  24. package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
  25. package/packages/gatsaeng-os/python/proactive.py +158 -0
  26. package/packages/gatsaeng-os/python/requirements.txt +11 -0
  27. package/packages/gatsaeng-os/python/run.py +28 -0
  28. package/packages/gatsaeng-os/python/scoring.py +44 -0
  29. package/packages/gatsaeng-os/python/streak.py +70 -0
  30. package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
  31. package/packages/gatsaeng-os/python/timing_engine.py +117 -0
  32. package/packages/gatsaeng-os/python/vault_io.py +423 -0
  33. package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
  34. package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
  35. package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
  36. package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
  37. package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
  38. package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
  39. package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
  40. package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
  41. package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
  42. package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
  43. package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
  44. package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
  45. package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
  46. package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
  47. package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
  48. package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
  49. package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
  50. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
  51. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
  52. package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
  53. package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
  54. package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
  55. package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
  56. package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
  57. package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
  58. package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
  59. package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
  60. package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
  61. package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
  62. package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
  63. package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
  64. package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
  65. package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
  66. package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
  67. package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
  68. package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
  69. package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
  70. package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
  71. package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
  72. package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
  73. package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
  74. package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
  75. package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
  76. package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
  77. package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
  78. package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
  79. package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
  80. package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
  81. package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
  82. package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
  83. package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
  84. package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
  85. package/packages/gatsaeng-os/src/app/error.tsx +30 -0
  86. package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
  87. package/packages/gatsaeng-os/src/app/globals.css +208 -0
  88. package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
  89. package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
  90. package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
  91. package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
  92. package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
  93. package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
  94. package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
  95. package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
  96. package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
  97. package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
  98. package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
  99. package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
  100. package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
  101. package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
  102. package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
  103. package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
  104. package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
  105. package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
  106. package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
  107. package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
  108. package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
  109. package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
  110. package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
  111. package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
  112. package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
  113. package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
  114. package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
  115. package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
  116. package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
  117. package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
  118. package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
  119. package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
  120. package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
  121. package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
  122. package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
  123. package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
  124. package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
  125. package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
  126. package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
  127. package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
  128. package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
  129. package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
  130. package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
  131. package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
  132. package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
  133. package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
  134. package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
  135. package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
  136. package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
  137. package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
  138. package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
  139. package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
  140. package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
  141. package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
  142. package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
  143. package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
  144. package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
  145. package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
  146. package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
  147. package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
  148. package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
  149. package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
  150. package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
  151. package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
  152. package/packages/gatsaeng-os/src/lib/date.ts +7 -0
  153. package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
  154. package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
  155. package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
  156. package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
  157. package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
  158. package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
  159. package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
  160. package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
  161. package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
  162. package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
  163. package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
  164. package/packages/gatsaeng-os/src/middleware.ts +34 -0
  165. package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
  166. package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
  167. package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
  168. package/packages/gatsaeng-os/src/types/index.ts +320 -0
  169. package/packages/gatsaeng-os/tsconfig.json +34 -0
  170. package/templates/scripts/forge_qa.sh.tmpl +237 -0
  171. package/templates/scripts/forge_ship.sh.tmpl +183 -0
  172. package/templates/scripts/session_indexer.py.tmpl +420 -0
  173. package/templates/scripts/tracer.py.tmpl +266 -0
  174. package/templates/workspace/AGENTS.md.tmpl +190 -0
  175. package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
  176. package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
  177. package/templates/workspace/MEMORY.md.tmpl +35 -0
  178. package/templates/workspace/SOUL.md.tmpl +258 -0
  179. package/templates/workspace/TOOLS.md.tmpl +28 -0
  180. package/templates/workspace/USER.md.tmpl +43 -0
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # forge_ship.sh — Commit → Version bump → Changelog → Push → Draft PR
5
+ # Usage: forge_ship.sh --repo <path> --msg "<pr title>" [--patch|--minor|--major] [--dry-run]
6
+
7
+ REPO=""
8
+ MSG=""
9
+ BUMP="patch"
10
+ DRY_RUN=false
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case $1 in
14
+ --repo) REPO="$2"; shift 2 ;;
15
+ --msg) MSG="$2"; shift 2 ;;
16
+ --patch) BUMP="patch"; shift ;;
17
+ --minor) BUMP="minor"; shift ;;
18
+ --major) BUMP="major"; shift ;;
19
+ --dry-run) DRY_RUN=true; shift ;;
20
+ *) echo "Unknown option: $1"; exit 1 ;;
21
+ esac
22
+ done
23
+
24
+ if [[ -z "$REPO" ]]; then
25
+ echo "ERROR: --repo <path> is required"
26
+ exit 1
27
+ fi
28
+
29
+ if [[ ! -d "$REPO/.git" ]]; then
30
+ echo "ERROR: $REPO is not a git repository"
31
+ exit 1
32
+ fi
33
+
34
+ cd "$REPO"
35
+ BRANCH=$(git branch --show-current)
36
+ echo "=== forge_ship: $REPO (branch: $BRANCH) ==="
37
+
38
+ # ── Step 1: Auto-commit uncommitted changes ──
39
+ if [[ -n $(git status --porcelain) ]]; then
40
+ echo "[1/6] Uncommitted changes detected — auto-committing..."
41
+ if $DRY_RUN; then
42
+ echo " [dry-run] would: git add -A && git commit -m 'chore: pre-ship snapshot'"
43
+ else
44
+ git add -A
45
+ git commit -m "chore: pre-ship snapshot"
46
+ fi
47
+ else
48
+ echo "[1/6] Working tree clean — no auto-commit needed."
49
+ fi
50
+
51
+ # ── Step 2: Extract new commits since origin/main ──
52
+ echo "[2/6] Extracting new commits..."
53
+ COMMITS=$(git log --oneline origin/main..HEAD 2>/dev/null || git log --oneline -10)
54
+ COMMIT_COUNT=$(echo "$COMMITS" | grep -c . || echo 0)
55
+
56
+ if [[ "$COMMIT_COUNT" -eq 0 ]]; then
57
+ echo " No new commits since origin/main. Nothing to ship."
58
+ exit 0
59
+ fi
60
+
61
+ echo " Found $COMMIT_COUNT new commit(s):"
62
+ echo "$COMMITS" | sed 's/^/ /'
63
+
64
+ # ── Step 3: Semver bump (if VERSION file exists) ──
65
+ NEW_VERSION=""
66
+ if [[ -f VERSION ]]; then
67
+ CURRENT=$(cat VERSION | tr -d '[:space:]')
68
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
69
+ case $BUMP in
70
+ major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
71
+ minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
72
+ patch) PATCH=$((PATCH + 1)) ;;
73
+ esac
74
+ NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
75
+ echo "[3/6] VERSION bump: $CURRENT → $NEW_VERSION ($BUMP)"
76
+ if $DRY_RUN; then
77
+ echo " [dry-run] would write $NEW_VERSION to VERSION"
78
+ else
79
+ echo "$NEW_VERSION" > VERSION
80
+ fi
81
+ else
82
+ echo "[3/6] No VERSION file — skipping semver bump."
83
+ fi
84
+
85
+ # ── Step 4: Update CHANGELOG.md ──
86
+ TODAY=$(date +%Y-%m-%d)
87
+ CHANGELOG_ENTRY="## [$TODAY]"
88
+ if [[ -n "$NEW_VERSION" ]]; then
89
+ CHANGELOG_ENTRY="## [${NEW_VERSION}] — $TODAY"
90
+ fi
91
+
92
+ CHANGELOG_BODY=""
93
+ while IFS= read -r line; do
94
+ HASH="${line%% *}"
95
+ DESC="${line#* }"
96
+ CHANGELOG_BODY="${CHANGELOG_BODY}\n- ${DESC} (\`${HASH}\`)"
97
+ done <<< "$COMMITS"
98
+
99
+ if [[ -f CHANGELOG.md ]]; then
100
+ echo "[4/6] Updating CHANGELOG.md..."
101
+ if $DRY_RUN; then
102
+ echo " [dry-run] would prepend section: $CHANGELOG_ENTRY"
103
+ printf " [dry-run] entries:$CHANGELOG_BODY\n"
104
+ else
105
+ TMPFILE=$(mktemp)
106
+ {
107
+ echo "$CHANGELOG_ENTRY"
108
+ printf "%b\n" "$CHANGELOG_BODY"
109
+ echo ""
110
+ cat CHANGELOG.md
111
+ } > "$TMPFILE"
112
+ mv "$TMPFILE" CHANGELOG.md
113
+ fi
114
+ else
115
+ echo "[4/6] Creating CHANGELOG.md..."
116
+ if $DRY_RUN; then
117
+ echo " [dry-run] would create CHANGELOG.md with section: $CHANGELOG_ENTRY"
118
+ printf " [dry-run] entries:$CHANGELOG_BODY\n"
119
+ else
120
+ {
121
+ echo "# Changelog"
122
+ echo ""
123
+ echo "$CHANGELOG_ENTRY"
124
+ printf "%b\n" "$CHANGELOG_BODY"
125
+ } > CHANGELOG.md
126
+ fi
127
+ fi
128
+
129
+ # ── Step 5: Commit VERSION/CHANGELOG changes ──
130
+ if $DRY_RUN; then
131
+ echo "[5/6] [dry-run] would commit VERSION/CHANGELOG updates"
132
+ else
133
+ if [[ -n $(git status --porcelain) ]]; then
134
+ echo "[5/6] Committing VERSION/CHANGELOG updates..."
135
+ git add VERSION CHANGELOG.md 2>/dev/null || git add CHANGELOG.md
136
+ COMMIT_MSG="chore: bump"
137
+ [[ -n "$NEW_VERSION" ]] && COMMIT_MSG="chore: bump v${NEW_VERSION}"
138
+ git commit -m "$COMMIT_MSG"
139
+ else
140
+ echo "[5/6] No VERSION/CHANGELOG changes to commit."
141
+ fi
142
+ fi
143
+
144
+ # ── Step 6: Push + Draft PR ──
145
+ PR_URL=""
146
+ if $DRY_RUN; then
147
+ echo "[6/6] [dry-run] would: git push origin HEAD"
148
+ if command -v gh &>/dev/null; then
149
+ PR_TITLE="${MSG:-Ship $BRANCH}"
150
+ echo " [dry-run] would: gh pr create --title \"$PR_TITLE\" --fill --draft"
151
+ else
152
+ echo " [dry-run] gh CLI not found. Would push only."
153
+ fi
154
+ else
155
+ echo "[6/6] Pushing..."
156
+ git push origin HEAD
157
+
158
+ if command -v gh &>/dev/null; then
159
+ PR_TITLE="${MSG:-Ship $BRANCH}"
160
+ PR_URL=$(gh pr create --title "$PR_TITLE" --fill --draft 2>&1) || true
161
+ if echo "$PR_URL" | grep -q "https://"; then
162
+ PR_URL=$(echo "$PR_URL" | grep -o 'https://[^ ]*')
163
+ else
164
+ echo " PR creation failed or already exists: $PR_URL"
165
+ PR_URL=""
166
+ fi
167
+ else
168
+ echo " gh CLI not found. Push complete. Create PR manually."
169
+ fi
170
+ fi
171
+
172
+ # ── Summary ──
173
+ echo ""
174
+ echo "=============================="
175
+ echo " forge_ship complete"
176
+ echo "------------------------------"
177
+ echo " Repo: $REPO"
178
+ echo " Branch: $BRANCH"
179
+ echo " Commits: $COMMIT_COUNT"
180
+ [[ -n "$NEW_VERSION" ]] && echo " Version: $NEW_VERSION"
181
+ [[ -n "$PR_URL" ]] && echo " PR: $PR_URL"
182
+ $DRY_RUN && echo " Mode: DRY-RUN (no changes made)"
183
+ echo "=============================="
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Session Auto-Indexer v2 — ARETE Memory System
4
+ Session end → JSONL → memory/daily log auto-extraction
5
+
6
+ v2 changes:
7
+ - Domain agent support (configurable)
8
+ - Topic session (-topic-*.jsonl) inclusion
9
+ - Domain agents: result summary only (cron noise removed)
10
+ - FORGE git log summary
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ import re
17
+ import hashlib
18
+ import subprocess
19
+ from datetime import datetime, timezone, timedelta
20
+ from pathlib import Path
21
+
22
+ # ── Config ──────────────────────────────────────────────────────
23
+ AGENTS_DIR = Path("{{OPENCLAW_PATH}}/agents")
24
+ MEMORY_DIR = Path("{{WORKSPACE_PATH}}/memory")
25
+ STATE_FILE = MEMORY_DIR / ".session_indexer_state.json"
26
+ KST = timezone(timedelta(hours=9))
27
+
28
+ # Full conversation indexing (including user messages)
29
+ FULL_INDEX_AGENTS = ["main"]
30
+
31
+ # Domain agents: result summary only (last assistant messages)
32
+ DOMAIN_AGENTS = {{DOMAIN_AGENTS_CONFIG}}
33
+
34
+ # Include topic sessions
35
+ INCLUDE_TOPIC_SESSIONS = True
36
+
37
+ # Minimum user messages per session
38
+ MIN_USER_MESSAGES = 2
39
+
40
+ # FORGE git repository paths (add your project repos here)
41
+ FORGE_REPOS = []
42
+
43
+ # ── Utils ──────────────────────────────────────────────────────
44
+ def load_state():
45
+ if STATE_FILE.exists():
46
+ return json.loads(STATE_FILE.read_text())
47
+ return {"indexed": {}, "forge_last_commit": {}}
48
+
49
+ def save_state(state):
50
+ STATE_FILE.write_text(json.dumps(state, indent=2))
51
+
52
+ def file_hash(path: Path) -> str:
53
+ return hashlib.md5(str(path.stat().st_mtime).encode()).hexdigest()[:8]
54
+
55
+ def is_cron_session(first_user_text: str) -> bool:
56
+ cron_kw = [
57
+ "[cron:", "Read HEARTBEAT.md", "[HOOK-SESSION-IDLE]",
58
+ "Heartbeat prompt", "heartbeat poll",
59
+ ]
60
+ return any(kw in first_user_text for kw in cron_kw)
61
+
62
+ def extract_text_from_content(content) -> str:
63
+ if isinstance(content, list):
64
+ return " ".join(
65
+ c.get("text", "")
66
+ for c in content
67
+ if isinstance(c, dict) and c.get("type") == "text"
68
+ ).strip()
69
+ elif isinstance(content, str):
70
+ return content.strip()
71
+ return ""
72
+
73
+ def clean_metadata(text: str) -> str:
74
+ """Remove conversation info JSON blocks"""
75
+ text = re.sub(r'Conversation info \(untrusted metadata\):\s*```json.*?```', '', text, flags=re.DOTALL)
76
+ text = re.sub(r'Sender \(untrusted metadata\):\s*```json.*?```', '', text, flags=re.DOTALL)
77
+ return text.strip()
78
+
79
+ def guard_text(text: str):
80
+ """Basic text sanitization. Replace with inbound_guard if available."""
81
+ class Result:
82
+ def __init__(self, t):
83
+ self.redacted_text = t
84
+ self.risk_level = "low"
85
+ self.warnings = []
86
+ return Result(text)
87
+
88
+ # ── Full indexing (main) ──────────────────────────────────────────
89
+ def extract_session_full(jsonl_path: Path) -> dict | None:
90
+ """Extract user-assistant conversation core from JSONL (main agent)"""
91
+ messages = []
92
+ session_ts = None
93
+
94
+ try:
95
+ with open(jsonl_path) as f:
96
+ for line in f:
97
+ obj = json.loads(line.strip())
98
+ t = obj.get("type")
99
+
100
+ if t == "session":
101
+ session_ts = obj.get("timestamp")
102
+
103
+ elif t == "message":
104
+ msg = obj.get("message", {})
105
+ role = msg.get("role", "")
106
+ content = msg.get("content", "")
107
+ text = extract_text_from_content(content)
108
+
109
+ if not text:
110
+ continue
111
+ if role == "user" and (
112
+ "Read HEARTBEAT.md" in text or
113
+ "[cron:" in text or
114
+ "[HOOK-SESSION-IDLE]" in text or
115
+ len(text) < 5
116
+ ):
117
+ continue
118
+ if role == "assistant" and text in ("HEARTBEAT_OK", "NO_REPLY"):
119
+ continue
120
+
121
+ if role in ("user", "assistant"):
122
+ clean = clean_metadata(text)
123
+ if not clean:
124
+ continue
125
+ guarded = guard_text(clean[:800])
126
+ messages.append({
127
+ "role": role,
128
+ "text": guarded.redacted_text,
129
+ "risk": guarded.risk_level,
130
+ "warnings": guarded.warnings,
131
+ })
132
+
133
+ except Exception as e:
134
+ print(f" Warning: parse failed {jsonl_path.name}: {e}", file=sys.stderr)
135
+ return None
136
+
137
+ user_msgs = [m for m in messages if m["role"] == "user"]
138
+ if len(user_msgs) < MIN_USER_MESSAGES:
139
+ return None
140
+
141
+ return {
142
+ "session_id": jsonl_path.stem,
143
+ "timestamp": session_ts,
144
+ "messages": messages,
145
+ "mode": "full",
146
+ }
147
+
148
+ def summarize_session_full(session: dict, agent: str = "main") -> str:
149
+ ts = session.get("timestamp", "")
150
+ if ts:
151
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
152
+ time_str = dt.strftime("%H:%M")
153
+ else:
154
+ time_str = "??"
155
+
156
+ label = "Agent" if agent == "main" else agent
157
+ lines = [f"### Session {session['session_id'][:8]} ({time_str} KST)"]
158
+
159
+ for msg in session["messages"]:
160
+ prefix = "**User**" if msg["role"] == "user" else f"**{label}**"
161
+ text = msg["text"]
162
+ if msg["role"] == "assistant" and len(text) > 400:
163
+ text = text[:400] + "..."
164
+ lines.append(f"{prefix}: {text}")
165
+
166
+ lines.append("")
167
+ return "\n".join(lines)
168
+
169
+ # ── Domain agent summary indexing ────────────────────────────────
170
+ def extract_session_domain(jsonl_path: Path, tag: str) -> dict | None:
171
+ """Domain agents: extract result summaries only"""
172
+ session_ts = None
173
+ first_user_text = ""
174
+ all_assistant_texts = []
175
+
176
+ try:
177
+ with open(jsonl_path) as f:
178
+ for line in f:
179
+ obj = json.loads(line.strip())
180
+ t = obj.get("type")
181
+
182
+ if t == "session":
183
+ session_ts = obj.get("timestamp")
184
+
185
+ elif t == "message":
186
+ msg = obj.get("message", {})
187
+ role = msg.get("role", "")
188
+ text = extract_text_from_content(msg.get("content", ""))
189
+
190
+ if not text:
191
+ continue
192
+
193
+ if role == "user" and not first_user_text:
194
+ first_user_text = text
195
+
196
+ if role == "assistant" and text not in ("HEARTBEAT_OK", "NO_REPLY"):
197
+ if len(text) > 20:
198
+ all_assistant_texts.append(text)
199
+
200
+ except Exception as e:
201
+ print(f" Warning: parse failed {jsonl_path.name}: {e}", file=sys.stderr)
202
+ return None
203
+
204
+ if not all_assistant_texts:
205
+ return None
206
+
207
+ last_assistant_texts = all_assistant_texts[-3:]
208
+
209
+ return {
210
+ "session_id": jsonl_path.stem,
211
+ "timestamp": session_ts,
212
+ "is_cron": is_cron_session(first_user_text),
213
+ "first_user": first_user_text[:100],
214
+ "results": last_assistant_texts,
215
+ "tag": tag,
216
+ "mode": "domain",
217
+ }
218
+
219
+ def summarize_session_domain(session: dict) -> str:
220
+ ts = session.get("timestamp", "")
221
+ if ts:
222
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
223
+ time_str = dt.strftime("%H:%M")
224
+ else:
225
+ time_str = "??"
226
+
227
+ tag = session.get("tag", "Agent")
228
+ session_type = "cron" if session.get("is_cron") else "direct"
229
+ lines = [f"### {tag} Session {session['session_id'][:8]} ({time_str} KST, {session_type})"]
230
+
231
+ for result in session["results"]:
232
+ if len(result) > 300:
233
+ result = result[:300] + "..."
234
+ lines.append(result)
235
+
236
+ lines.append("")
237
+ return "\n".join(lines)
238
+
239
+ # ── FORGE git log summary ──────────────────────────────────────────
240
+ def get_forge_git_summary(state: dict) -> str | None:
241
+ """Extract new commits from FORGE work repos"""
242
+ summaries = []
243
+
244
+ for repo in FORGE_REPOS:
245
+ repo = Path(repo)
246
+ if not repo.exists():
247
+ continue
248
+
249
+ repo_key = str(repo)
250
+ last_commit = state.get("forge_last_commit", {}).get(repo_key, "")
251
+
252
+ try:
253
+ result = subprocess.run(
254
+ ["git", "-C", str(repo), "log", "--oneline", "-10",
255
+ "--format=%H|%s|%ai"],
256
+ capture_output=True, text=True, timeout=10
257
+ )
258
+ if result.returncode != 0:
259
+ continue
260
+
261
+ commits = []
262
+ for line in result.stdout.strip().split("\n"):
263
+ if not line:
264
+ continue
265
+ parts = line.split("|", 2)
266
+ if len(parts) < 3:
267
+ continue
268
+ sha, msg, date_str = parts
269
+ commits.append((sha[:8], msg, date_str))
270
+
271
+ if not commits:
272
+ continue
273
+
274
+ new_commits = []
275
+ for sha, msg, date_str in commits:
276
+ if sha == last_commit:
277
+ break
278
+ new_commits.append((sha, msg, date_str))
279
+
280
+ if new_commits:
281
+ repo_name = repo.name
282
+ lines = [f"**FORGE [{repo_name}]** {len(new_commits)} new commits:"]
283
+ for sha, msg, _ in new_commits[:5]:
284
+ lines.append(f" - `{sha}` {msg}")
285
+ summaries.append("\n".join(lines))
286
+ state.setdefault("forge_last_commit", {})[repo_key] = commits[0][0]
287
+
288
+ except Exception as e:
289
+ print(f" Warning: FORGE git log failed {repo}: {e}", file=sys.stderr)
290
+
291
+ if summaries:
292
+ return "\n\n".join(summaries)
293
+ return None
294
+
295
+ # ── Daily log ──────────────────────────────────────────────────────
296
+ def get_daily_log_path(date_str: str) -> Path:
297
+ MEMORY_DIR.mkdir(exist_ok=True)
298
+ return MEMORY_DIR / f"{date_str}.md"
299
+
300
+ def append_to_daily_log(date_str: str, content: str):
301
+ path = get_daily_log_path(date_str)
302
+ header = f"\n## Auto-Indexed Sessions ({date_str})\n"
303
+ if path.exists():
304
+ existing = path.read_text()
305
+ if "## Auto-Indexed Sessions" not in existing:
306
+ with open(path, "a") as f:
307
+ f.write(header + content)
308
+ else:
309
+ with open(path, "a") as f:
310
+ f.write(content)
311
+ else:
312
+ path.write_text(header + content)
313
+
314
+ # ── Session file collection ──────────────────────────────────────
315
+ def collect_session_files(agent: str) -> list[Path]:
316
+ sessions_dir = AGENTS_DIR / agent / "sessions"
317
+ if not sessions_dir.exists():
318
+ return []
319
+
320
+ files = []
321
+ for f in sessions_dir.glob("*.jsonl*"):
322
+ if "deleted" in f.name or "reset" in f.name:
323
+ continue
324
+ if not f.name.endswith(".jsonl"):
325
+ continue
326
+ is_topic = "topic" in f.name
327
+ if is_topic and not INCLUDE_TOPIC_SESSIONS:
328
+ continue
329
+ files.append(f)
330
+
331
+ return sorted(files)
332
+
333
+ # ── Main ──────────────────────────────────────────────────────
334
+ def main():
335
+ state = load_state()
336
+ indexed = state["indexed"]
337
+ today = datetime.now(KST).strftime("%Y-%m-%d")
338
+
339
+ new_count = 0
340
+ skipped = 0
341
+
342
+ # 1. Full indexing agents (main)
343
+ for agent in FULL_INDEX_AGENTS:
344
+ for jsonl_file in collect_session_files(agent):
345
+ fhash = file_hash(jsonl_file)
346
+ fkey = f"{agent}/{jsonl_file.name}"
347
+
348
+ if indexed.get(fkey) == fhash:
349
+ skipped += 1
350
+ continue
351
+
352
+ session = extract_session_full(jsonl_file)
353
+ if not session:
354
+ indexed[fkey] = fhash
355
+ continue
356
+
357
+ ts = session.get("timestamp", "")
358
+ date_str = today
359
+ if ts:
360
+ try:
361
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
362
+ date_str = dt.strftime("%Y-%m-%d")
363
+ except:
364
+ pass
365
+
366
+ summary = summarize_session_full(session, agent)
367
+ append_to_daily_log(date_str, summary)
368
+
369
+ indexed[fkey] = fhash
370
+ new_count += 1
371
+ print(f" Indexed: {agent}/{jsonl_file.stem[:12]} -> memory/{date_str}.md")
372
+
373
+ # 2. Domain agent summary indexing
374
+ for agent_name, config in DOMAIN_AGENTS.items():
375
+ if not isinstance(config, dict):
376
+ continue
377
+ tag = config.get("tag", agent_name)
378
+ for jsonl_file in collect_session_files(agent_name):
379
+ fhash = file_hash(jsonl_file)
380
+ fkey = f"{agent_name}/{jsonl_file.name}"
381
+
382
+ if indexed.get(fkey) == fhash:
383
+ skipped += 1
384
+ continue
385
+
386
+ session = extract_session_domain(jsonl_file, tag)
387
+ if not session:
388
+ indexed[fkey] = fhash
389
+ continue
390
+
391
+ ts = session.get("timestamp", "")
392
+ date_str = today
393
+ if ts:
394
+ try:
395
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
396
+ date_str = dt.strftime("%Y-%m-%d")
397
+ except:
398
+ pass
399
+
400
+ summary = summarize_session_domain(session)
401
+ append_to_daily_log(date_str, summary)
402
+
403
+ indexed[fkey] = fhash
404
+ new_count += 1
405
+ print(f" Indexed: {agent_name}/{jsonl_file.stem[:12]} -> memory/{date_str}.md")
406
+
407
+ # 3. FORGE git log summary
408
+ forge_summary = get_forge_git_summary(state)
409
+ if forge_summary:
410
+ forge_block = f"\n### FORGE Work Summary\n{forge_summary}\n"
411
+ append_to_daily_log(today, forge_block)
412
+ new_count += 1
413
+ print(f" Indexed: FORGE git summary -> memory/{today}.md")
414
+
415
+ save_state({"indexed": indexed, "forge_last_commit": state.get("forge_last_commit", {})})
416
+
417
+ print(f"\nDone: {new_count} new indexed, {skipped} skipped")
418
+
419
+ if __name__ == "__main__":
420
+ main()