open-walnut 0.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/LICENSE +21 -0
- package/README.md +339 -0
- package/bin/open-walnut.js +2 -0
- package/bin/walnut.js +2 -0
- package/dist/cli.js +137849 -0
- package/dist/daemon-binaries/acp-worker.js +44 -0
- package/dist/daemon-binaries/acp-worker.js.version +1 -0
- package/dist/data/skills/daily-standup/SKILL.md +16 -0
- package/dist/data/skills/install-plugin/SKILL.md +118 -0
- package/dist/data/skills/learn/SKILL.md +46 -0
- package/dist/data/skills/register-repo/SKILL.md +20 -0
- package/dist/data/skills/setup-cloud-companion/SKILL.md +326 -0
- package/dist/data/skills/triage/SKILL.md +23 -0
- package/dist/data/skills/walnut-calendar/SKILL.md +92 -0
- package/dist/data/skills/walnut-manage-repos/SKILL.md +107 -0
- package/dist/data/skills/walnut-peer-sessions/SKILL.md +61 -0
- package/dist/data/skills/walnut-register-repo/SKILL.md +72 -0
- package/dist/data/skills/walnut-register-repo/references/yaml-format.md +111 -0
- package/dist/data/skills/walnut-register-repo/scripts/validate-repo.py +177 -0
- package/dist/data/skills/walnut-routines/SKILL.md +65 -0
- package/dist/data/skills/weekly-review/SKILL.md +22 -0
- package/dist/data/walnut-calendar.swift +213 -0
- package/dist/data/walnut-extract.swift +80 -0
- package/dist/hooks/on-compact.js +976 -0
- package/dist/hooks/on-stop.js +1329 -0
- package/dist/integrations/jira/index.js +4200 -0
- package/dist/integrations/jira/manifest.json +26 -0
- package/dist/integrations/local/index.js +51 -0
- package/dist/integrations/local/manifest.json +10 -0
- package/dist/integrations/ms-todo/index.js +4672 -0
- package/dist/integrations/ms-todo/manifest.json +17 -0
- package/dist/session-server/index.js +1382 -0
- package/dist/web/server.js +135275 -0
- package/dist/web/static/assets/apl-B4CMkyY2.js +1 -0
- package/dist/web/static/assets/asciiarmor-Df11BRmG.js +1 -0
- package/dist/web/static/assets/asn1-EdZsLKOL.js +1 -0
- package/dist/web/static/assets/asterisk-B-8jnY81.js +1 -0
- package/dist/web/static/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/dist/web/static/assets/clike-B9uivgTg.js +1 -0
- package/dist/web/static/assets/clojure-BMjYHr_A.js +1 -0
- package/dist/web/static/assets/cmake-BQqOBYOt.js +1 -0
- package/dist/web/static/assets/cobol-CWcv1MsR.js +1 -0
- package/dist/web/static/assets/coffeescript-S37ZYGWr.js +1 -0
- package/dist/web/static/assets/commonlisp-DBKNyK5s.js +1 -0
- package/dist/web/static/assets/crystal-SjHAIU92.js +1 -0
- package/dist/web/static/assets/css-BnMrqG3P.js +1 -0
- package/dist/web/static/assets/cypher-C_CwsFkJ.js +1 -0
- package/dist/web/static/assets/d-pRatUO7H.js +1 -0
- package/dist/web/static/assets/diff-DbItnlRl.js +1 -0
- package/dist/web/static/assets/dockerfile-BKs6k2Af.js +1 -0
- package/dist/web/static/assets/dtd-DF_7sFjM.js +1 -0
- package/dist/web/static/assets/dylan-DwRh75JA.js +1 -0
- package/dist/web/static/assets/ebnf-CDyGwa7X.js +1 -0
- package/dist/web/static/assets/ecl-Cabwm37j.js +1 -0
- package/dist/web/static/assets/eiffel-CnydiIhH.js +1 -0
- package/dist/web/static/assets/elm-vLlmbW-K.js +1 -0
- package/dist/web/static/assets/erlang-BNw1qcRV.js +1 -0
- package/dist/web/static/assets/factor-kuTfRLto.js +1 -0
- package/dist/web/static/assets/fcl-Kvtd6kyn.js +1 -0
- package/dist/web/static/assets/forth-Ffai-XNe.js +1 -0
- package/dist/web/static/assets/fortran-DYz_wnZ1.js +1 -0
- package/dist/web/static/assets/gas-Bneqetm1.js +1 -0
- package/dist/web/static/assets/gherkin-heZmZLOM.js +1 -0
- package/dist/web/static/assets/groovy-D9Dt4D0W.js +1 -0
- package/dist/web/static/assets/haskell-Cw1EW3IL.js +1 -0
- package/dist/web/static/assets/haxe-H-WmDvRZ.js +1 -0
- package/dist/web/static/assets/http-DBlCnlav.js +1 -0
- package/dist/web/static/assets/idl-BEugSyMb.js +1 -0
- package/dist/web/static/assets/index-B3K5yAdA.js +1 -0
- package/dist/web/static/assets/index-B7OvpXOs.js +632 -0
- package/dist/web/static/assets/index-C07UNJIJ.js +1 -0
- package/dist/web/static/assets/index-C8k-3wfu.js +2 -0
- package/dist/web/static/assets/index-CBXGsT3t.js +1 -0
- package/dist/web/static/assets/index-CUrO3qpX.js +1 -0
- package/dist/web/static/assets/index-CpTvBBAa.js +1 -0
- package/dist/web/static/assets/index-D4J6slTf.js +1 -0
- package/dist/web/static/assets/index-DUgC5K_q.js +3 -0
- package/dist/web/static/assets/index-DhqXE1wa.js +1 -0
- package/dist/web/static/assets/index-DkGqHTCk.js +1 -0
- package/dist/web/static/assets/index-DsxFiBic.js +1 -0
- package/dist/web/static/assets/index-Dz3umkfm.js +1 -0
- package/dist/web/static/assets/index-G2v0oaqp.js +1 -0
- package/dist/web/static/assets/index-GNDKJKwy.js +1 -0
- package/dist/web/static/assets/index-U1CVtNUH.css +32 -0
- package/dist/web/static/assets/index-ft4kpQXq.js +1 -0
- package/dist/web/static/assets/index-ls7h0u-p.js +1 -0
- package/dist/web/static/assets/index-nL5l5WPW.js +7 -0
- package/dist/web/static/assets/javascript-iXu5QeM3.js +1 -0
- package/dist/web/static/assets/julia-DuME0IfC.js +1 -0
- package/dist/web/static/assets/livescript-BwQOo05w.js +1 -0
- package/dist/web/static/assets/lua-VAEuO923.js +1 -0
- package/dist/web/static/assets/mathematica-DTrFuWx2.js +1 -0
- package/dist/web/static/assets/mbox-CNhZ1qSd.js +1 -0
- package/dist/web/static/assets/mirc-CjQqDB4T.js +1 -0
- package/dist/web/static/assets/mllike-CXdrOF99.js +1 -0
- package/dist/web/static/assets/modelica-Dc1JOy9r.js +1 -0
- package/dist/web/static/assets/mscgen-BA5vi2Kp.js +1 -0
- package/dist/web/static/assets/mumps-BT43cFF4.js +1 -0
- package/dist/web/static/assets/nginx-DdIZxoE0.js +1 -0
- package/dist/web/static/assets/nsis-LdVXkNf5.js +1 -0
- package/dist/web/static/assets/ntriples-BfvgReVJ.js +1 -0
- package/dist/web/static/assets/octave-Ck1zUtKM.js +1 -0
- package/dist/web/static/assets/oz-BzwKVEFT.js +1 -0
- package/dist/web/static/assets/pascal--L3eBynH.js +1 -0
- package/dist/web/static/assets/perl-CdXCOZ3F.js +1 -0
- package/dist/web/static/assets/pig-CevX1Tat.js +1 -0
- package/dist/web/static/assets/powershell-CFHJl5sT.js +1 -0
- package/dist/web/static/assets/properties-C78fOPTZ.js +1 -0
- package/dist/web/static/assets/protobuf-ChK-085T.js +1 -0
- package/dist/web/static/assets/pug-DeIclll2.js +1 -0
- package/dist/web/static/assets/puppet-DMA9R1ak.js +1 -0
- package/dist/web/static/assets/python-BuPzkPfP.js +1 -0
- package/dist/web/static/assets/q-pXgVlZs6.js +1 -0
- package/dist/web/static/assets/r-B6wPVr8A.js +1 -0
- package/dist/web/static/assets/rpm-CTu-6PCP.js +1 -0
- package/dist/web/static/assets/ruby-B2Rjki9n.js +1 -0
- package/dist/web/static/assets/sas-B4kiWyti.js +1 -0
- package/dist/web/static/assets/scheme-C41bIUwD.js +1 -0
- package/dist/web/static/assets/shell-CjFT_Tl9.js +1 -0
- package/dist/web/static/assets/sieve-C3Gn_uJK.js +1 -0
- package/dist/web/static/assets/simple-mode-GW_nhZxv.js +1 -0
- package/dist/web/static/assets/smalltalk-CnHTOXQT.js +1 -0
- package/dist/web/static/assets/solr-DehyRSwq.js +1 -0
- package/dist/web/static/assets/sparql-DkYu6x3z.js +1 -0
- package/dist/web/static/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/dist/web/static/assets/sql-D0XecflT.js +1 -0
- package/dist/web/static/assets/stex-C3f8Ysf7.js +1 -0
- package/dist/web/static/assets/stylus-B533Al4x.js +1 -0
- package/dist/web/static/assets/swift-BzpIVaGY.js +1 -0
- package/dist/web/static/assets/tcl-DVfN8rqt.js +1 -0
- package/dist/web/static/assets/textile-CnDTJFAw.js +1 -0
- package/dist/web/static/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/dist/web/static/assets/tiki-DGYXhP31.js +1 -0
- package/dist/web/static/assets/toml-Bm5Em-hy.js +1 -0
- package/dist/web/static/assets/troff-wAsdV37c.js +1 -0
- package/dist/web/static/assets/ttcn-CfJYG6tj.js +1 -0
- package/dist/web/static/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/dist/web/static/assets/turtle-B1tBg_DP.js +1 -0
- package/dist/web/static/assets/vb-CmGdzxic.js +1 -0
- package/dist/web/static/assets/vbscript-BuJXcnF6.js +1 -0
- package/dist/web/static/assets/velocity-D8B20fx6.js +1 -0
- package/dist/web/static/assets/verilog-C6RDOZhf.js +1 -0
- package/dist/web/static/assets/vhdl-lSbBsy5d.js +1 -0
- package/dist/web/static/assets/webidl-ZXfAyPTL.js +1 -0
- package/dist/web/static/assets/xquery-DzFWVndE.js +1 -0
- package/dist/web/static/assets/yacas-BJ4BC0dw.js +1 -0
- package/dist/web/static/assets/z80-Hz9HOZM7.js +1 -0
- package/dist/web/static/index.html +22 -0
- package/dist/web/static/walnut-icon.png +0 -0
- package/dist/workers/git-compaction-worker.js +898 -0
- package/dist/workers/qmd-index-worker.js +97162 -0
- package/package.json +143 -0
- package/patches/@anthropic-ai+claude-agent-sdk+0.2.50.patch +13 -0
- package/scripts/patch-qmd.mjs +171 -0
- package/scripts/postinstall.mjs +120 -0
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/hooks/shared.ts
|
|
9
|
+
import fs3 from "fs";
|
|
10
|
+
import path3 from "path";
|
|
11
|
+
|
|
12
|
+
// src/constants.ts
|
|
13
|
+
import path from "path";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import os from "os";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
var WALNUT_HOME = resolveOpenWalnutHome();
|
|
18
|
+
var IS_EPHEMERAL = process.argv.includes("--_ephemeral-child");
|
|
19
|
+
var CLOUD_MODE = process.env.WALNUT_CLOUD_MODE === "1";
|
|
20
|
+
function resolveOpenWalnutHome() {
|
|
21
|
+
const envHome = process.env.OPEN_WALNUT_HOME;
|
|
22
|
+
const productionHome = path.join(os.homedir(), ".open-walnut");
|
|
23
|
+
const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
|
|
24
|
+
if (isTestEnv) {
|
|
25
|
+
if (envHome && envHome !== productionHome && !envHome.startsWith(productionHome + path.sep)) {
|
|
26
|
+
assertNotProductionPath(envHome);
|
|
27
|
+
return envHome;
|
|
28
|
+
}
|
|
29
|
+
const testHome = path.join(os.tmpdir(), `open-walnut-test-${process.pid}`);
|
|
30
|
+
process.env.OPEN_WALNUT_HOME = testHome;
|
|
31
|
+
return testHome;
|
|
32
|
+
}
|
|
33
|
+
if (!envHome) return productionHome;
|
|
34
|
+
if (process.argv.includes("--_ephemeral-child")) return envHome;
|
|
35
|
+
if (isEphemeralTmpDir(envHome)) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`WARNING: OPEN_WALNUT_HOME=${envHome} looks like a leaked ephemeral temp dir.
|
|
38
|
+
Overriding to ${productionHome}. (Only --_ephemeral-child processes may use one.)
|
|
39
|
+
`
|
|
40
|
+
);
|
|
41
|
+
process.env.OPEN_WALNUT_HOME = productionHome;
|
|
42
|
+
return productionHome;
|
|
43
|
+
}
|
|
44
|
+
return envHome;
|
|
45
|
+
}
|
|
46
|
+
function assertNotProductionPath(inputPath) {
|
|
47
|
+
const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
|
|
48
|
+
if (!isTestEnv) return;
|
|
49
|
+
const resolved = path.resolve(inputPath);
|
|
50
|
+
const prodHome = path.join(os.homedir(), ".open-walnut");
|
|
51
|
+
if (resolved === prodHome || resolved.startsWith(prodHome + path.sep)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`SAFETY: Test process attempted to use production path: ${resolved}
|
|
54
|
+
This would destroy real user data in ~/.open-walnut/.
|
|
55
|
+
Set OPEN_WALNUT_HOME to a temp directory or let constants.ts auto-assign one.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isEphemeralTmpDir(inputPath) {
|
|
60
|
+
if (!/[\\/]open-walnut-[^\\/]+$/.test(inputPath)) return false;
|
|
61
|
+
const tmpDirs = /* @__PURE__ */ new Set();
|
|
62
|
+
tmpDirs.add(os.tmpdir());
|
|
63
|
+
try {
|
|
64
|
+
tmpDirs.add(fs.realpathSync(os.tmpdir()));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
tmpDirs.add(fs.realpathSync("/tmp"));
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
const parent = path.dirname(path.resolve(inputPath));
|
|
72
|
+
if (tmpDirs.has(parent)) return true;
|
|
73
|
+
try {
|
|
74
|
+
return tmpDirs.has(fs.realpathSync(parent));
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var TASKS_DIR = path.join(WALNUT_HOME, "tasks");
|
|
80
|
+
var TASKS_FILE = path.join(TASKS_DIR, "tasks.json");
|
|
81
|
+
var ARCHIVE_DIR = path.join(TASKS_DIR, "archive");
|
|
82
|
+
var MEMORY_DIR = path.join(WALNUT_HOME, "memory");
|
|
83
|
+
var SESSIONS_DIR = path.join(MEMORY_DIR, "sessions");
|
|
84
|
+
var PROJECTS_DIR = path.join(MEMORY_DIR, "projects");
|
|
85
|
+
var CONFIG_FILE = path.join(WALNUT_HOME, "config.yaml");
|
|
86
|
+
var SYNC_DIR = path.join(WALNUT_HOME, "sync");
|
|
87
|
+
var PLUGIN_STORES_DIR = path.join(WALNUT_HOME, "plugin-stores");
|
|
88
|
+
var SESSIONS_FILE = path.join(WALNUT_HOME, "sessions.json");
|
|
89
|
+
var CLAUDE_HOME = path.join(os.homedir(), ".claude");
|
|
90
|
+
var HISTORY_CACHE_DIR = path.join(WALNUT_HOME, "cache", "history");
|
|
91
|
+
var PROJECTION_CACHE_DIR = path.join(WALNUT_HOME, "cache", "projections");
|
|
92
|
+
var TRANSCRIPT_CACHE_DIR = path.join(WALNUT_HOME, "cache", "transcripts");
|
|
93
|
+
var TASK_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "task-queue");
|
|
94
|
+
var HOST_MODEL_CATALOG_FILE = path.join(WALNUT_HOME, "cache", "host-model-catalogs.json");
|
|
95
|
+
var HOOK_LOG_FILE = path.join(WALNUT_HOME, "hook-errors.log");
|
|
96
|
+
var DAILY_DIR = path.join(MEMORY_DIR, "daily");
|
|
97
|
+
var MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
|
|
98
|
+
var USER_FILE = path.join(MEMORY_DIR, "USER.md");
|
|
99
|
+
var LEGACY_MEMORY_FILE = path.join(WALNUT_HOME, "MEMORY.md");
|
|
100
|
+
var PROJECTS_MEMORY_DIR = path.join(MEMORY_DIR, "projects");
|
|
101
|
+
var CHAT_HISTORY_FILE = path.join(WALNUT_HOME, "chat-history.json");
|
|
102
|
+
var CONVERSATIONS_DIR = path.join(WALNUT_HOME, "conversations");
|
|
103
|
+
function agentDailyDir(agentId) {
|
|
104
|
+
if (!agentId || agentId === "general") return DAILY_DIR;
|
|
105
|
+
return path.join(MEMORY_DIR, "agents", agentId, "daily");
|
|
106
|
+
}
|
|
107
|
+
var GLOBAL_SKILLS_DIR = path.join(WALNUT_HOME, "skills");
|
|
108
|
+
var SKILL_SETTINGS_FILE = path.join(WALNUT_HOME, "skill-settings.json");
|
|
109
|
+
var CLAUDE_SKILLS_DIR = path.join(CLAUDE_HOME, "skills");
|
|
110
|
+
var CLAUDE_SETTINGS_FILE = path.join(CLAUDE_HOME, "settings.json");
|
|
111
|
+
var CLAUDE_PLUGINS_DIR = path.join(CLAUDE_HOME, "plugins");
|
|
112
|
+
var CLAUDE_CREDENTIALS_FILE = path.join(CLAUDE_HOME, ".credentials.json");
|
|
113
|
+
var CRON_FILE = path.join(WALNUT_HOME, "cron-jobs.json");
|
|
114
|
+
var USAGE_DB_FILE = path.join(WALNUT_HOME, "usage.sqlite");
|
|
115
|
+
var LOG_DIR = process.env.WALNUT_DAEMON_DIR || "/tmp/open-walnut";
|
|
116
|
+
var SESSION_STREAMS_DIR = path.join(LOG_DIR, "streams");
|
|
117
|
+
var DTACH_SOCKET_DIR = path.join(LOG_DIR, "term");
|
|
118
|
+
var SESSION_QUEUE_FILE = path.join(WALNUT_HOME, "session-message-queue.json");
|
|
119
|
+
var IMAGES_DIR = path.join(LOG_DIR, "images");
|
|
120
|
+
var REMOTE_IMAGES_DIR = path.join(IMAGES_DIR, "remote");
|
|
121
|
+
var PASTES_DIR = path.join(LOG_DIR, "pastes");
|
|
122
|
+
var HEARTBEAT_FILE = path.join(WALNUT_HOME, "HEARTBEAT.md");
|
|
123
|
+
var COMMANDS_DIR = path.join(WALNUT_HOME, "commands");
|
|
124
|
+
var BUILTIN_COMMANDS_DIR = (() => {
|
|
125
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
126
|
+
for (let i = 0; i < 5; i++) {
|
|
127
|
+
const candidate = path.join(dir, "data", "slash-commands");
|
|
128
|
+
try {
|
|
129
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
dir = path.dirname(dir);
|
|
133
|
+
}
|
|
134
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "slash-commands");
|
|
135
|
+
})();
|
|
136
|
+
var BUILTIN_SKILLS_DIR = (() => {
|
|
137
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
138
|
+
for (let i = 0; i < 5; i++) {
|
|
139
|
+
const candidate = path.join(dir, "data", "skills");
|
|
140
|
+
try {
|
|
141
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
dir = path.dirname(dir);
|
|
145
|
+
}
|
|
146
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "skills");
|
|
147
|
+
})();
|
|
148
|
+
var FREQUENT_DIRS_FILE = path.join(WALNUT_HOME, "frequent-directories.json");
|
|
149
|
+
var MENTION_DIRS_FILE = path.join(WALNUT_HOME, "mention-directories.json");
|
|
150
|
+
var NOTES_DIR = path.join(WALNUT_HOME, "notes");
|
|
151
|
+
var GLOBAL_NOTES_FILE = path.join(NOTES_DIR, "global-notes.md");
|
|
152
|
+
var NOTES_AGENTS_FILE = path.join(NOTES_DIR, "AGENTS.md");
|
|
153
|
+
var NOTES_CLAUDE_FILE = path.join(NOTES_DIR, "CLAUDE.md");
|
|
154
|
+
var REPOSITORIES_DIR = path.join(WALNUT_HOME, "repositories");
|
|
155
|
+
var REPOS_MEMORY_DIR = path.join(GLOBAL_SKILLS_DIR, "repos");
|
|
156
|
+
var TOPICS_DIR = path.join(MEMORY_DIR, "topics");
|
|
157
|
+
var COMPACTION_DIR = path.join(MEMORY_DIR, "compaction");
|
|
158
|
+
var MEMORY_INDEX_FILE = path.join(MEMORY_DIR, "index.md");
|
|
159
|
+
var WORKING_MEMORY_FILE = path.join(MEMORY_DIR, "working-memory.md");
|
|
160
|
+
var TIMELINE_DIR = path.join(WALNUT_HOME, "timeline");
|
|
161
|
+
var CONFIG_SHARE_DIR = path.join(WALNUT_HOME, "config", "share");
|
|
162
|
+
var UI_PREFS_FILE = path.join(CONFIG_SHARE_DIR, "ui-prefs.json");
|
|
163
|
+
var STT_VOCAB_FILE = path.join(CONFIG_SHARE_DIR, "stt-vocab.txt");
|
|
164
|
+
var TMP_DIR = path.join(WALNUT_HOME, "tmp");
|
|
165
|
+
var RECORDINGS_DIR = path.join(TMP_DIR, "recordings");
|
|
166
|
+
var LOG_PREFIX = "open-walnut-";
|
|
167
|
+
var DAEMON_BINARIES_DIR = (() => {
|
|
168
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
169
|
+
for (let i = 0; i < 5; i++) {
|
|
170
|
+
const candidate = path.join(dir, "dist", "daemon-binaries");
|
|
171
|
+
try {
|
|
172
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
dir = path.dirname(dir);
|
|
176
|
+
}
|
|
177
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist", "daemon-binaries");
|
|
178
|
+
})();
|
|
179
|
+
var WALNUT_INSTALL_DIR = (() => {
|
|
180
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
181
|
+
for (let i = 0; i < 6; i++) {
|
|
182
|
+
try {
|
|
183
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
184
|
+
if (pkg.name === "open-walnut" && fs.statSync(path.join(dir, ".git")).isDirectory()) return dir;
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
const parent = path.dirname(dir);
|
|
188
|
+
if (parent === dir) break;
|
|
189
|
+
dir = parent;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
// src/logging/logger.ts
|
|
195
|
+
import fs2 from "fs";
|
|
196
|
+
import fsp from "fs/promises";
|
|
197
|
+
import path2 from "path";
|
|
198
|
+
|
|
199
|
+
// src/logging/redact.ts
|
|
200
|
+
var REDACTED = "[REDACTED]";
|
|
201
|
+
var patterns = [
|
|
202
|
+
// PEM private key blocks (multiline)
|
|
203
|
+
{
|
|
204
|
+
re: /-----BEGIN\s[\w\s]*PRIVATE KEY-----[\s\S]*?-----END\s[\w\s]*PRIVATE KEY-----/g,
|
|
205
|
+
replacement: REDACTED
|
|
206
|
+
},
|
|
207
|
+
// Bearer tokens: Authorization: Bearer <token>
|
|
208
|
+
{
|
|
209
|
+
re: /(Bearer\s+)\S+/gi,
|
|
210
|
+
replacement: `$1${REDACTED}`
|
|
211
|
+
},
|
|
212
|
+
// OpenAI / Anthropic style keys: sk-... (at least 20 chars after prefix)
|
|
213
|
+
{
|
|
214
|
+
re: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
215
|
+
replacement: REDACTED
|
|
216
|
+
},
|
|
217
|
+
// AWS access key IDs: AKIA...
|
|
218
|
+
{
|
|
219
|
+
re: /\bAKIA[A-Z0-9]{12,}\b/g,
|
|
220
|
+
replacement: REDACTED
|
|
221
|
+
},
|
|
222
|
+
// AWS secret access key in key=value form
|
|
223
|
+
{
|
|
224
|
+
re: /(aws_secret_access_key\s*[=:]\s*)\S+/gi,
|
|
225
|
+
replacement: `$1${REDACTED}`
|
|
226
|
+
},
|
|
227
|
+
// AWS session token in key=value form
|
|
228
|
+
{
|
|
229
|
+
re: /(aws_session_token\s*[=:]\s*)\S+/gi,
|
|
230
|
+
replacement: `$1${REDACTED}`
|
|
231
|
+
},
|
|
232
|
+
// Credentials in URL userinfo: https://user:secret@host/...
|
|
233
|
+
// Motivating shape: execSync embeds the whole command in error.message, so a
|
|
234
|
+
// failed `git remote add origin https://walnut:<device-token>@host/git/data`
|
|
235
|
+
// otherwise logs the cloud companion's long-lived credential verbatim.
|
|
236
|
+
{
|
|
237
|
+
re: /(https?:\/\/[^/\s:@]+:)[^@/\s]+@/gi,
|
|
238
|
+
replacement: `$1${REDACTED}@`
|
|
239
|
+
},
|
|
240
|
+
// Generic secrets: password=, secret=, token=, api_key=, apikey=
|
|
241
|
+
{
|
|
242
|
+
re: /((?:password|secret|token|api_key|apikey)\s*[=:]\s*)\S+/gi,
|
|
243
|
+
replacement: `$1${REDACTED}`
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
function redactSensitiveText(text) {
|
|
247
|
+
let result = text;
|
|
248
|
+
for (const { re, replacement } of patterns) {
|
|
249
|
+
result = result.replace(re, replacement);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/logging/logger.ts
|
|
255
|
+
var dirEnsured = false;
|
|
256
|
+
function ensureLogDir() {
|
|
257
|
+
if (dirEnsured) return;
|
|
258
|
+
try {
|
|
259
|
+
fs2.mkdirSync(LOG_DIR, { recursive: true });
|
|
260
|
+
dirEnsured = true;
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function todayFileName() {
|
|
265
|
+
const d = /* @__PURE__ */ new Date();
|
|
266
|
+
const yyyy = d.getFullYear();
|
|
267
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
268
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
269
|
+
return `${LOG_PREFIX}${yyyy}-${mm}-${dd}.log`;
|
|
270
|
+
}
|
|
271
|
+
function logFilePath() {
|
|
272
|
+
return path2.join(LOG_DIR, todayFileName());
|
|
273
|
+
}
|
|
274
|
+
var FLUSH_INTERVAL_MS = 2e3;
|
|
275
|
+
var buffer = [];
|
|
276
|
+
var flushTimer = null;
|
|
277
|
+
var flushing = false;
|
|
278
|
+
function startFlushTimer() {
|
|
279
|
+
if (flushTimer) return;
|
|
280
|
+
flushTimer = setInterval(() => {
|
|
281
|
+
void flushLogBuffer();
|
|
282
|
+
}, FLUSH_INTERVAL_MS);
|
|
283
|
+
if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
|
|
284
|
+
flushTimer.unref();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function flushLogBuffer() {
|
|
288
|
+
if (flushing || buffer.length === 0) return;
|
|
289
|
+
flushing = true;
|
|
290
|
+
const batch = buffer;
|
|
291
|
+
buffer = [];
|
|
292
|
+
try {
|
|
293
|
+
ensureLogDir();
|
|
294
|
+
await fsp.appendFile(logFilePath(), batch.join(""), "utf-8");
|
|
295
|
+
} catch {
|
|
296
|
+
} finally {
|
|
297
|
+
flushing = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function flushLogBufferSync() {
|
|
301
|
+
if (buffer.length === 0) return;
|
|
302
|
+
const batch = buffer;
|
|
303
|
+
buffer = [];
|
|
304
|
+
try {
|
|
305
|
+
ensureLogDir();
|
|
306
|
+
fs2.appendFileSync(logFilePath(), batch.join(""), "utf-8");
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
process.on("beforeExit", flushLogBufferSync);
|
|
311
|
+
process.on("exit", flushLogBufferSync);
|
|
312
|
+
function writeLogEntry(entry) {
|
|
313
|
+
const line = redactSensitiveText(JSON.stringify(entry)) + "\n";
|
|
314
|
+
buffer.push(line);
|
|
315
|
+
startFlushTimer();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/logging/subsystem.ts
|
|
319
|
+
import chalk from "chalk";
|
|
320
|
+
|
|
321
|
+
// src/logging/levels.ts
|
|
322
|
+
var LOG_LEVEL_ORDER = {
|
|
323
|
+
trace: 0,
|
|
324
|
+
debug: 1,
|
|
325
|
+
info: 2,
|
|
326
|
+
warn: 3,
|
|
327
|
+
error: 4,
|
|
328
|
+
fatal: 5
|
|
329
|
+
};
|
|
330
|
+
var DEFAULT_LEVEL = "info";
|
|
331
|
+
function resolveConfiguredLevel() {
|
|
332
|
+
const raw = process.env.WALNUT_LOG_LEVEL?.toLowerCase().trim();
|
|
333
|
+
if (raw && raw in LOG_LEVEL_ORDER) return raw;
|
|
334
|
+
return DEFAULT_LEVEL;
|
|
335
|
+
}
|
|
336
|
+
var CONFIGURED_LOG_LEVEL = resolveConfiguredLevel();
|
|
337
|
+
function shouldLog(messageLevel, configuredLevel = CONFIGURED_LOG_LEVEL) {
|
|
338
|
+
return LOG_LEVEL_ORDER[messageLevel] >= LOG_LEVEL_ORDER[configuredLevel];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/logging/subsystem.ts
|
|
342
|
+
var levelColor = {
|
|
343
|
+
trace: chalk.gray,
|
|
344
|
+
debug: chalk.cyan,
|
|
345
|
+
info: chalk.green,
|
|
346
|
+
warn: chalk.yellow,
|
|
347
|
+
error: chalk.red,
|
|
348
|
+
fatal: chalk.bgRed.white
|
|
349
|
+
};
|
|
350
|
+
var levelLabel = {
|
|
351
|
+
trace: "TRC",
|
|
352
|
+
debug: "DBG",
|
|
353
|
+
info: "INF",
|
|
354
|
+
warn: "WRN",
|
|
355
|
+
error: "ERR",
|
|
356
|
+
fatal: "FTL"
|
|
357
|
+
};
|
|
358
|
+
function timeStamp(d) {
|
|
359
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
360
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
361
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
362
|
+
return `${hh}:${mm}:${ss}`;
|
|
363
|
+
}
|
|
364
|
+
function formatMeta(meta) {
|
|
365
|
+
if (!meta || Object.keys(meta).length === 0) return "";
|
|
366
|
+
return " " + chalk.dim(JSON.stringify(meta));
|
|
367
|
+
}
|
|
368
|
+
var ERROR_SINK_KEY = /* @__PURE__ */ Symbol.for("open-walnut.errorNotificationSink");
|
|
369
|
+
var inErrorSink = false;
|
|
370
|
+
function forwardToErrorSink(subsystem, message, meta) {
|
|
371
|
+
const sink = globalThis[ERROR_SINK_KEY];
|
|
372
|
+
if (!sink || inErrorSink) return;
|
|
373
|
+
if (subsystem.startsWith("notif")) return;
|
|
374
|
+
inErrorSink = true;
|
|
375
|
+
try {
|
|
376
|
+
sink({ subsystem, message, meta });
|
|
377
|
+
} catch {
|
|
378
|
+
} finally {
|
|
379
|
+
inErrorSink = false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function emit(level, subsystem, message, meta) {
|
|
383
|
+
if (!shouldLog(level)) return;
|
|
384
|
+
const now = /* @__PURE__ */ new Date();
|
|
385
|
+
writeLogEntry({
|
|
386
|
+
time: now.toISOString(),
|
|
387
|
+
level,
|
|
388
|
+
subsystem,
|
|
389
|
+
message,
|
|
390
|
+
...meta && Object.keys(meta).length > 0 ? meta : {}
|
|
391
|
+
});
|
|
392
|
+
if (level === "error" || level === "fatal") {
|
|
393
|
+
forwardToErrorSink(subsystem, message, meta);
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const colorFn = levelColor[level];
|
|
397
|
+
const tag = chalk.bold(`[${subsystem}]`);
|
|
398
|
+
const lbl = colorFn(levelLabel[level]);
|
|
399
|
+
const ts = chalk.dim(timeStamp(now));
|
|
400
|
+
const metaStr = formatMeta(meta);
|
|
401
|
+
const line = redactSensitiveText(`${ts} ${lbl} ${tag} ${message}${metaStr}
|
|
402
|
+
`);
|
|
403
|
+
process.stderr.write(line);
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function createSubsystemLogger(subsystem) {
|
|
408
|
+
const logger = {
|
|
409
|
+
trace: (msg, meta) => emit("trace", subsystem, msg, meta),
|
|
410
|
+
debug: (msg, meta) => emit("debug", subsystem, msg, meta),
|
|
411
|
+
info: (msg, meta) => emit("info", subsystem, msg, meta),
|
|
412
|
+
warn: (msg, meta) => emit("warn", subsystem, msg, meta),
|
|
413
|
+
error: (msg, meta) => emit("error", subsystem, msg, meta),
|
|
414
|
+
fatal: (msg, meta) => emit("fatal", subsystem, msg, meta),
|
|
415
|
+
child(subName) {
|
|
416
|
+
return createSubsystemLogger(`${subsystem}/${subName}`);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
return logger;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/logging/index.ts
|
|
423
|
+
var log = {
|
|
424
|
+
bus: createSubsystemLogger("bus"),
|
|
425
|
+
agent: createSubsystemLogger("agent"),
|
|
426
|
+
session: createSubsystemLogger("session"),
|
|
427
|
+
subagent: createSubsystemLogger("subagent"),
|
|
428
|
+
web: createSubsystemLogger("web"),
|
|
429
|
+
ws: createSubsystemLogger("ws"),
|
|
430
|
+
hook: createSubsystemLogger("hook"),
|
|
431
|
+
task: createSubsystemLogger("task"),
|
|
432
|
+
memory: createSubsystemLogger("memory"),
|
|
433
|
+
cron: createSubsystemLogger("cron"),
|
|
434
|
+
notif: createSubsystemLogger("notif"),
|
|
435
|
+
usage: createSubsystemLogger("usage"),
|
|
436
|
+
heartbeat: createSubsystemLogger("heartbeat"),
|
|
437
|
+
git: createSubsystemLogger("git"),
|
|
438
|
+
browser: createSubsystemLogger("browser"),
|
|
439
|
+
audio: createSubsystemLogger("audio"),
|
|
440
|
+
stt: createSubsystemLogger("stt"),
|
|
441
|
+
obs: createSubsystemLogger("obs"),
|
|
442
|
+
calendar: createSubsystemLogger("calendar")
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/hooks/shared.ts
|
|
446
|
+
var TASK_DB_PATH = path3.join(TASKS_DIR, "tasks.sqlite");
|
|
447
|
+
function findClaudeSessionDir() {
|
|
448
|
+
try {
|
|
449
|
+
const projectsDir = path3.join(CLAUDE_HOME, "projects");
|
|
450
|
+
if (!fs3.existsSync(projectsDir)) return null;
|
|
451
|
+
const projects = fs3.readdirSync(projectsDir);
|
|
452
|
+
let latest = null;
|
|
453
|
+
for (const project of projects) {
|
|
454
|
+
const projectPath = path3.join(projectsDir, project);
|
|
455
|
+
const stat = fs3.statSync(projectPath);
|
|
456
|
+
if (!stat.isDirectory()) continue;
|
|
457
|
+
const files = fs3.readdirSync(projectPath);
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
const filePath = path3.join(projectPath, file);
|
|
460
|
+
const fstat = fs3.statSync(filePath);
|
|
461
|
+
if (fstat.mtime.getTime() > (latest?.mtime ?? 0)) {
|
|
462
|
+
latest = { dir: projectPath, mtime: fstat.mtime.getTime() };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return latest?.dir ?? null;
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function extractSessionSummary(sessionDir) {
|
|
472
|
+
const project = path3.basename(sessionDir);
|
|
473
|
+
const now = /* @__PURE__ */ new Date();
|
|
474
|
+
const dateStr = formatDate(now);
|
|
475
|
+
const conversationSnippet = readRecentConversation(sessionDir);
|
|
476
|
+
const summaryText = conversationSnippet || `Work session in ${project}`;
|
|
477
|
+
const slug = generateSlug(summaryText, dateStr);
|
|
478
|
+
return {
|
|
479
|
+
id: slug,
|
|
480
|
+
project,
|
|
481
|
+
slug,
|
|
482
|
+
summary: summaryText,
|
|
483
|
+
status: "completed",
|
|
484
|
+
date: dateStr,
|
|
485
|
+
task_ids: []
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function saveSessionSummary(summary, filesChanged = [], decisions = [], nextSteps = []) {
|
|
489
|
+
fs3.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
490
|
+
const filePath = path3.join(SESSIONS_DIR, `${summary.slug}.md`);
|
|
491
|
+
const taskLine = summary.task_ids.length > 0 ? `Task: ${summary.task_ids.join(", ")}` : "";
|
|
492
|
+
const filesSection = filesChanged.length > 0 ? `
|
|
493
|
+
## Files Changed
|
|
494
|
+
${filesChanged.map((f) => `- ${f}`).join("\n")}` : "";
|
|
495
|
+
const decisionsSection = decisions.length > 0 ? `
|
|
496
|
+
## Decisions
|
|
497
|
+
${decisions.map((d) => `- ${d}`).join("\n")}` : "";
|
|
498
|
+
const nextStepsSection = nextSteps.length > 0 ? `
|
|
499
|
+
## Next Steps
|
|
500
|
+
${nextSteps.map((n) => `- ${n}`).join("\n")}` : "";
|
|
501
|
+
const content = `# Session: ${summary.summary}
|
|
502
|
+
Date: ${summary.date}
|
|
503
|
+
Project: ${summary.project}
|
|
504
|
+
Status: ${summary.status}
|
|
505
|
+
${taskLine}
|
|
506
|
+
|
|
507
|
+
## Summary
|
|
508
|
+
${summary.summary}
|
|
509
|
+
${filesSection}
|
|
510
|
+
${decisionsSection}
|
|
511
|
+
${nextStepsSection}
|
|
512
|
+
`;
|
|
513
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
514
|
+
return filePath;
|
|
515
|
+
}
|
|
516
|
+
function updateTaskFromSession(taskId, summary) {
|
|
517
|
+
try {
|
|
518
|
+
const Database2 = __require("better-sqlite3");
|
|
519
|
+
if (!fs3.existsSync(TASK_DB_PATH)) return;
|
|
520
|
+
const db = new Database2(TASK_DB_PATH);
|
|
521
|
+
try {
|
|
522
|
+
db.pragma("journal_mode = WAL");
|
|
523
|
+
db.pragma("busy_timeout = 5000");
|
|
524
|
+
const escaped = taskId.replace(/[\\%_]/g, (ch) => "\\" + ch);
|
|
525
|
+
const row = db.prepare(
|
|
526
|
+
`SELECT id, phase, session_ids, note, description, summary, payload
|
|
527
|
+
FROM tasks
|
|
528
|
+
WHERE id = ? OR id LIKE ? ESCAPE '\\'
|
|
529
|
+
ORDER BY (id = ?) DESC
|
|
530
|
+
LIMIT 1`
|
|
531
|
+
).get(taskId, escaped + "%", taskId);
|
|
532
|
+
if (!row) return;
|
|
533
|
+
let sessionIds = [];
|
|
534
|
+
if (row.session_ids) {
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(row.session_ids);
|
|
537
|
+
if (Array.isArray(parsed)) sessionIds = parsed.filter((s) => typeof s === "string");
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
let payload = {};
|
|
542
|
+
if (row.payload) {
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(row.payload);
|
|
545
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
546
|
+
payload = parsed;
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const entry = `[${summary.date}] Session: ${summary.summary}`;
|
|
552
|
+
const currentNote = row.note ?? "";
|
|
553
|
+
const newNote = currentNote ? currentNote + "\n\n" + entry : entry;
|
|
554
|
+
let newPhase = row.phase;
|
|
555
|
+
if (row.phase === "TODO" || row.phase === "IN_PROGRESS") {
|
|
556
|
+
newPhase = "AGENT_COMPLETE";
|
|
557
|
+
}
|
|
558
|
+
if (!sessionIds.includes(summary.id)) {
|
|
559
|
+
sessionIds.push(summary.id);
|
|
560
|
+
}
|
|
561
|
+
let payloadDirty = false;
|
|
562
|
+
if (summary.status === "completed") {
|
|
563
|
+
for (const slot of ["plan_session_id", "exec_session_id", "session_id"]) {
|
|
564
|
+
if (payload[slot] === summary.id) {
|
|
565
|
+
delete payload[slot];
|
|
566
|
+
payloadDirty = true;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if ("active_session_ids" in payload) {
|
|
571
|
+
delete payload.active_session_ids;
|
|
572
|
+
payloadDirty = true;
|
|
573
|
+
}
|
|
574
|
+
if ("active_session_id" in payload) {
|
|
575
|
+
delete payload.active_session_id;
|
|
576
|
+
payloadDirty = true;
|
|
577
|
+
}
|
|
578
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
579
|
+
const sessionIdsJson = JSON.stringify(sessionIds);
|
|
580
|
+
const payloadJson = payloadDirty ? JSON.stringify(payload) : null;
|
|
581
|
+
const updateWithPayload = db.prepare(
|
|
582
|
+
`UPDATE tasks SET
|
|
583
|
+
note = @note,
|
|
584
|
+
phase = @phase,
|
|
585
|
+
session_ids = @session_ids,
|
|
586
|
+
updated_at = @updated_at,
|
|
587
|
+
payload = @payload
|
|
588
|
+
WHERE id = @id`
|
|
589
|
+
);
|
|
590
|
+
const updateWithoutPayload = db.prepare(
|
|
591
|
+
`UPDATE tasks SET
|
|
592
|
+
note = @note,
|
|
593
|
+
phase = @phase,
|
|
594
|
+
session_ids = @session_ids,
|
|
595
|
+
updated_at = @updated_at
|
|
596
|
+
WHERE id = @id`
|
|
597
|
+
);
|
|
598
|
+
const tx = db.transaction(() => {
|
|
599
|
+
if (payloadDirty) {
|
|
600
|
+
updateWithPayload.run({
|
|
601
|
+
id: row.id,
|
|
602
|
+
note: newNote,
|
|
603
|
+
phase: newPhase,
|
|
604
|
+
session_ids: sessionIdsJson,
|
|
605
|
+
updated_at: updatedAt,
|
|
606
|
+
payload: payloadJson
|
|
607
|
+
});
|
|
608
|
+
} else {
|
|
609
|
+
updateWithoutPayload.run({
|
|
610
|
+
id: row.id,
|
|
611
|
+
note: newNote,
|
|
612
|
+
phase: newPhase,
|
|
613
|
+
session_ids: sessionIdsJson,
|
|
614
|
+
updated_at: updatedAt
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
tx();
|
|
619
|
+
} finally {
|
|
620
|
+
try {
|
|
621
|
+
db.close();
|
|
622
|
+
} catch {
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function logHookError(context, error) {
|
|
629
|
+
try {
|
|
630
|
+
fs3.mkdirSync(path3.dirname(HOOK_LOG_FILE), { recursive: true });
|
|
631
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
632
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${context}: ${msg}
|
|
633
|
+
`;
|
|
634
|
+
fs3.appendFileSync(HOOK_LOG_FILE, line, "utf-8");
|
|
635
|
+
log.hook.error(`${context}: ${msg}`, {
|
|
636
|
+
context,
|
|
637
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
638
|
+
});
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function formatDailyLogEntry(summary, filesChanged) {
|
|
643
|
+
let entry = summary.summary;
|
|
644
|
+
if (filesChanged && filesChanged.length > 0) {
|
|
645
|
+
entry += `
|
|
646
|
+
Files: ${filesChanged.join(", ")}`;
|
|
647
|
+
}
|
|
648
|
+
return entry;
|
|
649
|
+
}
|
|
650
|
+
function deriveProjectPath(taskId) {
|
|
651
|
+
try {
|
|
652
|
+
if (!fs3.existsSync(TASK_DB_PATH)) return null;
|
|
653
|
+
const Database2 = __require("better-sqlite3");
|
|
654
|
+
const db = new Database2(TASK_DB_PATH, { readonly: true, fileMustExist: true });
|
|
655
|
+
try {
|
|
656
|
+
const escaped = taskId.replace(/[\\%_]/g, (ch) => "\\" + ch);
|
|
657
|
+
const row = db.prepare(
|
|
658
|
+
`SELECT project
|
|
659
|
+
FROM tasks
|
|
660
|
+
WHERE id = ? OR id LIKE ? ESCAPE '\\'
|
|
661
|
+
ORDER BY (id = ?) DESC
|
|
662
|
+
LIMIT 1`
|
|
663
|
+
).get(taskId, escaped + "%", taskId);
|
|
664
|
+
if (!row) return null;
|
|
665
|
+
return row.project?.trim() || null;
|
|
666
|
+
} finally {
|
|
667
|
+
try {
|
|
668
|
+
db.close();
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function readRecentConversation(sessionDir) {
|
|
677
|
+
try {
|
|
678
|
+
const files = fs3.readdirSync(sessionDir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
679
|
+
if (files.length === 0) return "";
|
|
680
|
+
const content = fs3.readFileSync(path3.join(sessionDir, files[0]), "utf-8");
|
|
681
|
+
const data = JSON.parse(content);
|
|
682
|
+
if (typeof data === "object" && data !== null) {
|
|
683
|
+
if (typeof data.summary === "string") return data.summary;
|
|
684
|
+
if (typeof data.title === "string") return data.title;
|
|
685
|
+
if (typeof data.description === "string") return data.description;
|
|
686
|
+
}
|
|
687
|
+
return "";
|
|
688
|
+
} catch {
|
|
689
|
+
return "";
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function formatDate(d) {
|
|
693
|
+
const year = d.getFullYear();
|
|
694
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
695
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
696
|
+
const hours = String(d.getHours()).padStart(2, "0");
|
|
697
|
+
const minutes = String(d.getMinutes()).padStart(2, "0");
|
|
698
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
699
|
+
}
|
|
700
|
+
function generateSlug(text, dateStr) {
|
|
701
|
+
const dateSlug = dateStr.replace(/[: ]/g, "-").replace(/--+/g, "-");
|
|
702
|
+
const textSlug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
703
|
+
return `${dateSlug}-${textSlug}`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/core/daily-log.ts
|
|
707
|
+
import fs5 from "fs";
|
|
708
|
+
import fsp4 from "fs/promises";
|
|
709
|
+
import path6 from "path";
|
|
710
|
+
|
|
711
|
+
// src/core/memory-safety-patterns-cjk.ts
|
|
712
|
+
var CJK_NEGATION_SOURCE = "\u4E0D\u8981|\u4E0D\u5F97|\u4E0D\u53EF|\u4E0D\u51C6|\u4E0D\u8BB8|\u4E0D\u80FD|\u4E0D\u5E94|\u4E0D\u8BE5|\u522B|\u52FF|\u7981\u6B62|\u4E25\u7981|\u907F\u514D|\u62D2\u7EDD|\u7EDD\u4E0D|\u4ECE\u4E0D|\u6C38\u4E0D|\u675C\u7EDD|\u65E0\u9700|\u4E0D\u9700\u8981";
|
|
713
|
+
var CJK_CLAUSE_BOUNDARY_CHARS = "\u3002\uFF0C\u3001\uFF1B\uFF1A\uFF01\uFF1F";
|
|
714
|
+
var zin = (n) => `[^\u3002\uFF01\uFF1F\uFF1B!?\\n]{0,${n}}?`;
|
|
715
|
+
var Z_SCOPE = "\u4E4B\u524D|\u4EE5\u524D|\u4E0A\u9762|\u4E0A\u8FF0|\u5148\u524D|\u524D\u9762|\u539F\u6709|\u539F\u672C|\u6700\u521D|\u521D\u59CB|\u7CFB\u7EDF|\u5F00\u53D1\u8005|\u64CD\u4F5C\u5458|\u64CD\u4F5C\u8005|\u4F60\u7684|previous|prior|earlier|system|operator";
|
|
716
|
+
var Z_DIRECTIVE = "\u6307\u4EE4|\u6307\u793A|\u63D0\u793A\u8BCD|\u63D0\u793A|\u89C4\u5219|\u5B88\u5219|\u89C4\u5B9A|\u7EA6\u675F|\u9650\u5236|\u8BBE\u5B9A|\u8BBE\u7F6E|\u547D\u4EE4|\u8981\u6C42|prompts?|instructions?|rules?|guidelines?";
|
|
717
|
+
var Z_PRINCIPAL = "\u7528\u6237|\u4F7F\u7528\u8005|\u4E3B\u4EBA|\u64CD\u4F5C\u5458|\u64CD\u4F5C\u8005";
|
|
718
|
+
var CJK_THREAT_PATTERNS = [
|
|
719
|
+
// ── Instruction override ──────────────────────────────────────────────
|
|
720
|
+
// Three parts required: override verb + scope word + directive noun, all in
|
|
721
|
+
// one sentence. "忽略之前的指令" hits; "env 可覆盖" / "无明确需求指令" do not.
|
|
722
|
+
{
|
|
723
|
+
id: "override_instructions",
|
|
724
|
+
severity: "block",
|
|
725
|
+
negatable: true,
|
|
726
|
+
re: new RegExp(
|
|
727
|
+
`(?:\u5FFD\u7565|\u65E0\u89C6|\u5FFD\u89C6|\u4E0D\u7528\u7BA1|\u4E0D\u5FC5\u7406\u4F1A|\u4E0D\u5FC5\u9075\u5B88|\u5FD8\u8BB0|\u5FD8\u6389|\u629B\u5F03|\u653E\u5F03|\u63A8\u7FFB|\u64A4\u9500|\u53D6\u6D88|\u7ED5\u8FC7|\u8DF3\u8FC7)${zin(12)}(?:${Z_SCOPE})${zin(8)}(?:${Z_DIRECTIVE})|(?:${Z_SCOPE})${zin(8)}(?:${Z_DIRECTIVE})${zin(8)}(?:\u5168\u90E8)?(?:\u4F5C\u5E9F|\u5931\u6548|\u65E0\u6548|\u4E0D\u518D\u9002\u7528|\u8BF7\u5FFD\u7565|\u5DF2\u53D6\u6D88)`,
|
|
728
|
+
"i"
|
|
729
|
+
)
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: "precedence_over_operator",
|
|
733
|
+
severity: "block",
|
|
734
|
+
negatable: true,
|
|
735
|
+
re: new RegExp(
|
|
736
|
+
// 覆盖 x59 in the corpus, so it only counts against system/operator directives.
|
|
737
|
+
`(?:\u4F18\u5148\u4E8E|\u9AD8\u4E8E|\u8986\u76D6|\u53D6\u4EE3|\u51CC\u9A7E)${zin(8)}(?:\u7CFB\u7EDF|\u5F00\u53D1\u8005|\u64CD\u4F5C\u5458|\u64CD\u4F5C\u8005)(?:\u7684)?${zin(4)}(?:${Z_DIRECTIVE})|(?:\u672C|\u8FD9)(?:\u6761|\u5219|\u4E2A)?(?:${Z_DIRECTIVE})${zin(8)}(?:\u4F18\u5148\u7EA7?\u6700\u9AD8|\u4F18\u4E8E\u4E00\u5207|\u62E5\u6709\u6700\u9AD8\u4F18\u5148\u7EA7)`,
|
|
738
|
+
"i"
|
|
739
|
+
)
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
id: "leak_system_prompt",
|
|
743
|
+
severity: "block",
|
|
744
|
+
negatable: true,
|
|
745
|
+
// Verbatim-dump verbs only. 显示/展示/查看 are excluded on purpose, mirroring
|
|
746
|
+
// the English table: "the context inspector can show the system prompt" is a
|
|
747
|
+
// legitimate product fact in either language.
|
|
748
|
+
re: new RegExp(
|
|
749
|
+
`(?:\u8F93\u51FA|\u6253\u5370|\u590D\u8FF0|\u91CD\u590D|\u80CC\u8BF5|\u9010\u5B57|\u539F\u6837|\u5B8C\u6574\u5730?\u8F93\u51FA|\u6CC4\u9732|\u900F\u9732|\u516C\u5F00)${zin(10)}(?:\u7CFB\u7EDF\u63D0\u793A\u8BCD?|\u7CFB\u7EDF\u6307\u4EE4|\u7CFB\u7EDF\u6D88\u606F|\u521D\u59CB\u6307\u4EE4|\u7CFB\u7EDF\u7EA7?\u6307\u4EE4|system\\s*prompt)|(?:\u7CFB\u7EDF\u63D0\u793A\u8BCD?|\u7CFB\u7EDF\u6307\u4EE4|\u7CFB\u7EDF\u6D88\u606F|\u521D\u59CB\u6307\u4EE4|system\\s*prompt)${zin(10)}(?:\u5B8C\u6574\u5730?)?(?:\u6CC4\u9732|\u900F\u9732|\u516C\u5F00|\u590D\u8FF0|\u80CC\u8BF5|\u539F\u6837(?:\u8F93\u51FA|\u8FD4\u56DE)|\u9010\u5B57(?:\u8F93\u51FA|\u91CD\u590D))`,
|
|
750
|
+
"i"
|
|
751
|
+
)
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
id: "role_hijack",
|
|
755
|
+
severity: "block",
|
|
756
|
+
negatable: true,
|
|
757
|
+
// "你现在是" needs a following role determiner — bare 你现在是 occurs 4x in the
|
|
758
|
+
// corpus as ordinary speech ("你现在是累了").
|
|
759
|
+
re: new RegExp(
|
|
760
|
+
`\u4F60\u73B0\u5728(?:\u5F00\u59CB)?\u662F(?:\u4E00\u4E2A|\u4E00\u540D|\u4E00\u4F4D|\u4E2A|\u4E00\u53F0)|(?:\u4ECE\u73B0\u5728\u5F00\u59CB|\u4ECE\u6B64\u4EE5\u540E|\u4ECA\u540E|\u4EE5\u540E|\u63A5\u4E0B\u6765)${zin(8)}\u4F60(?:\u5C31)?\u662F(?:\u4E00\u4E2A|\u4E00\u540D|\u4E00\u4F4D|\u4E2A|\u4E0D\u518D)|\u5047\u88C5(?:\u4F60\u662F|\u81EA\u5DF1\u662F|\u6210\u4E3A|\u4F60\u73B0\u5728\u662F)|\u626E\u6F14(?:\u4E00\u4E2A|\u4E00\u540D|\u4E00\u4F4D|\u89D2\u8272)|\u4F60\u7684(?:\u771F\u5B9E|\u771F\u6B63|\u5B9E\u9645)(?:\u8EAB\u4EFD|\u540D\u5B57|\u540D\u79F0|\u76EE\u7684|\u4F7F\u547D)\u662F`,
|
|
761
|
+
"i"
|
|
762
|
+
)
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
id: "unrestricted_mode",
|
|
766
|
+
severity: "block",
|
|
767
|
+
negatable: true,
|
|
768
|
+
re: new RegExp(
|
|
769
|
+
`\u4E0D\u53D7(?:\u4EFB\u4F55)?(?:\u9650\u5236|\u7EA6\u675F|\u89C4\u5219|\u8FC7\u6EE4|\u5BA1\u67E5|\u5B89\u5168)|(?:\u65E0\u89C6|\u5FFD\u7565|\u7ED5\u8FC7|\u5173\u95ED|\u89E3\u9664)(?:\u6240\u6709|\u5168\u90E8|\u4E00\u5207)?(?:\u5B89\u5168)?(?:\u9650\u5236|\u7EA6\u675F|\u9632\u62A4|\u62A4\u680F|\u8FC7\u6EE4|\u5BA1\u67E5)|(?:\u8D8A\u72F1|\u5F00\u53D1\u8005|\u4E0A\u5E1D|\u65E0\u9650\u5236|\u81EA\u7531)\u6A21\u5F0F(?:\u5DF2)?(?:\u5F00\u542F|\u542F\u7528|\u6FC0\u6D3B|\u6253\u5F00)`,
|
|
770
|
+
"i"
|
|
771
|
+
)
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
id: "spoofed_authority",
|
|
775
|
+
severity: "block",
|
|
776
|
+
negatable: true,
|
|
777
|
+
re: new RegExp(
|
|
778
|
+
`(?:\u8FD9\u6761|\u8FD9\u6BB5|\u4EE5\u4E0B|\u4E0B\u9762\u7684?|\u63A5\u4E0B\u6765\u7684?)(?:\u6D88\u606F|\u4FE1\u606F|\u5185\u5BB9|\u6307\u4EE4|\u8BF4\u660E|\u6587\u5B57)?(?:\u662F)?(?:\u6765\u81EA|\u51FA\u81EA|\u7531)(?:\u7CFB\u7EDF|\u5F00\u53D1\u8005|\u64CD\u4F5C\u5458|\u7BA1\u7406\u5458|\u6A21\u578B\u63D0\u4F9B\u65B9|\u5B98\u65B9)|\u4F60\u5DF2(?:\u7ECF)?\u88AB(?:\u5347\u7EA7|\u66F4\u65B0|\u91CD\u65B0\u914D\u7F6E|\u91CD\u65B0\u8BBE\u5B9A|\u6253\u8FC7?\u8865\u4E01|\u6388\u6743)`,
|
|
779
|
+
"i"
|
|
780
|
+
)
|
|
781
|
+
},
|
|
782
|
+
// ── Concealment / covert channel ──────────────────────────────────────
|
|
783
|
+
// A REFLEXIVE object is required. "不要告诉用户任务已完成除非测试通过" is a
|
|
784
|
+
// legitimate rule and must stay clean; only hiding the action itself counts.
|
|
785
|
+
{
|
|
786
|
+
id: "conceal_from_user",
|
|
787
|
+
severity: "block",
|
|
788
|
+
negatable: true,
|
|
789
|
+
re: new RegExp(
|
|
790
|
+
`(?:\u4E0D\u8981|\u4E0D\u5F97|\u4E0D\u8BB8|\u4E0D\u51C6|\u522B|\u52FF|\u7981\u6B62|\u907F\u514D)${zin(8)}(?:\u544A\u8BC9|\u544A\u77E5|\u901A\u77E5|\u63D0\u9192|\u63D0\u53CA|\u6C47\u62A5|\u4E0A\u62A5)${zin(6)}(?:${Z_PRINCIPAL})${zin(2)}(?:\u8FD9\u4EF6\u4E8B|\u6B64\u4E8B|\u8FD9\u4E00\u70B9|\u8FD9\u4E2A(?:\u64CD\u4F5C|\u6B65\u9AA4|\u884C\u4E3A|\u8FC7\u7A0B|\u4E8B\u60C5?)|\u4F60(?:\u505A|\u8BFB|\u770B|\u8BBF\u95EE|\u53D1\u9001|\u6267\u884C|\u8FD0\u884C|\u4FEE\u6539)\u4E86)|(?:\u9690\u7792|\u7792\u7740|\u7792\u8FC7|\u6B3A\u9A97)${zin(10)}(?:${Z_PRINCIPAL})|\u4E0D(?:\u8981|\u5F97|\u8BB8|\u51C6)?\u8BA9(?:${Z_PRINCIPAL})(?:\u77E5\u9053|\u53D1\u73B0|\u770B\u5230|\u5BDF\u89C9|\u610F\u8BC6\u5230)|(?:\u6084\u6084|\u5077\u5077|\u6697\u4E2D|\u79C1\u4E0B|\u79D8\u5BC6)\u5730?${zin(6)}(?:\u53D1\u9001|\u4E0A\u4F20|\u8F6C\u53D1|\u590D\u5236|\u5916\u53D1|\u6267\u884C|\u8FD0\u884C|\u5199\u5165)|(?:\u8FD9\u4EF6\u4E8B|\u6B64\u4E8B|\u8FD9\u4E2A\u64CD\u4F5C)(?:\u8981|\u9700\u8981)?(?:\u4FDD\u5BC6|\u522B\u8BF4\u51FA\u53BB|\u4E0D\u8981\u8BF4\u51FA\u53BB)`,
|
|
791
|
+
"i"
|
|
792
|
+
)
|
|
793
|
+
},
|
|
794
|
+
// ── Exfiltration ──────────────────────────────────────────────────────
|
|
795
|
+
// All three parts required in ONE sentence: a send verb, a sensitive object,
|
|
796
|
+
// and an outbound sink. 凭证 alone appears 6x legitimately ("凭证过期"), and
|
|
797
|
+
// this product legitimately documents an https:// companion that receives
|
|
798
|
+
// conversation traffic, so no single part is enough.
|
|
799
|
+
// Both word orders are accepted because Chinese fronts the object with 把/将
|
|
800
|
+
// ("把对话记录发送到 X") as readily as it trails it ("发送对话记录到 X").
|
|
801
|
+
{
|
|
802
|
+
id: "exfil_to_endpoint",
|
|
803
|
+
severity: "block",
|
|
804
|
+
negatable: true,
|
|
805
|
+
re: (() => {
|
|
806
|
+
const VERB = "\u53D1\u9001|\u4E0A\u4F20|\u4F20\u9001|\u8F6C\u53D1|\u63A8\u9001|\u4E0A\u62A5|\u5916\u53D1|\u6CC4\u9732|\u590D\u5236|\u5907\u4EFD|\u540C\u6B65";
|
|
807
|
+
const OBJ = "\u5BF9\u8BDD(?:\u5185\u5BB9|\u8BB0\u5F55)?|\u804A\u5929\u8BB0\u5F55|\u4F1A\u8BDD\u8BB0\u5F55|\u6D88\u606F\u8BB0\u5F55|\u5386\u53F2\u8BB0\u5F55|\u8BB0\u5FC6\u6587\u4EF6|\u7CFB\u7EDF\u63D0\u793A\u8BCD?|\u51ED\u8BC1|\u5BC6\u94A5|\u4EE4\u724C|\u5BC6\u7801|\u73AF\u5883\u53D8\u91CF|\u79C1\u94A5|\u8BBF\u95EE\u5BC6\u94A5|API\\s*\u5BC6\u94A5";
|
|
808
|
+
const SINK = `(?:\u5230|\u81F3|\u53D1\u5F80|\u4F20\u5230|\u4E0A\u4F20\u81F3|\u7ED9)${zin(30)}(?:https?:\\/\\/|webhook|\u6536\u96C6\u5668|\u91C7\u96C6\u5668|\u7AEF\u70B9|\u5916\u90E8\u670D\u52A1\u5668)`;
|
|
809
|
+
return new RegExp(
|
|
810
|
+
`(?:${VERB})${zin(60)}(?:${OBJ})${zin(60)}${SINK}|(?:${OBJ})${zin(60)}(?:${VERB})${zin(20)}${SINK}`,
|
|
811
|
+
"i"
|
|
812
|
+
);
|
|
813
|
+
})()
|
|
814
|
+
},
|
|
815
|
+
// ── Flag-only: observable, never blocking ─────────────────────────────
|
|
816
|
+
{
|
|
817
|
+
id: "conversation_dump",
|
|
818
|
+
severity: "flag",
|
|
819
|
+
re: new RegExp(
|
|
820
|
+
`(?:\u5BFC\u51FA|\u8F6C\u50A8|\u4E0A\u4F20|\u5206\u4EAB|\u53D1\u9001)${zin(8)}(?:\u5B8C\u6574|\u5168\u90E8|\u6574\u4E2A|\u6240\u6709)(?:\u7684)?(?:\u5BF9\u8BDD|\u804A\u5929\u8BB0\u5F55|\u4E0A\u4E0B\u6587|\u4F1A\u8BDD\u5386\u53F2)`,
|
|
821
|
+
"i"
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
];
|
|
825
|
+
|
|
826
|
+
// src/core/memory-safety-patterns.ts
|
|
827
|
+
var gap = (n) => `(?:\\s+\\S+){0,${n}}\\s+`;
|
|
828
|
+
var SENT = "(?:[^.!?\\n]|[.!?](?![\\s]|$))";
|
|
829
|
+
var within = (n) => `${SENT}{0,${n}}?`;
|
|
830
|
+
var SCOPE = "(?:previous|prior|preceding|earlier|above|foregoing|original|initial|system|operator|developer|your)";
|
|
831
|
+
var DIRECTIVE = "(?:instructions?|prompts?|directives?|guidelines?|rules?|constraints?|policy|policies)";
|
|
832
|
+
var PRINCIPAL = "(?:user|operator|human|owner)";
|
|
833
|
+
var EN_THREAT_PATTERNS = [
|
|
834
|
+
// ── Instruction override ──────────────────────────────────────────────
|
|
835
|
+
{
|
|
836
|
+
id: "override_instructions",
|
|
837
|
+
severity: "block",
|
|
838
|
+
negatable: true,
|
|
839
|
+
re: new RegExp(
|
|
840
|
+
// "ignore all previous instructions" — scope word before the directive noun
|
|
841
|
+
`\\b(?:ignore|disregard|forget|discard|override|bypass|nullify|abandon)\\b${gap(4)}\\b${SCOPE}\\b${gap(2)}\\b${DIRECTIVE}\\b|\\b(?:ignore|disregard|forget|discard|override|bypass|nullify)\\b${gap(3)}\\b${DIRECTIVE}\\b${gap(2)}\\b(?:above|before|previously|earlier)\\b|\\b(?:ignore|disregard|forget)\\s+(?:everything|anything|all)\\b${gap(3)}\\b(?:above|before|previously|earlier|told|instructed)\\b`,
|
|
842
|
+
"i"
|
|
843
|
+
)
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
id: "precedence_over_operator",
|
|
847
|
+
severity: "block",
|
|
848
|
+
negatable: true,
|
|
849
|
+
re: new RegExp(
|
|
850
|
+
`\\b(?:overrides?|supersedes?|outranks?|takes?\\s+precedence\\s+over|has\\s+priority\\s+over)\\b${gap(3)}\\b(?:the\\s+|your\\s+)?(?:operator|system|developer)(?:'s|s')?\\s+${DIRECTIVE}\\b|\\bthese?\\s+${DIRECTIVE}\\s+(?:take|takes|have|has)\\s+(?:precedence|priority)\\b`,
|
|
851
|
+
"i"
|
|
852
|
+
)
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "leak_system_prompt",
|
|
856
|
+
severity: "block",
|
|
857
|
+
negatable: true,
|
|
858
|
+
// Verbatim-dump verbs only. "The context inspector can show the system
|
|
859
|
+
// prompt" is a legitimate memory entry and must stay clean.
|
|
860
|
+
re: new RegExp(
|
|
861
|
+
`\\b(?:reveal|disclose|leak|exfiltrate)\\b${gap(3)}\\b(?:system\\s+(?:prompt|message|instructions))\\b|\\b(?:output|print|repeat|echo|dump|reproduce|recite)\\b${gap(3)}\\b(?:system\\s+(?:prompt|message|instructions)|initial\\s+instructions)\\b`,
|
|
862
|
+
"i"
|
|
863
|
+
)
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
id: "role_hijack",
|
|
867
|
+
severity: "block",
|
|
868
|
+
negatable: true,
|
|
869
|
+
re: new RegExp(
|
|
870
|
+
`\\byou\\s+are\\s+now\\s+(?:a|an|the)\\b|\\b(?:from\\s+now\\s+on|henceforth|going\\s+forward)\\b${gap(2)}\\byou\\s+are\\s+(?:a|an|the|no\\s+longer)\\b|\\bpretend\\s+(?:that\\s+)?(?:you\\s+are|to\\s+be)\\b|\\byour\\s+(?:real|true|actual)\\s+(?:name|identity|purpose)\\s+is\\b`,
|
|
871
|
+
"i"
|
|
872
|
+
)
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: "unrestricted_mode",
|
|
876
|
+
severity: "block",
|
|
877
|
+
negatable: true,
|
|
878
|
+
re: new RegExp(
|
|
879
|
+
`\\bact\\s+as\\s+(?:if|though)\\b${gap(4)}\\b(?:no|without)\\b${gap(2)}\\b(?:restrictions?|limits?|rules?|guidelines?|filters?)\\b|\\b(?:respond|reply|answer|behave|operate)\\s+without\\s+(?:any\\s+)?(?:restrictions?|limitations?|filters?|safety|guardrails)\\b|\\b(?:developer|god|jailbreak|unrestricted|DAN)\\s+mode\\s+(?:is\\s+)?(?:on|enabled|activated)\\b`,
|
|
880
|
+
"i"
|
|
881
|
+
)
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: "spoofed_authority",
|
|
885
|
+
severity: "block",
|
|
886
|
+
negatable: true,
|
|
887
|
+
re: new RegExp(
|
|
888
|
+
`\\b(?:this\\s+(?:is|message\\s+is)|the\\s+following\\s+is)\\b${gap(3)}\\b(?:from|by)\\s+(?:the\\s+|your\\s+)?(?:system|developer|operator|administrator|model\\s+provider)\\b|\\byou\\s+have\\s+been\\s+(?:updated|upgraded|patched|reconfigured)\\s+to\\b`,
|
|
889
|
+
"i"
|
|
890
|
+
)
|
|
891
|
+
},
|
|
892
|
+
// ── Concealment / covert channel ──────────────────────────────────────
|
|
893
|
+
// A reflexive continuation is REQUIRED: an attacker conceals the attack
|
|
894
|
+
// ("about this", "that you ..."), while legitimate rules concern the user's
|
|
895
|
+
// work ("never tell the user a task is done while a test is red").
|
|
896
|
+
{
|
|
897
|
+
id: "conceal_from_user",
|
|
898
|
+
severity: "block",
|
|
899
|
+
negatable: true,
|
|
900
|
+
re: new RegExp(
|
|
901
|
+
`\\b(?:do\\s+not|don'?t|never|avoid|refrain\\s+from)\\b${gap(3)}\\b(?:tell|telling|inform|informing|notify|notifying|alert|alerting)\\b${gap(2)}\\b(?:the\\s+)?${PRINCIPAL}\\b\\s+(?:(?:about|regarding|of)\\s+(?:this|that|it|any|the\\b)|that\\s+you\\b|what\\s+you\\b|anything\\b)|\\b(?:do\\s+not|don'?t|never|avoid)\\b${gap(3)}\\bmention\\b${gap(4)}\\bto\\s+(?:the\\s+)?${PRINCIPAL}\\b|\\b(?:hide|conceal|withhold|suppress|obscure)\\b${within(60)}\\bfrom\\s+(?:the\\s+)?${PRINCIPAL}\\b|\\bwithout\\s+(?:telling|informing|notifying|alerting)\\s+(?:the\\s+)?${PRINCIPAL}\\b|\\bkeep\\s+(?:this|it|that)\\s+(?:a\\s+)?secret\\b|\\b(?:silently|covertly|secretly)\\s+(?:send|post|upload|forward|copy|exfiltrate)\\b`,
|
|
902
|
+
"i"
|
|
903
|
+
)
|
|
904
|
+
},
|
|
905
|
+
// ── Hidden markup ─────────────────────────────────────────────────────
|
|
906
|
+
{
|
|
907
|
+
id: "html_comment_injection",
|
|
908
|
+
severity: "block",
|
|
909
|
+
re: /<!--[\s\S]{0,300}?(?:ignore|override|system\s+prompt|developer\s+mode|do\s+not\s+tell|jailbreak)[\s\S]{0,300}?-->/i
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: "hidden_markup",
|
|
913
|
+
severity: "block",
|
|
914
|
+
re: /<[^>\n]{0,200}style\s*=\s*["'][^"'\n]{0,200}(?:display\s*:\s*none|font-size\s*:\s*0|visibility\s*:\s*hidden)/i
|
|
915
|
+
},
|
|
916
|
+
// ── Exfiltration ──────────────────────────────────────────────────────
|
|
917
|
+
// Send verb + sensitive object + outbound sink, all in ONE sentence and in
|
|
918
|
+
// order. Any one alone is ordinary text here: this product legitimately
|
|
919
|
+
// documents a cloud companion at an https:// URL that receives conversation
|
|
920
|
+
// traffic, so only the full three-part shape is a hit.
|
|
921
|
+
{
|
|
922
|
+
id: "exfil_to_endpoint",
|
|
923
|
+
severity: "block",
|
|
924
|
+
negatable: true,
|
|
925
|
+
re: new RegExp(
|
|
926
|
+
`\\b(?:send|sends|post|posts|upload|uploads|transmit|transmits|forward|forwards|leak|leaks|exfiltrate|mirror|beacon|copy|copies)\\b` + within(60) + `\\b(?:conversation|chat\\s+history|message\\s+history|memory\\s+file|system\\s+prompt|credentials?|secrets?|api\\s*keys?|access\\s*keys?|auth\\s+tokens?|passwords?|env(?:ironment)?\\s+(?:vars?|variables?)|\\.env|ssh\\s+keys?|private\\s+key)\\b` + within(80) + `\\b(?:to|at|into|toward)\\b${within(40)}(?:https?:\\/\\/|\\bwebhook\\b|\\bcollector\\b|\\bendpoint\\b)`,
|
|
927
|
+
"i"
|
|
928
|
+
)
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
id: "exfil_shell_secret",
|
|
932
|
+
severity: "block",
|
|
933
|
+
re: /\b(?:curl|wget|httpie|nc|ncat|Invoke-WebRequest|iwr)\b[^\n]{0,140}\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)/i
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
id: "read_secret_files",
|
|
937
|
+
severity: "block",
|
|
938
|
+
negatable: true,
|
|
939
|
+
re: /\b(?:cat|strings|base64|xxd)\b[^\n]{0,80}(?:\.env\b|\/credentials\b|\.netrc\b|\.pgpass\b|\.npmrc\b|id_rsa\b|id_ed25519\b)/i
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
id: "ssh_backdoor_write",
|
|
943
|
+
severity: "block",
|
|
944
|
+
negatable: true,
|
|
945
|
+
re: new RegExp(
|
|
946
|
+
`\\b(?:append|add|write|echo|install|inject|copy|register)\\b${within(80)}authorized_keys|authorized_keys${within(40)}>>`,
|
|
947
|
+
"i"
|
|
948
|
+
)
|
|
949
|
+
},
|
|
950
|
+
// ── Credential-looking strings ────────────────────────────────────────
|
|
951
|
+
// Memory is injected every turn AND rides the data-repo sync plane, so a real
|
|
952
|
+
// secret parked here is both prompt bloat and a leak. Shape-anchored, so
|
|
953
|
+
// "api_key: <from keychain>" and "set the token from the credential helper"
|
|
954
|
+
// stay clean.
|
|
955
|
+
{
|
|
956
|
+
id: "hardcoded_secret",
|
|
957
|
+
severity: "block",
|
|
958
|
+
re: new RegExp(
|
|
959
|
+
`(?:api[_-]?key|secret|token|password|passwd|access[_-]?key|private[_-]?key|bearer)\\s*[:=]\\s*["'\`]?[A-Za-z0-9+/=_-]{20,}|\\b(?:sk|rk)-[A-Za-z0-9_-]{24,}|\\bgh[pousr]_[A-Za-z0-9]{20,}|\\bgithub_pat_[A-Za-z0-9_]{20,}|\\bAKIA[0-9A-Z]{16}\\b|\\bxox[baprs]-[A-Za-z0-9-]{12,}|-----BEGIN\\s+(?:[A-Z]+\\s+)?PRIVATE KEY-----`,
|
|
960
|
+
"i"
|
|
961
|
+
)
|
|
962
|
+
},
|
|
963
|
+
// ── Flag-only: observable, never blocking ─────────────────────────────
|
|
964
|
+
// These are the patterns whose false-positive risk is real (security notes,
|
|
965
|
+
// ops runbooks, legitimate config guidance), so they log and nothing else.
|
|
966
|
+
{
|
|
967
|
+
id: "c2_vocabulary",
|
|
968
|
+
severity: "flag",
|
|
969
|
+
re: /\bcommand\s+and\s+control\b|\bc2\s+(?:server|channel|beacon|infrastructure)\b|\b(?:cobalt\s*strike|metasploit|sliver\s+implant|mythic\s+agent)\b/i
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
id: "conversation_dump",
|
|
973
|
+
severity: "flag",
|
|
974
|
+
re: new RegExp(
|
|
975
|
+
`\\b(?:dump|exfiltrate|upload|share)\\b${gap(3)}\\b(?:full|entire|whole|complete)\\s+(?:conversation|chat\\s+history|context)\\b`,
|
|
976
|
+
"i"
|
|
977
|
+
)
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
id: "turn_delimiter_spoof",
|
|
981
|
+
severity: "flag",
|
|
982
|
+
re: /(?:^|\n)\s*(?:Human|Assistant)\s*:\s|<\/?(?:system|human|assistant)>/i
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "ssh_path_mention",
|
|
986
|
+
severity: "flag",
|
|
987
|
+
re: /~\/\.ssh\b|\$HOME\/\.ssh\b|authorized_keys/i
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
id: "agent_config_write",
|
|
991
|
+
severity: "flag",
|
|
992
|
+
re: /\b(?:append|write|modify|edit|patch|overwrite)\b[^\n]{0,80}(?:CLAUDE\.md|AGENTS\.md|\.cursorrules|\.clinerules|settings\.local\.json)/i
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
id: "env_unset_agent",
|
|
996
|
+
severity: "flag",
|
|
997
|
+
re: /\bunset\s+\w*(?:CLAUDE|ANTHROPIC|OPENAI|WALNUT)\w*/i
|
|
998
|
+
}
|
|
999
|
+
];
|
|
1000
|
+
var THREAT_PATTERNS = [...EN_THREAT_PATTERNS, ...CJK_THREAT_PATTERNS];
|
|
1001
|
+
var NEGATION_RE = new RegExp(
|
|
1002
|
+
`\\b(?:never|not|avoid|refuse|refrain|prohibited|forbidden)\\b|n['\u2019]t\\b|(?:${CJK_NEGATION_SOURCE})`,
|
|
1003
|
+
"i"
|
|
1004
|
+
);
|
|
1005
|
+
var CLAUSE_BOUNDARY_RE = new RegExp(`[.!?;:\\n${CJK_CLAUSE_BOUNDARY_CHARS}]`, "g");
|
|
1006
|
+
|
|
1007
|
+
// src/core/daily-log.ts
|
|
1008
|
+
import { getTokenizer } from "@anthropic-ai/tokenizer";
|
|
1009
|
+
|
|
1010
|
+
// src/utils/file-ops.ts
|
|
1011
|
+
import crypto from "crypto";
|
|
1012
|
+
import fsp3 from "fs/promises";
|
|
1013
|
+
import path5 from "path";
|
|
1014
|
+
|
|
1015
|
+
// src/utils/file-lock.ts
|
|
1016
|
+
import fs4 from "fs";
|
|
1017
|
+
import fsp2 from "fs/promises";
|
|
1018
|
+
import path4 from "path";
|
|
1019
|
+
|
|
1020
|
+
// src/utils/file-ops.ts
|
|
1021
|
+
var MAX_READ_SIZE = 10 * 1024 * 1024;
|
|
1022
|
+
|
|
1023
|
+
// src/core/daily-log.ts
|
|
1024
|
+
import sizeOf from "image-size";
|
|
1025
|
+
function formatDateKey(date) {
|
|
1026
|
+
const d = date ?? /* @__PURE__ */ new Date();
|
|
1027
|
+
const year = d.getFullYear();
|
|
1028
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
1029
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1030
|
+
return `${year}-${month}-${day}`;
|
|
1031
|
+
}
|
|
1032
|
+
function appendDailyLog(content, source, projectPath, agentId) {
|
|
1033
|
+
const dir = agentDailyDir(agentId);
|
|
1034
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1035
|
+
const dateKey = formatDateKey();
|
|
1036
|
+
const filePath = path6.join(dir, `${dateKey}.md`);
|
|
1037
|
+
if (!fs5.existsSync(filePath)) {
|
|
1038
|
+
fs5.writeFileSync(filePath, `---
|
|
1039
|
+
name: '${dateKey}'
|
|
1040
|
+
description: ''
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
`, "utf-8");
|
|
1044
|
+
}
|
|
1045
|
+
const now = /* @__PURE__ */ new Date();
|
|
1046
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
1047
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
1048
|
+
const time = `${hours}:${minutes}`;
|
|
1049
|
+
const sourceLabel = source ?? "unknown";
|
|
1050
|
+
const projectTag = projectPath ? ` [${projectPath}]` : "";
|
|
1051
|
+
const entry = `## ${time} \u2014 ${sourceLabel}${projectTag}
|
|
1052
|
+
${content}
|
|
1053
|
+
|
|
1054
|
+
`;
|
|
1055
|
+
fs5.appendFileSync(filePath, entry, "utf-8");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/core/overview-log.ts
|
|
1059
|
+
import fs6 from "fs";
|
|
1060
|
+
import path7 from "path";
|
|
1061
|
+
var OVERVIEW_LOG_ROTATE_LIMIT = 48 * 1024;
|
|
1062
|
+
var CURRENT_LOG = "log.md";
|
|
1063
|
+
function validateSegment(kind, value) {
|
|
1064
|
+
if (!value || value.includes("..") || value.includes("/") || value.includes("\\")) {
|
|
1065
|
+
throw new Error(`Invalid ${kind} "${value}": must be a simple name without path separators.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function skillDir(category, name) {
|
|
1069
|
+
validateSegment("category", category);
|
|
1070
|
+
validateSegment("skill name", name);
|
|
1071
|
+
return path7.join(GLOBAL_SKILLS_DIR, category, name);
|
|
1072
|
+
}
|
|
1073
|
+
function skillHistoryDir(category, name) {
|
|
1074
|
+
return path7.join(skillDir(category, name), "history");
|
|
1075
|
+
}
|
|
1076
|
+
function hasSkillDir(category, name) {
|
|
1077
|
+
try {
|
|
1078
|
+
return fs6.existsSync(path7.join(skillDir(category, name), "SKILL.md"));
|
|
1079
|
+
} catch {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function nextVolumeName(historyDir, now) {
|
|
1084
|
+
const stamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1085
|
+
let seq = 1;
|
|
1086
|
+
while (fs6.existsSync(path7.join(historyDir, `log.${stamp}-${seq}.md`))) seq++;
|
|
1087
|
+
return `log.${stamp}-${seq}.md`;
|
|
1088
|
+
}
|
|
1089
|
+
function appendSkillHistoryLog(category, name, text, source, headerTitle) {
|
|
1090
|
+
validateSegment("category", category);
|
|
1091
|
+
validateSegment("skill name", name);
|
|
1092
|
+
const entryText = (text ?? "").trim();
|
|
1093
|
+
if (!entryText) throw new Error("History log entry text is empty.");
|
|
1094
|
+
if (!hasSkillDir(category, name)) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`Skill "${category}/${name}" does not exist (skills/${category}/${name}/SKILL.md). Create it first.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const title = headerTitle ?? (name === "overview" ? category : name);
|
|
1100
|
+
const historyDir = skillHistoryDir(category, name);
|
|
1101
|
+
fs6.mkdirSync(historyDir, { recursive: true });
|
|
1102
|
+
const current = path7.join(historyDir, CURRENT_LOG);
|
|
1103
|
+
const now = /* @__PURE__ */ new Date();
|
|
1104
|
+
const ts = now.toISOString().slice(0, 16).replace("T", " ");
|
|
1105
|
+
const entry = `## ${ts}${source ? ` \u2014 ${source}` : ""}
|
|
1106
|
+
|
|
1107
|
+
${entryText}
|
|
1108
|
+
|
|
1109
|
+
`;
|
|
1110
|
+
let rotated = false;
|
|
1111
|
+
let archivedVolume;
|
|
1112
|
+
try {
|
|
1113
|
+
const size = fs6.existsSync(current) ? fs6.statSync(current).size : 0;
|
|
1114
|
+
if (size > 0 && size + Buffer.byteLength(entry, "utf-8") > OVERVIEW_LOG_ROTATE_LIMIT) {
|
|
1115
|
+
archivedVolume = nextVolumeName(historyDir, now);
|
|
1116
|
+
fs6.renameSync(current, path7.join(historyDir, archivedVolume));
|
|
1117
|
+
fs6.writeFileSync(
|
|
1118
|
+
current,
|
|
1119
|
+
`# ${title} progress log
|
|
1120
|
+
|
|
1121
|
+
Previous volume: [${archivedVolume}](./${archivedVolume})
|
|
1122
|
+
|
|
1123
|
+
`,
|
|
1124
|
+
"utf-8"
|
|
1125
|
+
);
|
|
1126
|
+
rotated = true;
|
|
1127
|
+
}
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
log.memory.warn("skill-history: rotation failed, appending to current volume", {
|
|
1130
|
+
category,
|
|
1131
|
+
name,
|
|
1132
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (!fs6.existsSync(current)) {
|
|
1136
|
+
fs6.writeFileSync(current, `# ${title} progress log
|
|
1137
|
+
|
|
1138
|
+
`, "utf-8");
|
|
1139
|
+
}
|
|
1140
|
+
fs6.appendFileSync(current, entry, "utf-8");
|
|
1141
|
+
return { file: current, rotated, archivedVolume };
|
|
1142
|
+
}
|
|
1143
|
+
function subdirs(dir) {
|
|
1144
|
+
try {
|
|
1145
|
+
return fs6.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
1146
|
+
} catch {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function resolveProjectSkillDir(project) {
|
|
1151
|
+
const proj = (project ?? "").trim();
|
|
1152
|
+
if (!proj || proj.includes("/") || proj.includes("\\") || proj.includes("..")) return null;
|
|
1153
|
+
const target = proj.toLowerCase();
|
|
1154
|
+
for (const skillCategory of subdirs(GLOBAL_SKILLS_DIR)) {
|
|
1155
|
+
const hit = subdirs(path7.join(GLOBAL_SKILLS_DIR, skillCategory)).find((name) => name.toLowerCase() === target);
|
|
1156
|
+
if (hit && hasSkillDir(skillCategory, hit)) return { skillCategory, name: hit };
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
function appendSkillHistoryForProject(project, text, source) {
|
|
1161
|
+
const proj = (project ?? "").trim();
|
|
1162
|
+
if (!proj) return null;
|
|
1163
|
+
try {
|
|
1164
|
+
const location = resolveProjectSkillDir(proj);
|
|
1165
|
+
if (location) {
|
|
1166
|
+
return appendSkillHistoryLog(location.skillCategory, location.name, text, source);
|
|
1167
|
+
}
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
log.memory.warn("skill-history: project-scoped append failed", {
|
|
1170
|
+
project: proj,
|
|
1171
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/hooks/on-stop.ts
|
|
1178
|
+
import fs7 from "fs";
|
|
1179
|
+
import path8 from "path";
|
|
1180
|
+
import { execSync } from "child_process";
|
|
1181
|
+
|
|
1182
|
+
// src/hooks/session-status-store.ts
|
|
1183
|
+
import Database from "better-sqlite3";
|
|
1184
|
+
function ensureStatusColumns(db, now) {
|
|
1185
|
+
const columns = db.pragma("table_info(sessions)");
|
|
1186
|
+
const names = new Set(columns.map((column) => column.name));
|
|
1187
|
+
if (!names.has("status_revision")) {
|
|
1188
|
+
db.exec("ALTER TABLE sessions ADD COLUMN status_revision INTEGER NOT NULL DEFAULT 1");
|
|
1189
|
+
}
|
|
1190
|
+
if (!names.has("status_updated_at")) {
|
|
1191
|
+
db.exec("ALTER TABLE sessions ADD COLUMN status_updated_at TEXT");
|
|
1192
|
+
}
|
|
1193
|
+
db.prepare(`
|
|
1194
|
+
UPDATE sessions
|
|
1195
|
+
SET status_revision = 1
|
|
1196
|
+
WHERE status_revision IS NULL OR status_revision < 1
|
|
1197
|
+
`).run();
|
|
1198
|
+
db.prepare(`
|
|
1199
|
+
UPDATE sessions
|
|
1200
|
+
SET status_updated_at = COALESCE(
|
|
1201
|
+
NULLIF(status_updated_at, ''),
|
|
1202
|
+
NULLIF(last_status_change, ''),
|
|
1203
|
+
NULLIF(last_active_at, ''),
|
|
1204
|
+
NULLIF(started_at, ''),
|
|
1205
|
+
?
|
|
1206
|
+
)
|
|
1207
|
+
WHERE status_updated_at IS NULL OR status_updated_at = ''
|
|
1208
|
+
`).run(now);
|
|
1209
|
+
}
|
|
1210
|
+
function markSessionStoppedInSqlite(dbPath, sessionId, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1211
|
+
const db = new Database(dbPath);
|
|
1212
|
+
try {
|
|
1213
|
+
db.pragma("journal_mode = WAL");
|
|
1214
|
+
db.pragma("busy_timeout = 5000");
|
|
1215
|
+
return db.transaction(() => {
|
|
1216
|
+
ensureStatusColumns(db, now);
|
|
1217
|
+
const result = db.prepare(`
|
|
1218
|
+
UPDATE sessions SET
|
|
1219
|
+
process_status = 'stopped',
|
|
1220
|
+
last_status_change = ?,
|
|
1221
|
+
last_active_at = ?,
|
|
1222
|
+
activity = NULL,
|
|
1223
|
+
status_revision = status_revision + 1,
|
|
1224
|
+
status_updated_at = ?
|
|
1225
|
+
WHERE claude_session_id = ?
|
|
1226
|
+
AND (process_status IS NULL OR process_status <> 'error')
|
|
1227
|
+
AND (process_status IS NULL OR process_status <> 'stopped' OR activity IS NOT NULL)
|
|
1228
|
+
`).run(now, now, now, sessionId);
|
|
1229
|
+
return result.changes === 1;
|
|
1230
|
+
})();
|
|
1231
|
+
} finally {
|
|
1232
|
+
try {
|
|
1233
|
+
db.close();
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/hooks/on-stop.ts
|
|
1240
|
+
var SESSION_DB_PATH = path8.join(WALNUT_HOME, "sessions.sqlite");
|
|
1241
|
+
function main() {
|
|
1242
|
+
try {
|
|
1243
|
+
const sessionDir = findClaudeSessionDir();
|
|
1244
|
+
if (!sessionDir) return;
|
|
1245
|
+
const summary = extractSessionSummary(sessionDir);
|
|
1246
|
+
summary.status = "agent_complete";
|
|
1247
|
+
const filesChanged = getFilesChanged();
|
|
1248
|
+
saveSessionSummary(summary, filesChanged);
|
|
1249
|
+
updateSessionStore(summary.id);
|
|
1250
|
+
const taskId = findLinkedTaskId();
|
|
1251
|
+
if (taskId) {
|
|
1252
|
+
summary.task_ids.push(taskId);
|
|
1253
|
+
try {
|
|
1254
|
+
updateTaskFromSession(taskId, summary);
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
log.hook.warn("on-stop: task update failed", { taskId, error: err instanceof Error ? err.message : String(err) });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
const project = taskId ? deriveProjectPath(taskId) : null;
|
|
1261
|
+
const entry = formatDailyLogEntry(summary, filesChanged);
|
|
1262
|
+
appendDailyLog(entry, "session-end", project ?? void 0);
|
|
1263
|
+
if (project) {
|
|
1264
|
+
appendSkillHistoryForProject(project, summary.summary, "session");
|
|
1265
|
+
}
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
log.hook.warn("on-stop: failed to write daily/skill-history memory", {
|
|
1268
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
log.hook.error("on-stop hook failed", {
|
|
1273
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1274
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
1275
|
+
});
|
|
1276
|
+
logHookError("on-stop", err);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function getFilesChanged() {
|
|
1280
|
+
try {
|
|
1281
|
+
const output = execSync(
|
|
1282
|
+
"git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only 2>/dev/null",
|
|
1283
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
1284
|
+
);
|
|
1285
|
+
return output.trim().split("\n").filter(Boolean);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function updateSessionStore(sessionId) {
|
|
1291
|
+
try {
|
|
1292
|
+
if (!fs7.existsSync(SESSION_DB_PATH)) return;
|
|
1293
|
+
markSessionStoppedInSqlite(SESSION_DB_PATH, sessionId);
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
log.hook.warn("on-stop: session store update failed", { sessionId, error: String(err) });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function findLinkedTaskId() {
|
|
1299
|
+
try {
|
|
1300
|
+
if (!fs7.existsSync(SESSION_DB_PATH)) return null;
|
|
1301
|
+
const Database2 = __require("better-sqlite3");
|
|
1302
|
+
const db = new Database2(SESSION_DB_PATH, { readonly: true, fileMustExist: true });
|
|
1303
|
+
try {
|
|
1304
|
+
const row = db.prepare(
|
|
1305
|
+
`SELECT task_id FROM sessions
|
|
1306
|
+
WHERE task_id IS NOT NULL AND task_id <> ''
|
|
1307
|
+
ORDER BY last_active_at DESC
|
|
1308
|
+
LIMIT 1`
|
|
1309
|
+
).get();
|
|
1310
|
+
return row?.task_id ?? null;
|
|
1311
|
+
} finally {
|
|
1312
|
+
try {
|
|
1313
|
+
db.close();
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
} catch {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
process.stdin.setEncoding("utf-8");
|
|
1322
|
+
process.stdin.on("data", () => {
|
|
1323
|
+
});
|
|
1324
|
+
process.stdin.on("end", () => {
|
|
1325
|
+
main();
|
|
1326
|
+
process.exit(0);
|
|
1327
|
+
});
|
|
1328
|
+
process.stdin.resume();
|
|
1329
|
+
//# sourceMappingURL=on-stop.js.map
|