memento-mcp 0.3.2 → 0.3.3
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 +8 -8
- package/docs/self-hosting.md +1 -1
- package/package.json +1 -1
- package/scripts/memento-sessionstart-identity.sh +38 -4
- package/scripts/memento-stop-recall.sh +38 -13
- package/scripts/memento-userprompt-recall.sh +38 -14
- package/src/cli.js +350 -94
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ not just at session boundaries.
|
|
|
150
150
|
|
|
151
151
|
Installing Memento gives your agent memory. *The Protocol* is the system you build around it — orientation after context loss, automatic recall, writing discipline, distillation before context resets, identity that persists across sessions.
|
|
152
152
|
|
|
153
|
-
Full guide: **[The Protocol](https://hifathom.com/
|
|
153
|
+
Full guide: **[The Protocol](https://hifathom.com/memento/docs/protocol)** on hifathom.com.
|
|
154
154
|
|
|
155
155
|
## Hooks
|
|
156
156
|
|
|
@@ -168,19 +168,19 @@ See **[scripts/README.md](scripts/README.md)** for setup, configuration, and how
|
|
|
168
168
|
|
|
169
169
|
## Dashboard
|
|
170
170
|
|
|
171
|
-
Browse and manage memories visually at [hifathom.com/dashboard](https://hifathom.com/dashboard). Paste your API key and workspace name to connect.
|
|
171
|
+
Browse and manage memories visually at [hifathom.com/memento/dashboard](https://hifathom.com/memento/dashboard). Paste your API key and workspace name to connect.
|
|
172
172
|
|
|
173
173
|
---
|
|
174
174
|
|
|
175
175
|
## Documentation
|
|
176
176
|
|
|
177
|
-
Full reference docs at [hifathom.com/
|
|
177
|
+
Full reference docs at [hifathom.com/memento](https://hifathom.com/memento):
|
|
178
178
|
|
|
179
|
-
- **[Quick Start](https://hifathom.com/
|
|
180
|
-
- **[The Protocol](https://hifathom.com/
|
|
181
|
-
- **[Core Concepts](https://hifathom.com/
|
|
182
|
-
- **[MCP Tools](https://hifathom.com/
|
|
183
|
-
- **[API Reference](https://hifathom.com/
|
|
179
|
+
- **[Quick Start](https://hifathom.com/memento/docs/quick-start)** — 5-minute setup guide
|
|
180
|
+
- **[The Protocol](https://hifathom.com/memento/docs/protocol)** — orientation, recall hooks, writing discipline, distillation, identity
|
|
181
|
+
- **[Core Concepts](https://hifathom.com/memento/docs/concepts)** — memories, working memory, skip lists, identity crystals
|
|
182
|
+
- **[MCP Tools](https://hifathom.com/memento/docs/mcp-tools)** — full tool reference with parameters and examples
|
|
183
|
+
- **[API Reference](https://hifathom.com/memento/docs/api)** — REST endpoints, request/response schemas, authentication
|
|
184
184
|
- **[Self-Hosting](docs/self-hosting.md)** — deploy your own instance with Cloudflare Workers + Turso
|
|
185
185
|
|
|
186
186
|
---
|
package/docs/self-hosting.md
CHANGED
|
@@ -82,5 +82,5 @@ Workers AI and Vectorize are included in the Cloudflare Workers free tier.
|
|
|
82
82
|
|
|
83
83
|
- You manage your own Turso databases and Cloudflare account
|
|
84
84
|
- You handle upgrades by pulling from the repo and redeploying
|
|
85
|
-
- No usage dashboard at hifathom.com (you'd use Cloudflare's dashboard)
|
|
85
|
+
- No usage dashboard at hifathom.com/memento/dashboard (you'd use Cloudflare's dashboard)
|
|
86
86
|
- Signup endpoint creates keys in your control plane database
|
package/package.json
CHANGED
|
@@ -99,9 +99,33 @@ PID3=$!
|
|
|
99
99
|
|
|
100
100
|
wait $PID1 $PID2 $PID3 2>/dev/null
|
|
101
101
|
|
|
102
|
-
#
|
|
102
|
+
# Version check (non-blocking, best-effort)
|
|
103
|
+
# Read installed version from .memento/version (written by init/update)
|
|
104
|
+
VERSION_FILE=""
|
|
105
|
+
_vd="$(pwd)"
|
|
106
|
+
while true; do
|
|
107
|
+
if [ -f "$_vd/.memento/version" ]; then
|
|
108
|
+
VERSION_FILE="$_vd/.memento/version"
|
|
109
|
+
break
|
|
110
|
+
fi
|
|
111
|
+
_vp="$(dirname "$_vd")"
|
|
112
|
+
[ "$_vp" = "$_vd" ] && break
|
|
113
|
+
_vd="$_vp"
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
export LOCAL_VERSION=""
|
|
117
|
+
export LATEST_VERSION=""
|
|
118
|
+
if [ -n "$VERSION_FILE" ]; then
|
|
119
|
+
LOCAL_VERSION=$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
120
|
+
if [ -n "$LOCAL_VERSION" ]; then
|
|
121
|
+
LATEST_VERSION=$(curl -s --max-time 2 "https://registry.npmjs.org/memento-mcp/latest" 2>/dev/null \
|
|
122
|
+
| python3 -c "import json,sys; print(json.load(sys.stdin).get('version',''))" 2>/dev/null || echo "")
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Build output from the three responses + version check
|
|
103
127
|
python3 -c "
|
|
104
|
-
import json, sys
|
|
128
|
+
import json, sys, os
|
|
105
129
|
|
|
106
130
|
sections = []
|
|
107
131
|
|
|
@@ -155,6 +179,12 @@ if not sections:
|
|
|
155
179
|
context = '\n\n'.join(sections)
|
|
156
180
|
context += '\n\nREMINDER: If Memento MCP tools are not loaded, run: ToolSearch query=\"+memento\" max_results=20'
|
|
157
181
|
|
|
182
|
+
# 4. Version check — append update notice if newer version available
|
|
183
|
+
local_ver = os.environ.get('LOCAL_VERSION', '').strip()
|
|
184
|
+
latest_ver = os.environ.get('LATEST_VERSION', '').strip()
|
|
185
|
+
if local_ver and latest_ver and local_ver != latest_ver:
|
|
186
|
+
context += f'\n\nMemento update available: v{local_ver} → v{latest_ver}. Run: npx memento-mcp update'
|
|
187
|
+
|
|
158
188
|
print(json.dumps({
|
|
159
189
|
'hookSpecificOutput': {
|
|
160
190
|
'hookEventName': 'SessionStart',
|
|
@@ -163,7 +193,11 @@ print(json.dumps({
|
|
|
163
193
|
}))
|
|
164
194
|
" "$IDENTITY_TMP" "$ACTIVE_TMP" "$SKIP_TMP" 2>/dev/null
|
|
165
195
|
|
|
166
|
-
# Toast: done
|
|
167
|
-
"$
|
|
196
|
+
# Toast: done (with update notice if applicable)
|
|
197
|
+
if [ -n "$LATEST_VERSION" ] && [ -n "$LOCAL_VERSION" ] && [ "$LATEST_VERSION" != "$LOCAL_VERSION" ]; then
|
|
198
|
+
"$TOAST" memento "⬆ Memento v${LATEST_VERSION} available" &>/dev/null
|
|
199
|
+
else
|
|
200
|
+
"$TOAST" memento "✓ Identity loaded" &>/dev/null
|
|
201
|
+
fi
|
|
168
202
|
|
|
169
203
|
exit 0
|
|
@@ -78,13 +78,13 @@ QUERY="${ASSISTANT_MSG:0:500}"
|
|
|
78
78
|
# Toast: start retrieving
|
|
79
79
|
"$TOAST" memento "⏳ Autonomous recall..." &>/dev/null
|
|
80
80
|
|
|
81
|
-
# Call Memento /v1/context
|
|
82
|
-
RESULT=$(curl -s --max-time
|
|
81
|
+
# Call Memento /v1/context (with auto_extract for passive memory formation)
|
|
82
|
+
RESULT=$(curl -s --max-time 8 \
|
|
83
83
|
-X POST \
|
|
84
84
|
-H "Authorization: Bearer $MEMENTO_KEY" \
|
|
85
85
|
-H "X-Memento-Workspace: $MEMENTO_WS" \
|
|
86
86
|
-H "Content-Type: application/json" \
|
|
87
|
-
-d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"]}" \
|
|
87
|
+
-d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"], \"auto_extract\": true, \"extract_role\": \"assistant\", \"extract_content\": $(echo "$ASSISTANT_MSG" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}" \
|
|
88
88
|
"$MEMENTO_API/v1/context" 2>/dev/null \
|
|
89
89
|
| python3 -c "
|
|
90
90
|
import json, sys
|
|
@@ -111,35 +111,60 @@ try:
|
|
|
111
111
|
for s in skip_matches:
|
|
112
112
|
lines.append(f' ⚠ SKIP: {s[\"item\"]} — {s[\"reason\"]} (expires: {s[\"expires\"]})')
|
|
113
113
|
|
|
114
|
+
# Auto-extracted memories
|
|
115
|
+
extracted = data.get('extracted', [])
|
|
116
|
+
if extracted:
|
|
117
|
+
lines.append('')
|
|
118
|
+
lines.append('MEMORIES STORED:')
|
|
119
|
+
for e in extracted:
|
|
120
|
+
etags = [t for t in e.get('tags', []) if t != 'source:auto-extract']
|
|
121
|
+
etag_str = f' [{\", \".join(etags)}]' if etags else ''
|
|
122
|
+
lines.append(f' {e[\"id\"]} ({e.get(\"type\", \"observation\")}){etag_str} — {e[\"content\"]}')
|
|
123
|
+
|
|
124
|
+
# Output: recall_count \t extracted_count \t detail
|
|
125
|
+
extracted_count = len(extracted)
|
|
114
126
|
detail = '\n'.join(lines)
|
|
115
|
-
print(f'{count}\t{detail}')
|
|
127
|
+
print(f'{count}\t{extracted_count}\t{detail}')
|
|
116
128
|
except Exception:
|
|
117
|
-
print('0\t')
|
|
129
|
+
print('0\t0\t')
|
|
118
130
|
" 2>/dev/null)
|
|
119
131
|
|
|
120
|
-
# Parse
|
|
132
|
+
# Parse count, extracted count, and detail
|
|
121
133
|
SAAS_COUNT=$(echo "$RESULT" | head -1 | cut -f1)
|
|
122
|
-
|
|
134
|
+
EXTRACTED_COUNT=$(echo "$RESULT" | head -1 | cut -f2)
|
|
135
|
+
SAAS_DETAIL=$(echo "$RESULT" | head -1 | cut -f3-)
|
|
123
136
|
REMAINING=$(echo "$RESULT" | tail -n +2)
|
|
124
137
|
if [ -n "$REMAINING" ]; then
|
|
125
138
|
SAAS_DETAIL="$SAAS_DETAIL"$'\n'"$REMAINING"
|
|
126
139
|
fi
|
|
127
140
|
|
|
128
141
|
if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
|
|
129
|
-
"$
|
|
130
|
-
|
|
142
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
143
|
+
"$TOAST" memento "✓ ${EXTRACTED_COUNT} memories stored" &>/dev/null
|
|
144
|
+
else
|
|
145
|
+
"$TOAST" memento "✓ No memories matched" &>/dev/null
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
else
|
|
149
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
150
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} recalled, ${EXTRACTED_COUNT} stored" &>/dev/null
|
|
151
|
+
else
|
|
152
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} memories recalled" &>/dev/null
|
|
153
|
+
fi
|
|
131
154
|
fi
|
|
132
155
|
|
|
133
|
-
#
|
|
134
|
-
"
|
|
156
|
+
# Build summary line
|
|
157
|
+
SUMMARY="Autonomous Recall: ${SAAS_COUNT} memories"
|
|
158
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
159
|
+
SUMMARY="${SUMMARY}, ${EXTRACTED_COUNT} memories stored"
|
|
160
|
+
fi
|
|
135
161
|
|
|
136
162
|
# Block the Stop so Claude continues — the reason becomes Claude's next instruction.
|
|
137
|
-
REASON="
|
|
163
|
+
REASON="${SUMMARY} surfaced from your last response.
|
|
138
164
|
${SAAS_DETAIL}
|
|
139
165
|
|
|
140
166
|
You have absorbed these memories into context. If any recalled memory is stale, wrong, or overlaps with others — update, delete, or consolidate it now. Otherwise continue naturally."
|
|
141
167
|
|
|
142
|
-
SUMMARY="Autonomous Recall: ${SAAS_COUNT} memories"
|
|
143
168
|
python3 -c "
|
|
144
169
|
import json, sys
|
|
145
170
|
print(json.dumps({
|
|
@@ -70,13 +70,13 @@ QUERY="${USER_MESSAGE:0:500}"
|
|
|
70
70
|
# Toast: start retrieving
|
|
71
71
|
"$TOAST" memento "⏳ Retrieving memories..." &>/dev/null
|
|
72
72
|
|
|
73
|
-
# Call Memento SaaS /v1/context
|
|
74
|
-
SAAS_OUTPUT=$(curl -s --max-time
|
|
73
|
+
# Call Memento SaaS /v1/context (with auto_extract for passive memory formation)
|
|
74
|
+
SAAS_OUTPUT=$(curl -s --max-time 8 \
|
|
75
75
|
-X POST \
|
|
76
76
|
-H "Authorization: Bearer $MEMENTO_KEY" \
|
|
77
77
|
-H "X-Memento-Workspace: $MEMENTO_WS" \
|
|
78
78
|
-H "Content-Type: application/json" \
|
|
79
|
-
-d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"]}" \
|
|
79
|
+
-d "{\"message\": $(echo "$QUERY" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'), \"include\": [\"memories\", \"skip_list\"], \"auto_extract\": true, \"extract_role\": \"user\", \"extract_content\": $(echo "$USER_MESSAGE" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')}" \
|
|
80
80
|
"$MEMENTO_API/v1/context" 2>/dev/null \
|
|
81
81
|
| python3 -c "
|
|
82
82
|
import json, sys
|
|
@@ -105,16 +105,28 @@ try:
|
|
|
105
105
|
for s in skip_matches:
|
|
106
106
|
lines.append(f' ⚠ SKIP: {s[\"item\"]} — {s[\"reason\"]} (expires: {s[\"expires\"]})')
|
|
107
107
|
|
|
108
|
-
#
|
|
108
|
+
# Auto-extracted memories
|
|
109
|
+
extracted = data.get('extracted', [])
|
|
110
|
+
if extracted:
|
|
111
|
+
lines.append('')
|
|
112
|
+
lines.append('MEMORIES STORED:')
|
|
113
|
+
for e in extracted:
|
|
114
|
+
etags = [t for t in e.get('tags', []) if t != 'source:auto-extract']
|
|
115
|
+
etag_str = f' [{\", \".join(etags)}]' if etags else ''
|
|
116
|
+
lines.append(f' {e[\"id\"]} ({e.get(\"type\", \"observation\")}){etag_str} — {e[\"content\"]}')
|
|
117
|
+
|
|
118
|
+
# Output: recall_count \t extracted_count \t detail
|
|
119
|
+
extracted_count = len(extracted)
|
|
109
120
|
detail = '\n'.join(lines)
|
|
110
|
-
print(f'{count}\t{detail}')
|
|
121
|
+
print(f'{count}\t{extracted_count}\t{detail}')
|
|
111
122
|
except Exception:
|
|
112
|
-
print('0\t')
|
|
123
|
+
print('0\t0\t')
|
|
113
124
|
" 2>/dev/null)
|
|
114
125
|
|
|
115
|
-
# Parse count and detail
|
|
126
|
+
# Parse count, extracted count, and detail
|
|
116
127
|
SAAS_COUNT=$(echo "$SAAS_OUTPUT" | head -1 | cut -f1)
|
|
117
|
-
|
|
128
|
+
EXTRACTED_COUNT=$(echo "$SAAS_OUTPUT" | head -1 | cut -f2)
|
|
129
|
+
SAAS_DETAIL=$(echo "$SAAS_OUTPUT" | head -1 | cut -f3-)
|
|
118
130
|
# Append any remaining lines (skip warnings etc.)
|
|
119
131
|
REMAINING=$(echo "$SAAS_OUTPUT" | tail -n +2)
|
|
120
132
|
if [ -n "$REMAINING" ]; then
|
|
@@ -122,18 +134,30 @@ if [ -n "$REMAINING" ]; then
|
|
|
122
134
|
fi
|
|
123
135
|
|
|
124
136
|
if [ -z "$SAAS_COUNT" ] || [ "$SAAS_COUNT" = "0" ]; then
|
|
125
|
-
"$
|
|
126
|
-
|
|
137
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
138
|
+
"$TOAST" memento "✓ ${EXTRACTED_COUNT} memories stored" &>/dev/null
|
|
139
|
+
else
|
|
140
|
+
"$TOAST" memento "✓ No memories matched" &>/dev/null
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
else
|
|
144
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
145
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} recalled, ${EXTRACTED_COUNT} stored" &>/dev/null
|
|
146
|
+
else
|
|
147
|
+
"$TOAST" memento "✓ ${SAAS_COUNT} memories recalled" &>/dev/null
|
|
148
|
+
fi
|
|
127
149
|
fi
|
|
128
150
|
|
|
129
|
-
#
|
|
130
|
-
"
|
|
151
|
+
# Build summary line
|
|
152
|
+
SUMMARY="Memento Recall: ${SAAS_COUNT} memories"
|
|
153
|
+
if [ -n "$EXTRACTED_COUNT" ] && [ "$EXTRACTED_COUNT" != "0" ]; then
|
|
154
|
+
SUMMARY="${SUMMARY}, ${EXTRACTED_COUNT} memories stored"
|
|
155
|
+
fi
|
|
131
156
|
|
|
132
|
-
DETAIL_TEXT="
|
|
157
|
+
DETAIL_TEXT="$SUMMARY"
|
|
133
158
|
DETAIL_TEXT="$DETAIL_TEXT"$'\n'"$SAAS_DETAIL"
|
|
134
159
|
DETAIL_TEXT="$DETAIL_TEXT"$'\n'$'\n'"REMINDER: If Memento MCP tools are not loaded, run: ToolSearch query=\"+memento\" max_results=20"
|
|
135
160
|
|
|
136
|
-
SUMMARY="Memento Recall: ${SAAS_COUNT} memories"
|
|
137
161
|
python3 -c "
|
|
138
162
|
import json, sys
|
|
139
163
|
summary = sys.argv[1]
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import readline from "node:readline";
|
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import https from "node:https";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { DEFAULTS } from "./config.js";
|
|
14
15
|
|
|
@@ -92,6 +93,91 @@ function httpsPost(url, body) {
|
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Instructions blob — used by headless integration and fallback print
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const INSTRUCTIONS_BLOB = `## Memento Protocol
|
|
101
|
+
|
|
102
|
+
Working memory is managed by Memento. MCP tools available:
|
|
103
|
+
\`memento_store\`, \`memento_recall\`, \`memento_item_list\`,
|
|
104
|
+
\`memento_skip_add\`, \`memento_skip_check\`.
|
|
105
|
+
|
|
106
|
+
**Memory discipline — notes are instructions, not logs.**
|
|
107
|
+
Write: "Skip X until condition Y" — not "checked X, it was quiet."
|
|
108
|
+
Every memory must answer: could a future agent with zero context
|
|
109
|
+
read this and know exactly what to do?
|
|
110
|
+
|
|
111
|
+
Use \`memento_store\` when you learn something worth keeping.
|
|
112
|
+
Use \`memento_skip_add\` for things to explicitly not re-investigate.
|
|
113
|
+
Use \`memento_recall\` to search memories by keyword or tag.
|
|
114
|
+
Hooks run automatically — recall before responses, distillation
|
|
115
|
+
before compaction. Trust the hooks. Focus on writing good memories.`;
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Headless agent integration
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const HEADLESS_CMDS = {
|
|
122
|
+
"claude-code": (prompt) => ["claude", "-p", prompt],
|
|
123
|
+
"codex": (prompt) => ["codex", "exec", prompt],
|
|
124
|
+
"gemini": (prompt) => ["gemini", prompt],
|
|
125
|
+
"opencode": (prompt) => ["opencode", "run", prompt],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function buildIntegrationPrompt(blob) {
|
|
129
|
+
return [
|
|
130
|
+
"The following instructions were generated by memento-mcp init for this project.",
|
|
131
|
+
"Add them to the file where you store persistent behavioral instructions",
|
|
132
|
+
"(e.g. CLAUDE.md for Claude Code). If the file exists, read it first and",
|
|
133
|
+
"integrate the new section without removing existing content. If a section",
|
|
134
|
+
"with the same heading already exists, replace it. If no instructions file",
|
|
135
|
+
"exists yet, create one.",
|
|
136
|
+
"",
|
|
137
|
+
"--- INSTRUCTIONS ---",
|
|
138
|
+
blob,
|
|
139
|
+
"--- END ---",
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function runAgentHeadless(agentKey, prompt) {
|
|
144
|
+
const cmdBuilder = HEADLESS_CMDS[agentKey];
|
|
145
|
+
if (!cmdBuilder) return null;
|
|
146
|
+
const [cmd, ...args] = cmdBuilder(prompt);
|
|
147
|
+
try {
|
|
148
|
+
const result = execFileSync(cmd, args, {
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
152
|
+
timeout: 60000,
|
|
153
|
+
});
|
|
154
|
+
return result;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// CLI flag parsing
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
function parseFlags(argv) {
|
|
165
|
+
const flags = { nonInteractive: false, apiKey: null };
|
|
166
|
+
for (let i = 0; i < argv.length; i++) {
|
|
167
|
+
if (argv[i] === "-y" || argv[i] === "--yes") {
|
|
168
|
+
flags.nonInteractive = true;
|
|
169
|
+
} else if (argv[i] === "--api-key" && argv[i + 1]) {
|
|
170
|
+
flags.apiKey = argv[i + 1];
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Also check environment variable
|
|
175
|
+
if (!flags.apiKey && process.env.MEMENTO_API_KEY) {
|
|
176
|
+
flags.apiKey = process.env.MEMENTO_API_KEY;
|
|
177
|
+
}
|
|
178
|
+
return flags;
|
|
179
|
+
}
|
|
180
|
+
|
|
95
181
|
// ---------------------------------------------------------------------------
|
|
96
182
|
// File writers
|
|
97
183
|
// ---------------------------------------------------------------------------
|
|
@@ -232,14 +318,14 @@ export { AGENTS, writeMcpJson, writeCodexToml, writeGeminiJson, writeOpencodeJso
|
|
|
232
318
|
// CLI
|
|
233
319
|
// ---------------------------------------------------------------------------
|
|
234
320
|
|
|
235
|
-
async function runInit() {
|
|
321
|
+
async function runInit(flags = {}) {
|
|
322
|
+
const { nonInteractive = false, apiKey: flagApiKey = null } = flags;
|
|
236
323
|
const cwd = process.cwd();
|
|
237
324
|
const projectName = path.basename(cwd);
|
|
238
325
|
|
|
239
|
-
const rl =
|
|
240
|
-
|
|
241
|
-
output: process.stdout
|
|
242
|
-
});
|
|
326
|
+
const rl = nonInteractive
|
|
327
|
+
? null
|
|
328
|
+
: readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
243
329
|
|
|
244
330
|
console.log(`
|
|
245
331
|
▗
|
|
@@ -251,12 +337,29 @@ async function runInit() {
|
|
|
251
337
|
`);
|
|
252
338
|
|
|
253
339
|
// 1. Workspace name
|
|
254
|
-
const workspace =
|
|
340
|
+
const workspace = nonInteractive
|
|
341
|
+
? projectName
|
|
342
|
+
: await ask(rl, "Workspace name", projectName);
|
|
255
343
|
|
|
256
344
|
// 2. API key
|
|
257
|
-
let apiKey =
|
|
345
|
+
let apiKey = flagApiKey || "";
|
|
346
|
+
if (!nonInteractive) {
|
|
347
|
+
apiKey = await ask(rl, "API key (leave blank to sign up)");
|
|
348
|
+
}
|
|
258
349
|
if (!apiKey) {
|
|
259
|
-
|
|
350
|
+
if (nonInteractive) {
|
|
351
|
+
// 5-second countdown warning, then auto-signup
|
|
352
|
+
process.stderr.write(
|
|
353
|
+
"\n No API key provided — a new one will be generated.\n" +
|
|
354
|
+
" Press Ctrl+C to cancel.\n "
|
|
355
|
+
);
|
|
356
|
+
for (let i = 5; i > 0; i--) {
|
|
357
|
+
process.stderr.write(`${i}... `);
|
|
358
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
359
|
+
}
|
|
360
|
+
process.stderr.write("\n\n");
|
|
361
|
+
}
|
|
362
|
+
const email = nonInteractive ? "" : await ask(rl, "Email for account recovery (optional)");
|
|
260
363
|
console.log("\nSigning up...");
|
|
261
364
|
try {
|
|
262
365
|
const body = { workspace };
|
|
@@ -267,73 +370,84 @@ async function runInit() {
|
|
|
267
370
|
console.log(` API key: ${apiKey}`);
|
|
268
371
|
} else if (resp.error) {
|
|
269
372
|
console.error(` Signup failed: ${resp.error}`);
|
|
270
|
-
rl
|
|
373
|
+
rl?.close();
|
|
271
374
|
process.exit(1);
|
|
272
375
|
} else {
|
|
273
376
|
console.error(" Unexpected response:", JSON.stringify(resp));
|
|
274
|
-
rl
|
|
377
|
+
rl?.close();
|
|
275
378
|
process.exit(1);
|
|
276
379
|
}
|
|
277
380
|
} catch (err) {
|
|
278
381
|
console.error(` Signup failed: ${err.message}`);
|
|
279
|
-
rl
|
|
382
|
+
rl?.close();
|
|
280
383
|
process.exit(1);
|
|
281
384
|
}
|
|
282
385
|
}
|
|
283
386
|
|
|
284
387
|
// 3. Features
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
388
|
+
let enableImages = false;
|
|
389
|
+
let enableIdentity = false;
|
|
390
|
+
if (!nonInteractive) {
|
|
391
|
+
console.log("\nOptional features:");
|
|
392
|
+
enableImages = await askYesNo(
|
|
393
|
+
rl,
|
|
394
|
+
" Enable image attachments? (attach images to memories via memento_store)",
|
|
395
|
+
false,
|
|
396
|
+
);
|
|
397
|
+
enableIdentity = await askYesNo(
|
|
398
|
+
rl,
|
|
399
|
+
" Enable identity crystal? (persist a first-person identity snapshot across sessions)",
|
|
400
|
+
false,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
296
403
|
|
|
297
404
|
// 4. Agent detection + selection
|
|
298
405
|
const agentKeys = Object.keys(AGENTS);
|
|
299
406
|
const detected = agentKeys.filter((key) => AGENTS[key].detect(cwd));
|
|
300
407
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
408
|
+
let selectedAgents;
|
|
409
|
+
if (nonInteractive) {
|
|
410
|
+
// Auto-detect: use first detected agent, or default to claude-code
|
|
411
|
+
selectedAgents = detected.length > 0 ? [detected[0]] : ["claude-code"];
|
|
412
|
+
console.log(` Agent: ${AGENTS[selectedAgents[0]].name} (auto-detected)`);
|
|
413
|
+
} else {
|
|
414
|
+
console.log("\nDetected agents:");
|
|
415
|
+
const markers = {
|
|
416
|
+
"claude-code": ".claude/",
|
|
417
|
+
codex: ".codex/",
|
|
418
|
+
gemini: ".gemini/",
|
|
419
|
+
opencode: "opencode.json",
|
|
420
|
+
};
|
|
421
|
+
for (const key of agentKeys) {
|
|
422
|
+
const agent = AGENTS[key];
|
|
423
|
+
const isDetected = detected.includes(key);
|
|
424
|
+
const mark = isDetected ? "✓" : " ";
|
|
425
|
+
const hint = isDetected ? ` (${markers[key]} found)` : "";
|
|
426
|
+
console.log(` ${mark} ${agent.name}${hint}`);
|
|
427
|
+
}
|
|
315
428
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
429
|
+
console.log("\n Configure for which agents?");
|
|
430
|
+
agentKeys.forEach((key, i) => {
|
|
431
|
+
const mark = detected.includes(key) ? " ✓" : "";
|
|
432
|
+
console.log(` ${i + 1}. ${AGENTS[key].name}${mark}`);
|
|
433
|
+
});
|
|
321
434
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
435
|
+
const defaultSelection =
|
|
436
|
+
detected.length > 0
|
|
437
|
+
? detected.map((key) => agentKeys.indexOf(key) + 1).join(",")
|
|
438
|
+
: "1";
|
|
439
|
+
const selectionStr = await ask(rl, "\n Enter numbers, comma-separated", defaultSelection);
|
|
440
|
+
|
|
441
|
+
const selectedIndices = selectionStr
|
|
442
|
+
.split(",")
|
|
443
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
444
|
+
.filter((n) => n >= 1 && n <= agentKeys.length);
|
|
445
|
+
selectedAgents = [...new Set(selectedIndices.map((i) => agentKeys[i - 1]))];
|
|
446
|
+
|
|
447
|
+
if (selectedAgents.length === 0) {
|
|
448
|
+
console.log(" No agents selected. Defaulting to Claude Code.");
|
|
449
|
+
selectedAgents.push("claude-code");
|
|
450
|
+
}
|
|
337
451
|
}
|
|
338
452
|
|
|
339
453
|
const hasClaude = selectedAgents.includes("claude-code");
|
|
@@ -345,28 +459,36 @@ async function runInit() {
|
|
|
345
459
|
let enableSessionStart = false;
|
|
346
460
|
|
|
347
461
|
if (hasClaude) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
true
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
" PreCompact — distill memories before context compression?",
|
|
358
|
-
true,
|
|
359
|
-
);
|
|
360
|
-
if (enableIdentity) {
|
|
361
|
-
enableSessionStart = await askYesNo(
|
|
462
|
+
if (nonInteractive) {
|
|
463
|
+
// All hooks on by default in non-interactive mode
|
|
464
|
+
enableUserPrompt = true;
|
|
465
|
+
enableStop = true;
|
|
466
|
+
enablePreCompact = true;
|
|
467
|
+
enableSessionStart = false; // identity not enabled in -y mode
|
|
468
|
+
} else {
|
|
469
|
+
console.log("\nClaude Code hooks (automate recall + distillation):");
|
|
470
|
+
enableUserPrompt = await askYesNo(
|
|
362
471
|
rl,
|
|
363
|
-
"
|
|
472
|
+
" UserPromptSubmit — recall on every message?",
|
|
364
473
|
true,
|
|
365
474
|
);
|
|
475
|
+
enableStop = await askYesNo(rl, " Stop — autonomous recall after responses?", true);
|
|
476
|
+
enablePreCompact = await askYesNo(
|
|
477
|
+
rl,
|
|
478
|
+
" PreCompact — distill memories before context compression?",
|
|
479
|
+
true,
|
|
480
|
+
);
|
|
481
|
+
if (enableIdentity) {
|
|
482
|
+
enableSessionStart = await askYesNo(
|
|
483
|
+
rl,
|
|
484
|
+
" SessionStart — inject identity + active items at startup?",
|
|
485
|
+
true,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
366
488
|
}
|
|
367
489
|
}
|
|
368
490
|
|
|
369
|
-
rl
|
|
491
|
+
rl?.close();
|
|
370
492
|
|
|
371
493
|
// Build config
|
|
372
494
|
const config = {
|
|
@@ -418,6 +540,12 @@ async function runInit() {
|
|
|
418
540
|
}
|
|
419
541
|
created.push(".memento/scripts/");
|
|
420
542
|
|
|
543
|
+
// 7b. Write .memento/version for update checks
|
|
544
|
+
const pkgJsonPath = path.resolve(__dirname, "..", "package.json");
|
|
545
|
+
const pkgVersion = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).version;
|
|
546
|
+
const versionPath = path.join(cwd, ".memento", "version");
|
|
547
|
+
fs.writeFileSync(versionPath, pkgVersion + "\n");
|
|
548
|
+
|
|
421
549
|
// 8. Write .claude/settings.local.json (hooks)
|
|
422
550
|
const hooks = {};
|
|
423
551
|
if (enableUserPrompt) {
|
|
@@ -522,7 +650,140 @@ async function runInit() {
|
|
|
522
650
|
}
|
|
523
651
|
console.log(" Your agent will wake up remembering.\n");
|
|
524
652
|
|
|
525
|
-
// 12.
|
|
653
|
+
// 12. Auto-integrate agent instructions
|
|
654
|
+
const primaryAgent = selectedAgents[0];
|
|
655
|
+
const prompt = buildIntegrationPrompt(INSTRUCTIONS_BLOB);
|
|
656
|
+
const cmdParts = HEADLESS_CMDS[primaryAgent]?.(prompt);
|
|
657
|
+
|
|
658
|
+
if (nonInteractive) {
|
|
659
|
+
// Auto-run headless integration, no prompt
|
|
660
|
+
if (cmdParts) {
|
|
661
|
+
console.log(` Integrating instructions via ${AGENTS[primaryAgent].name}...`);
|
|
662
|
+
const result = runAgentHeadless(primaryAgent, prompt);
|
|
663
|
+
if (result !== null) {
|
|
664
|
+
console.log(result);
|
|
665
|
+
} else {
|
|
666
|
+
// Fallback: print the blob
|
|
667
|
+
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
668
|
+
printInstructionsFallback(selectedAgents);
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
printInstructionsFallback(selectedAgents);
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
// Interactive: ask with explicit command shown
|
|
675
|
+
if (cmdParts) {
|
|
676
|
+
const [cmd, ...args] = cmdParts;
|
|
677
|
+
const displayCmd = `${cmd} ${args[0]}${args.length > 1 ? " ..." : ""}`;
|
|
678
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
679
|
+
console.log("─".repeat(60));
|
|
680
|
+
const integrate = await askYesNo(
|
|
681
|
+
rl2,
|
|
682
|
+
`\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n Proceed?`,
|
|
683
|
+
true,
|
|
684
|
+
);
|
|
685
|
+
rl2.close();
|
|
686
|
+
|
|
687
|
+
if (integrate) {
|
|
688
|
+
console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
|
|
689
|
+
const result = runAgentHeadless(primaryAgent, prompt);
|
|
690
|
+
if (result !== null) {
|
|
691
|
+
console.log(result);
|
|
692
|
+
} else {
|
|
693
|
+
console.log(" Agent integration failed — paste these instructions manually:\n");
|
|
694
|
+
printInstructionsFallback(selectedAgents);
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
printInstructionsFallback(selectedAgents);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
printInstructionsFallback(selectedAgents);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
// Update command — copy fresh hook scripts to an existing installation
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
async function runUpdate() {
|
|
710
|
+
const cwd = process.cwd();
|
|
711
|
+
const configPath = path.join(cwd, ".memento.json");
|
|
712
|
+
|
|
713
|
+
if (!fs.existsSync(configPath)) {
|
|
714
|
+
console.error(
|
|
715
|
+
" Error: .memento.json not found in current directory.\n" +
|
|
716
|
+
" Run `npx memento-mcp init` first to set up Memento.\n"
|
|
717
|
+
);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const pkgJsonPath = path.resolve(__dirname, "..", "package.json");
|
|
722
|
+
const pkgVersion = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).version;
|
|
723
|
+
|
|
724
|
+
const pkgScriptsDir = path.resolve(__dirname, "..", "scripts");
|
|
725
|
+
const localScriptsDir = path.join(cwd, ".memento", "scripts");
|
|
726
|
+
|
|
727
|
+
if (!fs.existsSync(localScriptsDir)) {
|
|
728
|
+
fs.mkdirSync(localScriptsDir, { recursive: true });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Copy all .sh files from package scripts/ to local .memento/scripts/
|
|
732
|
+
const scriptFiles = fs
|
|
733
|
+
.readdirSync(pkgScriptsDir)
|
|
734
|
+
.filter((f) => f.endsWith(".sh"));
|
|
735
|
+
|
|
736
|
+
const updated = [];
|
|
737
|
+
for (const name of scriptFiles) {
|
|
738
|
+
const src = path.join(pkgScriptsDir, name);
|
|
739
|
+
const dest = path.join(localScriptsDir, name);
|
|
740
|
+
fs.copyFileSync(src, dest);
|
|
741
|
+
fs.chmodSync(dest, 0o755);
|
|
742
|
+
updated.push(name);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Write .memento/version
|
|
746
|
+
const versionPath = path.join(cwd, ".memento", "version");
|
|
747
|
+
fs.writeFileSync(versionPath, pkgVersion + "\n");
|
|
748
|
+
|
|
749
|
+
// Ensure SessionStart hook is registered for Claude Code workspaces
|
|
750
|
+
const config = readJsonFile(configPath) || {};
|
|
751
|
+
const agents = config.agents || [];
|
|
752
|
+
const hasClaude = agents.includes("claude-code");
|
|
753
|
+
let hooksUpdated = false;
|
|
754
|
+
if (hasClaude) {
|
|
755
|
+
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
756
|
+
const claudeSettings = readJsonFile(settingsPath) || {};
|
|
757
|
+
const sessionStartCmd = path.join(localScriptsDir, "memento-sessionstart-identity.sh");
|
|
758
|
+
const existingHooks = claudeSettings.hooks?.["SessionStart"] || [];
|
|
759
|
+
const hasSessionStart = existingHooks.some((entry) =>
|
|
760
|
+
entry.hooks?.some((h) => h.command === sessionStartCmd)
|
|
761
|
+
);
|
|
762
|
+
if (!hasSessionStart) {
|
|
763
|
+
claudeSettings.hooks = claudeSettings.hooks || {};
|
|
764
|
+
claudeSettings.hooks["SessionStart"] = [
|
|
765
|
+
...existingHooks,
|
|
766
|
+
{ hooks: [{ type: "command", command: sessionStartCmd, timeout: 10000 }] },
|
|
767
|
+
];
|
|
768
|
+
writeJsonFile(settingsPath, claudeSettings);
|
|
769
|
+
hooksUpdated = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
console.log(`\n ✓ Memento hooks updated to v${pkgVersion}\n`);
|
|
774
|
+
console.log(" Updated scripts:");
|
|
775
|
+
for (const name of updated) {
|
|
776
|
+
console.log(` ${name}`);
|
|
777
|
+
}
|
|
778
|
+
if (hooksUpdated) {
|
|
779
|
+
console.log("\n Registered hooks:");
|
|
780
|
+
console.log(" SessionStart → memento-sessionstart-identity.sh (identity + version check)");
|
|
781
|
+
}
|
|
782
|
+
console.log(`\n Version written to .memento/version`);
|
|
783
|
+
console.log(" Restart your agent session to pick up changes.\n");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function printInstructionsFallback(selectedAgents) {
|
|
526
787
|
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
527
788
|
const docTarget = hasNonClaude
|
|
528
789
|
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
@@ -536,22 +797,7 @@ async function runInit() {
|
|
|
536
797
|
|
|
537
798
|
── paste below this line ──────────────────────────────
|
|
538
799
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
Working memory is managed by Memento. MCP tools available:
|
|
542
|
-
\`memento_store\`, \`memento_recall\`, \`memento_item_list\`,
|
|
543
|
-
\`memento_skip_add\`, \`memento_skip_check\`.
|
|
544
|
-
|
|
545
|
-
**Memory discipline — notes are instructions, not logs.**
|
|
546
|
-
Write: "Skip X until condition Y" — not "checked X, it was quiet."
|
|
547
|
-
Every memory must answer: could a future agent with zero context
|
|
548
|
-
read this and know exactly what to do?
|
|
549
|
-
|
|
550
|
-
Use \`memento_store\` when you learn something worth keeping.
|
|
551
|
-
Use \`memento_skip_add\` for things to explicitly not re-investigate.
|
|
552
|
-
Use \`memento_recall\` to search memories by keyword or tag.
|
|
553
|
-
Hooks run automatically — recall before responses, distillation
|
|
554
|
-
before compaction. Trust the hooks. Focus on writing good memories.
|
|
800
|
+
${INSTRUCTIONS_BLOB}
|
|
555
801
|
|
|
556
802
|
── paste above this line ──────────────────────────────
|
|
557
803
|
`);
|
|
@@ -569,7 +815,13 @@ if (isMain) {
|
|
|
569
815
|
const args = process.argv.slice(2);
|
|
570
816
|
|
|
571
817
|
if (args[0] === "init") {
|
|
572
|
-
|
|
818
|
+
const flags = parseFlags(args.slice(1));
|
|
819
|
+
runInit(flags).catch((err) => {
|
|
820
|
+
console.error(err);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
});
|
|
823
|
+
} else if (args[0] === "update") {
|
|
824
|
+
runUpdate().catch((err) => {
|
|
573
825
|
console.error(err);
|
|
574
826
|
process.exit(1);
|
|
575
827
|
});
|
|
@@ -584,11 +836,15 @@ if (isMain) {
|
|
|
584
836
|
Memento Protocol CLI
|
|
585
837
|
|
|
586
838
|
Usage:
|
|
587
|
-
npx memento-mcp init
|
|
588
|
-
npx memento-mcp
|
|
839
|
+
npx memento-mcp init Set up Memento in the current project
|
|
840
|
+
npx memento-mcp init -y Non-interactive setup (uses defaults)
|
|
841
|
+
npx memento-mcp init --api-key KEY Provide API key (skips signup)
|
|
842
|
+
npx memento-mcp update Update hook scripts to latest version
|
|
843
|
+
npx memento-mcp Start the MCP server (used by .mcp.json)
|
|
844
|
+
|
|
845
|
+
The -y flag enables fully non-interactive setup for CI/scripting.
|
|
846
|
+
Combine with --api-key to skip auto-signup.
|
|
589
847
|
|
|
590
|
-
This creates .memento.json, configures your agent's MCP client,
|
|
591
|
-
and sets up hooks (Claude Code) — all in one command.
|
|
592
848
|
Supports Claude Code, Codex, Gemini CLI, and OpenCode.
|
|
593
849
|
`);
|
|
594
850
|
process.exit(1);
|