synabun 1.3.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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SynaBun PostToolUse Hook — Plan Storage
|
|
5
|
+
*
|
|
6
|
+
* Matches: ^(Enter|Exit)PlanMode$
|
|
7
|
+
*
|
|
8
|
+
* EnterPlanMode: Injects a reminder to use AskUserQuestion for
|
|
9
|
+
* clarifications instead of plain-text questions.
|
|
10
|
+
*
|
|
11
|
+
* ExitPlanMode: When Claude exits plan mode (plan approved), this hook:
|
|
12
|
+
* 1. Finds the most recently modified plan file in ~/.claude/plans/
|
|
13
|
+
* 2. Auto-creates a child category under "plans" for the project if needed
|
|
14
|
+
* 3. Generates a local embedding and stores the plan in SQLite
|
|
15
|
+
* 4. Returns additionalContext confirming storage
|
|
16
|
+
*
|
|
17
|
+
* Input (stdin JSON):
|
|
18
|
+
* { session_id, tool_name, tool_input, tool_response, cwd }
|
|
19
|
+
*
|
|
20
|
+
* Output (stdout JSON):
|
|
21
|
+
* { additionalContext: "..." } on success, or {} on skip/error
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, writeFileSync, appendFileSync, readdirSync, statSync, existsSync, mkdirSync } from 'node:fs';
|
|
25
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
import { randomUUID } from 'node:crypto';
|
|
29
|
+
import { detectProject, getMcpCategoriesPath, MCP_DATA_DIR, DATA_DIR } from './shared.mjs';
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
|
|
33
|
+
// Debug log (append-only, survives across invocations)
|
|
34
|
+
const DEBUG_LOG_PATH = join(DATA_DIR, 'plan-debug.log');
|
|
35
|
+
function debugLog(msg) {
|
|
36
|
+
try {
|
|
37
|
+
const ts = new Date().toISOString();
|
|
38
|
+
appendFileSync(DEBUG_LOG_PATH, `[${ts}] ${msg}\n`, 'utf-8');
|
|
39
|
+
} catch { /* best-effort */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Plans directory — project-local to avoid ~/.claude/ sensitive-file permission prompts
|
|
43
|
+
const PLANS_DIR = join(DATA_DIR, 'plans');
|
|
44
|
+
// Fallback: Claude Code's ExitPlanMode may still write to ~/.claude/plans/ internally
|
|
45
|
+
const PLANS_DIR_FALLBACK = join(process.env.USERPROFILE || process.env.HOME || '', '.claude', 'plans');
|
|
46
|
+
|
|
47
|
+
// Dedup tracker — records which plan files have already been stored
|
|
48
|
+
const STORED_PLANS_PATH = join(DATA_DIR, 'stored-plans.json');
|
|
49
|
+
|
|
50
|
+
// SQLite database path
|
|
51
|
+
const DB_PATH = process.env.SQLITE_DB_PATH || join(MCP_DATA_DIR, 'memory.db');
|
|
52
|
+
|
|
53
|
+
// ─── Stdin ───
|
|
54
|
+
|
|
55
|
+
function readStdin() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
if (process.stdin.isTTY) return resolve('{}');
|
|
58
|
+
let data = '';
|
|
59
|
+
process.stdin.setEncoding('utf8');
|
|
60
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
61
|
+
process.stdin.on('end', () => resolve(data));
|
|
62
|
+
setTimeout(() => resolve(data || '{}'), 3000);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Local embedding generation ───
|
|
67
|
+
|
|
68
|
+
async function generateEmbedding(text) {
|
|
69
|
+
const embPath = join(__dirname, '..', '..', 'mcp-server', 'dist', 'services', 'local-embeddings.js');
|
|
70
|
+
if (!existsSync(embPath)) {
|
|
71
|
+
throw new Error('MCP server not built. Run: cd mcp-server && npm run build');
|
|
72
|
+
}
|
|
73
|
+
const { generateEmbedding: embed } = await import(pathToFileURL(embPath).href);
|
|
74
|
+
return embed(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Vector encoding ───
|
|
78
|
+
|
|
79
|
+
function encodeVector(vector) {
|
|
80
|
+
const f32 = new Float32Array(vector);
|
|
81
|
+
return new Uint8Array(f32.buffer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Database storage ───
|
|
85
|
+
|
|
86
|
+
function storeInSQLite(id, vector, payload) {
|
|
87
|
+
const db = new DatabaseSync(DB_PATH);
|
|
88
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
89
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
db.prepare(`
|
|
93
|
+
INSERT OR REPLACE INTO memories
|
|
94
|
+
(id, vector, content, category, subcategory, project, tags, importance, source,
|
|
95
|
+
created_at, updated_at, accessed_at, access_count, related_files,
|
|
96
|
+
related_memory_ids, file_checksums, trashed_at, source_session_chunks)
|
|
97
|
+
VALUES
|
|
98
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
`).run(
|
|
100
|
+
id,
|
|
101
|
+
encodeVector(vector),
|
|
102
|
+
payload.content,
|
|
103
|
+
payload.category,
|
|
104
|
+
payload.subcategory || null,
|
|
105
|
+
payload.project,
|
|
106
|
+
JSON.stringify(payload.tags || []),
|
|
107
|
+
payload.importance || 5,
|
|
108
|
+
payload.source || 'auto-saved',
|
|
109
|
+
payload.created_at,
|
|
110
|
+
payload.updated_at,
|
|
111
|
+
payload.accessed_at,
|
|
112
|
+
payload.access_count || 0,
|
|
113
|
+
null, // related_files
|
|
114
|
+
null, // related_memory_ids
|
|
115
|
+
null, // file_checksums
|
|
116
|
+
null, // trashed_at
|
|
117
|
+
null, // source_session_chunks
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
db.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Dedup tracker ───
|
|
125
|
+
|
|
126
|
+
function loadStoredPlans() {
|
|
127
|
+
try {
|
|
128
|
+
if (!existsSync(STORED_PLANS_PATH)) return {};
|
|
129
|
+
return JSON.parse(readFileSync(STORED_PLANS_PATH, 'utf-8'));
|
|
130
|
+
} catch { return {}; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function markPlanStored(fileName, memoryId) {
|
|
134
|
+
const stored = loadStoredPlans();
|
|
135
|
+
stored[fileName] = { memoryId, storedAt: new Date().toISOString() };
|
|
136
|
+
try {
|
|
137
|
+
if (!existsSync(dirname(STORED_PLANS_PATH))) mkdirSync(dirname(STORED_PLANS_PATH), { recursive: true });
|
|
138
|
+
writeFileSync(STORED_PLANS_PATH, JSON.stringify(stored, null, 2), 'utf-8');
|
|
139
|
+
} catch { /* best-effort */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Plan file discovery ───
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* List all plan .md files, sorted by mtime descending.
|
|
146
|
+
* Each entry: { name, path, mtime }
|
|
147
|
+
*/
|
|
148
|
+
function listPlanFiles() {
|
|
149
|
+
const results = [];
|
|
150
|
+
for (const dir of [PLANS_DIR, PLANS_DIR_FALLBACK]) {
|
|
151
|
+
if (!existsSync(dir)) continue;
|
|
152
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
153
|
+
// Skip duplicates (same filename in both dirs — prefer primary)
|
|
154
|
+
if (results.some((r) => r.name === f)) continue;
|
|
155
|
+
const fullPath = join(dir, f);
|
|
156
|
+
const stat = statSync(fullPath);
|
|
157
|
+
results.push({ name: f, path: fullPath, mtime: stat.mtimeMs });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return results.sort((a, b) => b.mtime - a.mtime);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Match the correct plan file using tool_response content from ExitPlanMode.
|
|
165
|
+
* Strategies (in order):
|
|
166
|
+
* 1. Filename extraction — tool_response may reference the plan filename
|
|
167
|
+
* 2. Content matching — tool_response may contain the plan text; find the file whose content matches
|
|
168
|
+
* Returns null if no match or if the matched file is already stored.
|
|
169
|
+
*/
|
|
170
|
+
function findPlanByContent(toolResponse) {
|
|
171
|
+
if (!toolResponse || typeof toolResponse !== 'string') return null;
|
|
172
|
+
|
|
173
|
+
const stored = loadStoredPlans();
|
|
174
|
+
const files = listPlanFiles();
|
|
175
|
+
if (files.length === 0) return null;
|
|
176
|
+
|
|
177
|
+
// Strategy 1: Extract filename pattern (word-word-word.md) from response
|
|
178
|
+
const fnameMatch = toolResponse.match(/([a-z]+-[a-z]+-[a-z]+\.md)/);
|
|
179
|
+
if (fnameMatch) {
|
|
180
|
+
const matched = files.find((f) => f.name === fnameMatch[1]);
|
|
181
|
+
if (matched && !stored[matched.name]) return { ...matched, method: 'filename' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Strategy 2: Content matching — compare response text against each unstored plan file
|
|
185
|
+
// tool_response from ExitPlanMode likely contains the plan content itself
|
|
186
|
+
const responseTrimmed = toolResponse.trim();
|
|
187
|
+
if (responseTrimmed.length > 50) {
|
|
188
|
+
// Extract first meaningful line (H1 heading or first paragraph) for fast pre-filter
|
|
189
|
+
const responseFirstLine = responseTrimmed.split('\n').find((l) => l.trim())?.trim() || '';
|
|
190
|
+
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (stored[file.name]) continue;
|
|
193
|
+
try {
|
|
194
|
+
const content = readFileSync(file.path, 'utf-8').trim();
|
|
195
|
+
// Exact match
|
|
196
|
+
if (content === responseTrimmed) return { ...file, method: 'exact-content' };
|
|
197
|
+
// Response contains the file content (response may have extra wrapper text)
|
|
198
|
+
if (responseTrimmed.includes(content)) return { ...file, method: 'content-in-response' };
|
|
199
|
+
// File content contains the response (file is the full plan, response is a subset)
|
|
200
|
+
if (content.includes(responseTrimmed)) return { ...file, method: 'response-in-content' };
|
|
201
|
+
// First-line match as fallback (H1 heading match)
|
|
202
|
+
if (responseFirstLine.length > 10) {
|
|
203
|
+
const fileFirstLine = content.split('\n').find((l) => l.trim())?.trim() || '';
|
|
204
|
+
if (fileFirstLine === responseFirstLine) return { ...file, method: 'heading-match' };
|
|
205
|
+
}
|
|
206
|
+
} catch { /* skip unreadable files */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fallback: return the most recently modified unstored plan file.
|
|
215
|
+
* Used only when content matching fails (no tool_response or no match).
|
|
216
|
+
*/
|
|
217
|
+
function findLatestPlan() {
|
|
218
|
+
const stored = loadStoredPlans();
|
|
219
|
+
const files = listPlanFiles();
|
|
220
|
+
if (files.length === 0) return null;
|
|
221
|
+
const unstored = files.find((f) => !stored[f.name]);
|
|
222
|
+
return unstored ? { ...unstored, method: 'mtime-fallback' } : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Category management ───
|
|
226
|
+
|
|
227
|
+
function ensureProjectCategory(project) {
|
|
228
|
+
const categoryName = `plans-${project}`;
|
|
229
|
+
const catPath = getMcpCategoriesPath();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const data = JSON.parse(readFileSync(catPath, 'utf-8'));
|
|
233
|
+
const categories = data.categories || [];
|
|
234
|
+
|
|
235
|
+
// Check if child category already exists
|
|
236
|
+
if (categories.some((c) => c.name === categoryName)) {
|
|
237
|
+
return categoryName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check if "plans" parent exists
|
|
241
|
+
if (!categories.some((c) => c.name === 'plans')) {
|
|
242
|
+
// Create parent if missing (shouldn't happen, but safety net)
|
|
243
|
+
categories.push({
|
|
244
|
+
name: 'plans',
|
|
245
|
+
description: 'Implementation plans stored after plan mode approval. Sub-categorized by project name.',
|
|
246
|
+
is_parent: true,
|
|
247
|
+
color: '#06d6a0',
|
|
248
|
+
created_at: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Create child category for this project
|
|
253
|
+
categories.push({
|
|
254
|
+
name: categoryName,
|
|
255
|
+
description: `Implementation plans for ${project} project`,
|
|
256
|
+
parent: 'plans',
|
|
257
|
+
created_at: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
data.categories = categories;
|
|
261
|
+
writeFileSync(catPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
262
|
+
|
|
263
|
+
return categoryName;
|
|
264
|
+
} catch {
|
|
265
|
+
// If categories file can't be read/written, fall back to parent
|
|
266
|
+
return 'plans';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Neural Interface cache invalidation ───
|
|
271
|
+
|
|
272
|
+
async function invalidateNeuralInterface() {
|
|
273
|
+
try {
|
|
274
|
+
// Trigger sync broadcast so the Neural Interface graph refreshes
|
|
275
|
+
const niUrl = process.env.SYNABUN_NI_URL || 'http://localhost:3344';
|
|
276
|
+
await fetch(`${niUrl}/api/memories?invalidate=true`, {
|
|
277
|
+
method: 'GET',
|
|
278
|
+
signal: AbortSignal.timeout(2000),
|
|
279
|
+
});
|
|
280
|
+
} catch { /* fire-and-forget */ }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Extract plan title from markdown ───
|
|
284
|
+
|
|
285
|
+
function extractPlanTitle(content) {
|
|
286
|
+
// Look for first H1 heading
|
|
287
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
288
|
+
if (h1Match) return h1Match[1].trim();
|
|
289
|
+
|
|
290
|
+
// Fall back to first non-empty line
|
|
291
|
+
const firstLine = content.split('\n').find((l) => l.trim());
|
|
292
|
+
return firstLine?.trim().slice(0, 100) || 'Untitled plan';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Main ───
|
|
296
|
+
|
|
297
|
+
async function main() {
|
|
298
|
+
let input = {};
|
|
299
|
+
try {
|
|
300
|
+
const raw = await readStdin();
|
|
301
|
+
input = JSON.parse(raw);
|
|
302
|
+
} catch { /* proceed */ }
|
|
303
|
+
|
|
304
|
+
const toolName = input.tool_name || '';
|
|
305
|
+
|
|
306
|
+
debugLog(`Hook invoked — tool_name: "${toolName}", session: ${input.session_id || 'unknown'}, cwd: ${input.cwd || 'unknown'}`);
|
|
307
|
+
|
|
308
|
+
// Handle EnterPlanMode — remind Claude to use AskUserQuestion
|
|
309
|
+
if (toolName === 'EnterPlanMode') {
|
|
310
|
+
debugLog('EnterPlanMode fired — injecting AskUserQuestion reminder');
|
|
311
|
+
process.stdout.write(JSON.stringify({
|
|
312
|
+
additionalContext: `SynaBun: You are now in plan mode. When you have questions or need clarification from the user, you MUST use the \`AskUserQuestion\` tool — do NOT write questions as plain text. Load it via \`ToolSearch\` if its schema is not yet available. Use \`ExitPlanMode\` for final plan approval.\n\n**Plan file location**: Write plan files to \`${PLANS_DIR}/\` — NOT \`~/.claude/plans/\`. This avoids sensitive-file permission prompts that stall the session.`,
|
|
313
|
+
}));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Only handle ExitPlanMode beyond this point
|
|
318
|
+
if (toolName !== 'ExitPlanMode') {
|
|
319
|
+
process.stdout.write(JSON.stringify({}));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Match plan file: content-match first (session-accurate), mtime fallback
|
|
324
|
+
const toolResponse = typeof input.tool_response === 'string'
|
|
325
|
+
? input.tool_response
|
|
326
|
+
: (input.tool_response != null ? JSON.stringify(input.tool_response) : '');
|
|
327
|
+
|
|
328
|
+
debugLog(`ExitPlanMode fired — tool_response length: ${toolResponse.length}, first 200 chars: ${toolResponse.slice(0, 200).replace(/\n/g, '\\n')}`);
|
|
329
|
+
|
|
330
|
+
const planFile = findPlanByContent(toolResponse) || findLatestPlan();
|
|
331
|
+
if (!planFile) {
|
|
332
|
+
process.stdout.write(JSON.stringify({
|
|
333
|
+
additionalContext: 'SynaBun: No unstored plan file found (all plans already in memory).',
|
|
334
|
+
}));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const planContent = readFileSync(planFile.path, 'utf-8');
|
|
339
|
+
if (!planContent.trim()) {
|
|
340
|
+
process.stdout.write(JSON.stringify({}));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const project = detectProject(input.cwd || '');
|
|
345
|
+
const planTitle = extractPlanTitle(planContent);
|
|
346
|
+
|
|
347
|
+
// Ensure child category exists under "plans"
|
|
348
|
+
const categoryName = ensureProjectCategory(project);
|
|
349
|
+
|
|
350
|
+
// Generate local embedding (Transformers.js, no API key needed)
|
|
351
|
+
const embedding = await generateEmbedding(planContent);
|
|
352
|
+
|
|
353
|
+
// Store in SQLite
|
|
354
|
+
const id = randomUUID();
|
|
355
|
+
const now = new Date().toISOString();
|
|
356
|
+
|
|
357
|
+
storeInSQLite(id, embedding, {
|
|
358
|
+
content: planContent,
|
|
359
|
+
category: categoryName,
|
|
360
|
+
project,
|
|
361
|
+
importance: 7,
|
|
362
|
+
tags: ['plan', 'implementation', project],
|
|
363
|
+
source: 'auto-saved',
|
|
364
|
+
subcategory: 'plan',
|
|
365
|
+
created_at: now,
|
|
366
|
+
updated_at: now,
|
|
367
|
+
accessed_at: now,
|
|
368
|
+
access_count: 0,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Mark this plan as stored (dedup for future ExitPlanMode calls)
|
|
372
|
+
markPlanStored(planFile.name, id);
|
|
373
|
+
|
|
374
|
+
// Invalidate Neural Interface cache (fire-and-forget)
|
|
375
|
+
invalidateNeuralInterface();
|
|
376
|
+
|
|
377
|
+
// Confirm storage — include match method for diagnostics
|
|
378
|
+
const shortTitle = planTitle.length > 60 ? planTitle.slice(0, 60) + '...' : planTitle;
|
|
379
|
+
const matchInfo = planFile.method || 'unknown';
|
|
380
|
+
debugLog(`Stored plan: ${planFile.name} [${matchInfo}] → memory ${id} (project: ${project})`);
|
|
381
|
+
process.stdout.write(JSON.stringify({
|
|
382
|
+
additionalContext: `SynaBun: Plan stored in memory [${id}] — "${shortTitle}" (category: ${categoryName}, project: ${project}). Source file: ${planFile.name} [matched: ${matchInfo}]`,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
main().catch((err) => {
|
|
387
|
+
// On error, still output valid JSON so Claude Code doesn't break
|
|
388
|
+
process.stdout.write(JSON.stringify({
|
|
389
|
+
additionalContext: `SynaBun: Plan storage failed — ${err.message}. The plan file is still saved at ${PLANS_DIR}.`,
|
|
390
|
+
}));
|
|
391
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SynaBun PostToolUse Hook for Claude Code (unified handler)
|
|
5
|
+
*
|
|
6
|
+
* Matches: ^Edit$|^Write$|^NotebookEdit$|mcp__SynaBun__remember
|
|
7
|
+
*
|
|
8
|
+
* Two responsibilities:
|
|
9
|
+
*
|
|
10
|
+
* 1. EDIT TRACKING — When Claude uses Edit, Write, or NotebookEdit,
|
|
11
|
+
* increments a pending-remember counter. If Claude finishes responding
|
|
12
|
+
* with 3+ unremembered edits, the Stop hook will block it.
|
|
13
|
+
*
|
|
14
|
+
* 2. FLAG MANAGEMENT — When Claude calls `remember`:
|
|
15
|
+
* - category "conversations" → clears pending-compact flag (compaction enforcement)
|
|
16
|
+
* - any other category → resets pending-remember flag (editCount→0,
|
|
17
|
+
* keeps rememberCount/totalEdits so subsequent edits start a fresh segment)
|
|
18
|
+
*
|
|
19
|
+
* Input (stdin JSON):
|
|
20
|
+
* { session_id, tool_name, tool_input: { ... }, tool_response: { ... } }
|
|
21
|
+
*
|
|
22
|
+
* Output (stdout JSON):
|
|
23
|
+
* - On edit at threshold multiples (3, 6, 9...): { additionalContext: "..." }
|
|
24
|
+
* - Otherwise: {} (side effects only)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, readdirSync } from 'node:fs';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { loadCategories, buildCategoryReference, detectProject } from './shared.mjs';
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const DATA_DIR = join(__dirname, '..', '..', 'data');
|
|
34
|
+
const PENDING_COMPACT_DIR = join(DATA_DIR, 'pending-compact');
|
|
35
|
+
const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
|
|
36
|
+
|
|
37
|
+
// Ensure directories exist
|
|
38
|
+
for (const dir of [PENDING_COMPACT_DIR, PENDING_REMEMBER_DIR]) {
|
|
39
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
43
|
+
const EDIT_THRESHOLD = 1;
|
|
44
|
+
|
|
45
|
+
function readStdin() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
if (process.stdin.isTTY) return resolve('{}');
|
|
48
|
+
let data = '';
|
|
49
|
+
process.stdin.setEncoding('utf8');
|
|
50
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
51
|
+
process.stdin.on('end', () => resolve(data));
|
|
52
|
+
setTimeout(() => resolve(data || '{}'), 2000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
let input = {};
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readStdin();
|
|
60
|
+
input = JSON.parse(raw);
|
|
61
|
+
} catch { /* proceed */ }
|
|
62
|
+
|
|
63
|
+
const sessionId = input.session_id || '';
|
|
64
|
+
const toolName = input.tool_name || '';
|
|
65
|
+
const toolInput = input.tool_input || {};
|
|
66
|
+
const cwd = input.cwd || '';
|
|
67
|
+
|
|
68
|
+
if (!sessionId) {
|
|
69
|
+
process.stdout.write(JSON.stringify({}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── EDIT TRACKING ───
|
|
74
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
75
|
+
const flagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
|
|
76
|
+
let flag = { editCount: 0, retries: 0, files: [], firstEditAt: null };
|
|
77
|
+
|
|
78
|
+
// Read existing flag if present
|
|
79
|
+
if (existsSync(flagPath)) {
|
|
80
|
+
try {
|
|
81
|
+
flag = JSON.parse(readFileSync(flagPath, 'utf-8'));
|
|
82
|
+
} catch { /* start fresh */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Increment edit counters
|
|
86
|
+
flag.editCount = (flag.editCount || 0) + 1;
|
|
87
|
+
flag.totalEdits = (flag.totalEdits || 0) + 1;
|
|
88
|
+
if (!flag.firstEditAt) flag.firstEditAt = new Date().toISOString();
|
|
89
|
+
flag.lastEditAt = new Date().toISOString();
|
|
90
|
+
|
|
91
|
+
// Track file path if available
|
|
92
|
+
const filePath = toolInput.file_path || toolInput.notebook_path || '';
|
|
93
|
+
if (filePath && Array.isArray(flag.files) && !flag.files.includes(filePath)) {
|
|
94
|
+
flag.files.push(filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
writeFileSync(flagPath, JSON.stringify(flag));
|
|
99
|
+
} catch { /* ok */ }
|
|
100
|
+
|
|
101
|
+
// Proactive nudge at every multiple of threshold (3, 6, 9, ...)
|
|
102
|
+
if (flag.editCount > 0 && flag.editCount % EDIT_THRESHOLD === 0) {
|
|
103
|
+
let nudgeText;
|
|
104
|
+
if (flag.editCount <= EDIT_THRESHOLD) {
|
|
105
|
+
nudgeText = `SynaBun: ${flag.editCount} file edits so far — remember to store this work in memory before wrapping up.`;
|
|
106
|
+
} else if (flag.editCount <= EDIT_THRESHOLD * 2) {
|
|
107
|
+
nudgeText = `SynaBun: ${flag.editCount} file edits without storing to memory. Call \`remember\` soon to avoid losing context.`;
|
|
108
|
+
} else {
|
|
109
|
+
nudgeText = `SynaBun: ${flag.editCount} file edits — this is significant work. You MUST call \`remember\` before continuing.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// On first threshold hit, append the full category reference (lazy injection)
|
|
113
|
+
if (!flag.categoryTreeInjected) {
|
|
114
|
+
try {
|
|
115
|
+
const categories = loadCategories();
|
|
116
|
+
const project = detectProject(cwd);
|
|
117
|
+
const categoryRef = buildCategoryReference(categories, project);
|
|
118
|
+
nudgeText += `\n\n${categoryRef}`;
|
|
119
|
+
flag.categoryTreeInjected = true;
|
|
120
|
+
writeFileSync(flagPath, JSON.stringify(flag));
|
|
121
|
+
} catch { /* category tree optional — nudge still works without it */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
process.stdout.write(JSON.stringify({ additionalContext: nudgeText }));
|
|
125
|
+
} else {
|
|
126
|
+
process.stdout.write(JSON.stringify({}));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── USER LEARNING CLEARING (remember or reflect for communication-style/personality) ───
|
|
132
|
+
// Since user learning is non-blocking (prompt-submit nudge only), any remember or
|
|
133
|
+
// reflect targeting communication-style clears the flag immediately.
|
|
134
|
+
const UL_CATEGORIES = ['communication-style', 'personality'];
|
|
135
|
+
const isRememberUL = toolName.includes('remember') && UL_CATEGORIES.includes(toolInput.category || '');
|
|
136
|
+
const isReflectUL = toolName.includes('reflect');
|
|
137
|
+
|
|
138
|
+
if (isRememberUL || isReflectUL) {
|
|
139
|
+
const ulFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
|
|
140
|
+
if (existsSync(ulFlagPath)) {
|
|
141
|
+
try {
|
|
142
|
+
const ulFlag = JSON.parse(readFileSync(ulFlagPath, 'utf-8'));
|
|
143
|
+
if (ulFlag.userLearningPending && (isRememberUL || isReflectUL)) {
|
|
144
|
+
ulFlag.userLearningPending = false;
|
|
145
|
+
ulFlag.userLearningObserved = true;
|
|
146
|
+
writeFileSync(ulFlagPath, JSON.stringify(ulFlag));
|
|
147
|
+
}
|
|
148
|
+
} catch { /* ok */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── REMEMBER FLAG CLEARING ───
|
|
153
|
+
// Both remember and reflect count as storing work — clear edit tracking for either.
|
|
154
|
+
if (toolName.includes('remember') || toolName.includes('reflect')) {
|
|
155
|
+
const category = toolInput.category || '';
|
|
156
|
+
|
|
157
|
+
if (category === 'conversations') {
|
|
158
|
+
// Clear ALL pending-compact flags (session ID may differ after compaction)
|
|
159
|
+
try {
|
|
160
|
+
if (existsSync(PENDING_COMPACT_DIR)) {
|
|
161
|
+
for (const f of readdirSync(PENDING_COMPACT_DIR).filter(f => f.endsWith('.json'))) {
|
|
162
|
+
try { unlinkSync(join(PENDING_COMPACT_DIR, f)); } catch { /* ok */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch { /* ok */ }
|
|
166
|
+
// Also reset pending-remember flag (conversations remember counts too)
|
|
167
|
+
const rememberFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
|
|
168
|
+
if (existsSync(rememberFlagPath)) {
|
|
169
|
+
try {
|
|
170
|
+
let flag = {};
|
|
171
|
+
try { flag = JSON.parse(readFileSync(rememberFlagPath, 'utf-8')); } catch { /* fresh */ }
|
|
172
|
+
flag.editCount = 0;
|
|
173
|
+
flag.messageCount = 0;
|
|
174
|
+
flag.retries = 0;
|
|
175
|
+
flag.files = [];
|
|
176
|
+
flag.firstEditAt = null;
|
|
177
|
+
flag.lastEditAt = null;
|
|
178
|
+
flag.rememberCount = (flag.rememberCount || 0) + 1;
|
|
179
|
+
flag.lastRememberedAt = new Date().toISOString();
|
|
180
|
+
writeFileSync(rememberFlagPath, JSON.stringify(flag));
|
|
181
|
+
} catch { /* ok */ }
|
|
182
|
+
}
|
|
183
|
+
} else if (category) {
|
|
184
|
+
// Reset pending-remember flag (keep file, zero counters for next task segment)
|
|
185
|
+
const rememberFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
|
|
186
|
+
if (existsSync(rememberFlagPath)) {
|
|
187
|
+
try {
|
|
188
|
+
let flag = {};
|
|
189
|
+
try { flag = JSON.parse(readFileSync(rememberFlagPath, 'utf-8')); } catch { /* start fresh */ }
|
|
190
|
+
|
|
191
|
+
const prevTotal = flag.totalEdits || flag.editCount || 0;
|
|
192
|
+
const prevRememberCount = flag.rememberCount || 0;
|
|
193
|
+
|
|
194
|
+
// Reset edit + message tracking but keep session-level stats
|
|
195
|
+
flag.editCount = 0;
|
|
196
|
+
flag.messageCount = 0;
|
|
197
|
+
flag.retries = 0;
|
|
198
|
+
flag.files = [];
|
|
199
|
+
flag.firstEditAt = null;
|
|
200
|
+
flag.lastEditAt = null;
|
|
201
|
+
flag.firstMessageAt = null;
|
|
202
|
+
flag.lastMessageAt = null;
|
|
203
|
+
flag.lastRememberedAt = new Date().toISOString();
|
|
204
|
+
flag.rememberCount = prevRememberCount + 1;
|
|
205
|
+
flag.totalEdits = prevTotal;
|
|
206
|
+
|
|
207
|
+
writeFileSync(rememberFlagPath, JSON.stringify(flag));
|
|
208
|
+
} catch {
|
|
209
|
+
// If reset fails, fall back to deleting
|
|
210
|
+
try { unlinkSync(rememberFlagPath); } catch { /* ok */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update loop memory tracking when remember is called during an active loop
|
|
217
|
+
if (toolName.includes('remember')) {
|
|
218
|
+
const LOOP_DIR = join(DATA_DIR, 'loop');
|
|
219
|
+
const loopPath = join(LOOP_DIR, `${sessionId}.json`);
|
|
220
|
+
if (existsSync(loopPath)) {
|
|
221
|
+
try {
|
|
222
|
+
const loop = JSON.parse(readFileSync(loopPath, 'utf-8'));
|
|
223
|
+
if (loop.active) {
|
|
224
|
+
loop.lastMemoryAt = loop.currentIteration || 0;
|
|
225
|
+
loop.memoryPending = false;
|
|
226
|
+
loop.memoryRetries = 0;
|
|
227
|
+
writeFileSync(loopPath, JSON.stringify(loop, null, 2));
|
|
228
|
+
}
|
|
229
|
+
} catch { /* ok */ }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Always output empty (no additional context needed)
|
|
234
|
+
process.stdout.write(JSON.stringify({}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
main().catch(() => {
|
|
238
|
+
process.stdout.write(JSON.stringify({}));
|
|
239
|
+
});
|