autohand-cli 0.6.12 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -48
- package/dist/AutomodeManager-FQQPBHAD.js +1422 -0
- package/dist/AutomodeManager-PUNJS7IZ.cjs +1422 -0
- package/dist/{CommunitySkillsCache-XPDVYU3K.js → CommunitySkillsCache-AY3ZYDTN.js} +1 -0
- package/dist/{CommunitySkillsCache-X3X237QQ.cjs → CommunitySkillsCache-KO4DSKMA.cjs} +1 -0
- package/dist/{GitHubRegistryFetcher-K744NNAJ.cjs → GitHubRegistryFetcher-S4JREWUQ.cjs} +1 -0
- package/dist/{GitHubRegistryFetcher-US2JJID4.js → GitHubRegistryFetcher-V23KTTLM.js} +1 -0
- package/dist/HookManager-OGINWAJN.cjs +7 -0
- package/dist/HookManager-VG46FXSZ.js +7 -0
- package/dist/MemoryManager-AFS5EZJ2.js +8 -0
- package/dist/MemoryManager-KE6EKW7Y.cjs +8 -0
- package/dist/{PermissionManager-PMTQN263.cjs → PermissionManager-MAPKIJMD.cjs} +1 -0
- package/dist/{PermissionManager-YFZI4ZZ6.js → PermissionManager-V2Q2OAS2.js} +1 -0
- package/dist/SessionManager-MKLGLZWC.cjs +10 -0
- package/dist/SessionManager-V25OJDDY.js +10 -0
- package/dist/{SkillsRegistry-OINIPILA.cjs → SkillsRegistry-4RU2OAEQ.cjs} +1 -0
- package/dist/{SkillsRegistry-7NICF6FY.js → SkillsRegistry-FTLVJZMA.js} +1 -0
- package/dist/{agents-GRAFXZY3.cjs → agents-CHNQ7LQ4.cjs} +1 -0
- package/dist/{agents-B33IAATH.js → agents-E4NEH2PN.js} +1 -0
- package/dist/{agents-new-67NJJSDA.cjs → agents-new-GHVWXW7T.cjs} +1 -0
- package/dist/{agents-new-KTXJFC5E.js → agents-new-W6HMQ7V5.js} +1 -0
- package/dist/automode-BJYGRMEQ.cjs +9 -0
- package/dist/automode-XCNP6HP4.js +9 -0
- package/dist/chunk-2FLBGPE3.js +199 -0
- package/dist/{chunk-5RX6NVQO.cjs → chunk-3L76MLO5.cjs} +20 -2
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-4L5WYXHN.js +246 -0
- package/dist/{chunk-SFT6BEYU.js → chunk-BAFJQUWR.js} +20 -2
- package/dist/chunk-CXZEPTRI.js +172 -0
- package/dist/{chunk-CT2VTDPQ.cjs → chunk-E6U4UE3B.cjs} +1 -1
- package/dist/{chunk-TG34DNLU.cjs → chunk-EBM5QDJ4.cjs} +2 -2
- package/dist/{chunk-MFLRXVKU.js → chunk-HUE3GT5M.js} +1 -1
- package/dist/{chunk-A2TFZJD4.js → chunk-LCCJ26QR.js} +2 -2
- package/dist/chunk-OBGZSXTJ.cjs +10 -0
- package/dist/chunk-OTS4YFSZ.cjs +172 -0
- package/dist/chunk-SXYYH3VD.cjs +497 -0
- package/dist/chunk-UOLJZFPJ.js +497 -0
- package/dist/chunk-UPR5PKX4.cjs +199 -0
- package/dist/chunk-URY4AS4L.cjs +246 -0
- package/dist/{completion-Y42FKDT3.js → completion-2XTEWNMC.js} +1 -0
- package/dist/{completion-WVFWX7XQ.cjs → completion-BTDPVWVB.cjs} +1 -0
- package/dist/{constants-QYBEF3DB.js → constants-RVCOJL3L.js} +1 -0
- package/dist/{constants-PE5DLI7Q.cjs → constants-V4V43DZQ.cjs} +1 -0
- package/dist/{defaultHooks-3G3DVF6I.js → defaultHooks-3GRSZNKA.js} +2 -0
- package/dist/{defaultHooks-Z4KA6U5C.cjs → defaultHooks-7EAUU6MN.cjs} +3 -1
- package/dist/{export-BKBJ7PB2.cjs → export-CB34FSEH.cjs} +1 -0
- package/dist/{export-WJ5P6E5Z.js → export-LNPP3XXH.js} +1 -0
- package/dist/{feedback-PZ2PINDU.js → feedback-IZKDNGHP.js} +1 -0
- package/dist/{feedback-R66B3B3C.cjs → feedback-OKGBXZZ4.cjs} +1 -0
- package/dist/{formatters-UG6VZJJ5.js → formatters-JJK6RS55.js} +1 -0
- package/dist/{formatters-N5IJKYZY.cjs → formatters-OIGPANHJ.cjs} +1 -0
- package/dist/{help-PKC6QCNG.js → help-MU553D6W.js} +1 -0
- package/dist/{help-UEELQRHP.cjs → help-QAAUDLFS.cjs} +1 -0
- package/dist/{hooks-5NY5K4L3.js → hooks-4N5VRVOF.js} +2 -1
- package/dist/hooks-7DZGBMXP.cjs +10 -0
- package/dist/index.cjs +933 -1395
- package/dist/index.js +863 -1325
- package/dist/{init-SLLSDDJN.cjs → init-2QEB7GL5.cjs} +1 -0
- package/dist/{init-DML7AOII.js → init-WW4RITLV.js} +1 -0
- package/dist/{lint-TA2ZHVLM.js → lint-L2TD6NY6.js} +1 -0
- package/dist/{lint-44UQJ673.cjs → lint-ZLRBEAFF.cjs} +1 -0
- package/dist/{localProjectPermissions-75X3ZGKH.cjs → localProjectPermissions-2DP6X55S.cjs} +1 -0
- package/dist/{localProjectPermissions-DURCNDZG.js → localProjectPermissions-5CXHD4DO.js} +1 -0
- package/dist/{login-BZ7J4Z4M.js → login-LH62FYMH.js} +1 -0
- package/dist/{login-HUH3CEWL.cjs → login-TWWYJKX6.cjs} +1 -0
- package/dist/{logout-3V3SH7OL.cjs → logout-35XNU6Q2.cjs} +1 -0
- package/dist/{logout-BZKEMNHB.js → logout-KPHUXO23.js} +1 -0
- package/dist/{memory-4GSP7NKV.js → memory-2SGSO4MW.js} +1 -0
- package/dist/{memory-CFNC7RJH.cjs → memory-ALXCFFQY.cjs} +1 -0
- package/dist/{model-TKVEJ5BC.cjs → model-JC43B3KM.cjs} +1 -0
- package/dist/{model-HKEFSH5E.js → model-U3BWIWVH.js} +1 -0
- package/dist/{new-EB2MBQXA.cjs → new-24YT5KVI.cjs} +1 -0
- package/dist/{new-EEZC4XXV.js → new-PCOF6OLV.js} +1 -0
- package/dist/{patch-J32X2QQP.cjs → patch-ETANEGRW.cjs} +3 -1
- package/dist/{patch-BAAQIYSW.js → patch-RPK3BZQR.js} +2 -0
- package/dist/{permissions-5MTH22EF.js → permissions-5W5JOVM7.js} +1 -0
- package/dist/{permissions-IP5SITPI.cjs → permissions-NHPJPHDV.cjs} +1 -0
- package/dist/{quit-RSYIERO5.js → quit-NIDVPHNL.js} +1 -0
- package/dist/{quit-2RYFGIJP.cjs → quit-TMKMX2YF.cjs} +1 -0
- package/dist/{resume-OYZMJRNO.cjs → resume-5C44HAAH.cjs} +1 -0
- package/dist/{resume-2NERFSTD.js → resume-ZZ2D2NMB.js} +1 -0
- package/dist/{session-T3TAZ5ZU.cjs → session-6TMBGN7N.cjs} +1 -0
- package/dist/{session-H5QWKE5E.js → session-76F55XKA.js} +1 -0
- package/dist/{sessions-4KXIT76T.js → sessions-ERKBJ7MC.js} +1 -0
- package/dist/{sessions-7RTCPVNE.cjs → sessions-NUPCJVCF.cjs} +1 -0
- package/dist/{skills-CRFOVWEQ.js → skills-R25OBHE5.js} +2 -1
- package/dist/skills-UCWKIHHG.cjs +13 -0
- package/dist/{skills-install-RMPXN6RK.js → skills-install-GNTBBL46.js} +1 -0
- package/dist/{skills-install-Z27KPEGF.cjs → skills-install-VUSVGSON.cjs} +1 -0
- package/dist/{skills-new-3QJUST7P.cjs → skills-new-AGXQNBRA.cjs} +1 -0
- package/dist/{skills-new-S2YPO635.js → skills-new-L36LQXIE.js} +1 -0
- package/dist/status-2QV7C3JJ.cjs +9 -0
- package/dist/{status-KC56ITDB.js → status-VJ6FOSRI.js} +2 -1
- package/dist/{theme-LIF3RD3A.cjs → theme-MI3BM56M.cjs} +1 -0
- package/dist/{theme-JAMJSCKR.js → theme-ZXGSJBZI.js} +1 -0
- package/dist/{undo-7QJBXARS.js → undo-3UU5LWQS.js} +1 -0
- package/dist/{undo-2WR2ZIEC.cjs → undo-W4VN2Y37.cjs} +1 -0
- package/package.json +2 -2
- package/dist/hooks-PIYYJZMI.cjs +0 -9
- package/dist/skills-6PIGHOWS.cjs +0 -12
- package/dist/status-VDVKZCQR.cjs +0 -8
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// src/core/HookManager.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { minimatch } from "minimatch";
|
|
4
|
+
var DEFAULT_HOOK_TIMEOUT = 5e3;
|
|
5
|
+
var HookManager = class {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.initialized = false;
|
|
8
|
+
this.settings = options.settings ?? { enabled: true, hooks: [] };
|
|
9
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
10
|
+
this.onPersist = options.onPersist;
|
|
11
|
+
this.onHookOutput = options.onHookOutput;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Initialize hooks - set up default hooks if none exist
|
|
15
|
+
*/
|
|
16
|
+
async initialize() {
|
|
17
|
+
if (this.initialized) return;
|
|
18
|
+
this.initialized = true;
|
|
19
|
+
if (!this.settings.hooks || this.settings.hooks.length === 0) {
|
|
20
|
+
await this.installDefaultHooks();
|
|
21
|
+
} else {
|
|
22
|
+
await this.mergeDefaultHooks();
|
|
23
|
+
}
|
|
24
|
+
await this.ensureHookScripts();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Install default hooks (disabled by default)
|
|
28
|
+
*/
|
|
29
|
+
async installDefaultHooks() {
|
|
30
|
+
try {
|
|
31
|
+
const { DEFAULT_HOOKS, SMART_COMMIT_HOOK } = await import("./defaultHooks-3GRSZNKA.js");
|
|
32
|
+
this.settings.hooks = [...DEFAULT_HOOKS, SMART_COMMIT_HOOK];
|
|
33
|
+
if (this.onPersist) {
|
|
34
|
+
await this.onPersist();
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get a unique identifier for a hook (used for deduplication)
|
|
41
|
+
* Uses script filename for script-based hooks, or event+description for inline commands
|
|
42
|
+
*/
|
|
43
|
+
getHookIdentifier(hook) {
|
|
44
|
+
const scriptMatch = hook.command.match(/([^/]+\.sh)$/);
|
|
45
|
+
if (scriptMatch) {
|
|
46
|
+
return `script:${scriptMatch[1]}`;
|
|
47
|
+
}
|
|
48
|
+
if (hook.description) {
|
|
49
|
+
return `${hook.event}:${hook.description}`;
|
|
50
|
+
}
|
|
51
|
+
return `${hook.event}:${hook.command}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Merge any missing default hooks with existing user hooks
|
|
55
|
+
* This ensures new built-in hooks are added when upgrading
|
|
56
|
+
*/
|
|
57
|
+
async mergeDefaultHooks() {
|
|
58
|
+
try {
|
|
59
|
+
const { DEFAULT_HOOKS, SMART_COMMIT_HOOK } = await import("./defaultHooks-3GRSZNKA.js");
|
|
60
|
+
const allDefaults = [...DEFAULT_HOOKS, SMART_COMMIT_HOOK];
|
|
61
|
+
const existingHooks = this.settings.hooks ?? [];
|
|
62
|
+
const existingIds = new Set(existingHooks.map((h) => this.getHookIdentifier(h)));
|
|
63
|
+
const missingHooks = allDefaults.filter((h) => !existingIds.has(this.getHookIdentifier(h)));
|
|
64
|
+
if (missingHooks.length > 0) {
|
|
65
|
+
this.settings.hooks = [...existingHooks, ...missingHooks];
|
|
66
|
+
if (this.onPersist) {
|
|
67
|
+
await this.onPersist();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("[hooks] Failed to merge default hooks:", error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Ensure all hook scripts exist in ~/.autohand/hooks/
|
|
76
|
+
* Installs bundled scripts for built-in hooks
|
|
77
|
+
*/
|
|
78
|
+
async ensureHookScripts() {
|
|
79
|
+
try {
|
|
80
|
+
const fs = await import("fs-extra");
|
|
81
|
+
const path = await import("path");
|
|
82
|
+
const os = await import("os");
|
|
83
|
+
const hooksDir = path.join(os.homedir(), ".autohand", "hooks");
|
|
84
|
+
await fs.ensureDir(hooksDir);
|
|
85
|
+
const { HOOK_SCRIPTS } = await import("./defaultHooks-3GRSZNKA.js");
|
|
86
|
+
for (const [scriptName, scriptContent] of Object.entries(HOOK_SCRIPTS)) {
|
|
87
|
+
const scriptPath = path.join(hooksDir, scriptName);
|
|
88
|
+
if (!await fs.pathExists(scriptPath)) {
|
|
89
|
+
await fs.writeFile(scriptPath, scriptContent, { mode: 493 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if hooks are globally enabled
|
|
97
|
+
*/
|
|
98
|
+
isEnabled() {
|
|
99
|
+
return this.settings.enabled !== false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get all registered hooks
|
|
103
|
+
*/
|
|
104
|
+
getHooks() {
|
|
105
|
+
return this.settings.hooks ?? [];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get current settings
|
|
109
|
+
*/
|
|
110
|
+
getSettings() {
|
|
111
|
+
return { ...this.settings };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Update settings
|
|
115
|
+
*/
|
|
116
|
+
async updateSettings(settings) {
|
|
117
|
+
this.settings = { ...this.settings, ...settings };
|
|
118
|
+
if (this.onPersist) {
|
|
119
|
+
await this.onPersist();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Add a new hook
|
|
124
|
+
*/
|
|
125
|
+
async addHook(hook) {
|
|
126
|
+
const hooks = this.settings.hooks ?? [];
|
|
127
|
+
hooks.push({ ...hook, enabled: hook.enabled !== false });
|
|
128
|
+
this.settings.hooks = hooks;
|
|
129
|
+
if (this.onPersist) {
|
|
130
|
+
await this.onPersist();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Remove a hook by index within its event type
|
|
135
|
+
*/
|
|
136
|
+
async removeHook(event, index) {
|
|
137
|
+
const hooks = this.settings.hooks ?? [];
|
|
138
|
+
const eventHooks = hooks.filter((h) => h.event === event);
|
|
139
|
+
if (index < 0 || index >= eventHooks.length) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const hookToRemove = eventHooks[index];
|
|
143
|
+
const globalIndex = hooks.indexOf(hookToRemove);
|
|
144
|
+
if (globalIndex !== -1) {
|
|
145
|
+
hooks.splice(globalIndex, 1);
|
|
146
|
+
this.settings.hooks = hooks;
|
|
147
|
+
if (this.onPersist) {
|
|
148
|
+
await this.onPersist();
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Toggle a hook's enabled status
|
|
156
|
+
*/
|
|
157
|
+
async toggleHook(event, index) {
|
|
158
|
+
const hooks = this.settings.hooks ?? [];
|
|
159
|
+
const eventHooks = hooks.filter((h) => h.event === event);
|
|
160
|
+
if (index < 0 || index >= eventHooks.length) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const hook = eventHooks[index];
|
|
164
|
+
hook.enabled = hook.enabled === false;
|
|
165
|
+
if (this.onPersist) {
|
|
166
|
+
await this.onPersist();
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if a hook's filter matches the context
|
|
172
|
+
*/
|
|
173
|
+
matchesFilter(filter, context) {
|
|
174
|
+
if (!filter) return true;
|
|
175
|
+
if (filter.tool && filter.tool.length > 0) {
|
|
176
|
+
if (!context.tool || !filter.tool.includes(context.tool)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (filter.path && filter.path.length > 0) {
|
|
181
|
+
if (!context.path) return false;
|
|
182
|
+
const matches = filter.path.some((pattern) => minimatch(context.path, pattern));
|
|
183
|
+
if (!matches) return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if a hook's regex matcher matches the context
|
|
189
|
+
* Matchers apply to tool names, notification types, session types, etc.
|
|
190
|
+
*/
|
|
191
|
+
matchesMatcher(hook, context) {
|
|
192
|
+
if (!hook.matcher) return true;
|
|
193
|
+
let value = "";
|
|
194
|
+
switch (hook.event) {
|
|
195
|
+
case "pre-tool":
|
|
196
|
+
case "post-tool":
|
|
197
|
+
case "permission-request":
|
|
198
|
+
value = context.tool ?? "";
|
|
199
|
+
break;
|
|
200
|
+
case "notification":
|
|
201
|
+
value = context.notificationType ?? "";
|
|
202
|
+
break;
|
|
203
|
+
case "session-start":
|
|
204
|
+
value = context.sessionType ?? "";
|
|
205
|
+
break;
|
|
206
|
+
case "session-end":
|
|
207
|
+
value = context.sessionEndReason ?? "";
|
|
208
|
+
break;
|
|
209
|
+
case "subagent-stop":
|
|
210
|
+
value = context.subagentType ?? "";
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return new RegExp(hook.matcher).test(value);
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Build environment variables from hook context
|
|
223
|
+
*/
|
|
224
|
+
buildEnvironment(context) {
|
|
225
|
+
const env = {
|
|
226
|
+
...process.env,
|
|
227
|
+
HOOK_EVENT: context.event,
|
|
228
|
+
HOOK_WORKSPACE: context.workspace
|
|
229
|
+
};
|
|
230
|
+
if (context.sessionId) env.HOOK_SESSION_ID = context.sessionId;
|
|
231
|
+
if (context.tool) env.HOOK_TOOL = context.tool;
|
|
232
|
+
if (context.toolCallId) env.HOOK_TOOL_CALL_ID = context.toolCallId;
|
|
233
|
+
if (context.args) env.HOOK_ARGS = JSON.stringify(context.args);
|
|
234
|
+
if (context.success !== void 0) env.HOOK_SUCCESS = String(context.success);
|
|
235
|
+
if (context.output) env.HOOK_OUTPUT = context.output;
|
|
236
|
+
if (context.duration !== void 0) env.HOOK_DURATION = String(context.duration);
|
|
237
|
+
if (context.path) env.HOOK_PATH = context.path;
|
|
238
|
+
if (context.changeType) env.HOOK_CHANGE_TYPE = context.changeType;
|
|
239
|
+
if (context.instruction) env.HOOK_INSTRUCTION = context.instruction;
|
|
240
|
+
if (context.mentionedFiles) env.HOOK_MENTIONED_FILES = JSON.stringify(context.mentionedFiles);
|
|
241
|
+
if (context.tokensUsed !== void 0) env.HOOK_TOKENS = String(context.tokensUsed);
|
|
242
|
+
if (context.toolCallsCount !== void 0) env.HOOK_TOOL_CALLS_COUNT = String(context.toolCallsCount);
|
|
243
|
+
if (context.toolCallsInTurn !== void 0) env.HOOK_TURN_TOOL_CALLS = String(context.toolCallsInTurn);
|
|
244
|
+
if (context.turnDuration !== void 0) env.HOOK_TURN_DURATION = String(context.turnDuration);
|
|
245
|
+
if (context.error) env.HOOK_ERROR = context.error;
|
|
246
|
+
if (context.errorCode) env.HOOK_ERROR_CODE = context.errorCode;
|
|
247
|
+
if (context.sessionType) env.HOOK_SESSION_TYPE = context.sessionType;
|
|
248
|
+
if (context.sessionEndReason) env.HOOK_SESSION_END_REASON = context.sessionEndReason;
|
|
249
|
+
if (context.subagentId) env.HOOK_SUBAGENT_ID = context.subagentId;
|
|
250
|
+
if (context.subagentType) env.HOOK_SUBAGENT_TYPE = context.subagentType;
|
|
251
|
+
if (context.permissionType) env.HOOK_PERMISSION_TYPE = context.permissionType;
|
|
252
|
+
if (context.notificationType) env.HOOK_NOTIFICATION_TYPE = context.notificationType;
|
|
253
|
+
if (context.notificationMessage) env.HOOK_NOTIFICATION_MSG = context.notificationMessage;
|
|
254
|
+
return env;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Build JSON input to pass via stdin to hook
|
|
258
|
+
*/
|
|
259
|
+
buildJsonInput(context) {
|
|
260
|
+
return JSON.stringify({
|
|
261
|
+
session_id: context.sessionId,
|
|
262
|
+
cwd: context.workspace,
|
|
263
|
+
hook_event_name: context.event,
|
|
264
|
+
// Tool context
|
|
265
|
+
tool_name: context.tool,
|
|
266
|
+
tool_input: context.args,
|
|
267
|
+
tool_use_id: context.toolCallId,
|
|
268
|
+
tool_response: context.output,
|
|
269
|
+
tool_success: context.success,
|
|
270
|
+
// File context
|
|
271
|
+
file_path: context.path,
|
|
272
|
+
change_type: context.changeType,
|
|
273
|
+
// Prompt context
|
|
274
|
+
instruction: context.instruction,
|
|
275
|
+
mentioned_files: context.mentionedFiles,
|
|
276
|
+
// Stop/response context
|
|
277
|
+
tokens_used: context.tokensUsed,
|
|
278
|
+
tool_calls_count: context.toolCallsCount,
|
|
279
|
+
turn_tool_calls: context.toolCallsInTurn,
|
|
280
|
+
turn_duration: context.turnDuration,
|
|
281
|
+
duration: context.duration,
|
|
282
|
+
// Error context
|
|
283
|
+
error: context.error,
|
|
284
|
+
error_code: context.errorCode,
|
|
285
|
+
// Session context
|
|
286
|
+
session_type: context.sessionType,
|
|
287
|
+
session_end_reason: context.sessionEndReason,
|
|
288
|
+
// Subagent context
|
|
289
|
+
subagent_id: context.subagentId,
|
|
290
|
+
subagent_name: context.subagentName,
|
|
291
|
+
subagent_type: context.subagentType,
|
|
292
|
+
subagent_success: context.subagentSuccess,
|
|
293
|
+
subagent_error: context.subagentError,
|
|
294
|
+
subagent_duration: context.subagentDuration,
|
|
295
|
+
// Permission context
|
|
296
|
+
permission_type: context.permissionType,
|
|
297
|
+
// Notification context
|
|
298
|
+
notification_type: context.notificationType,
|
|
299
|
+
notification_message: context.notificationMessage
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Parse JSON response from hook stdout
|
|
304
|
+
*/
|
|
305
|
+
parseHookResponse(stdout) {
|
|
306
|
+
const trimmed = stdout.trim();
|
|
307
|
+
if (!trimmed.startsWith("{")) {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(trimmed);
|
|
312
|
+
} catch {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Execute a single hook
|
|
318
|
+
*/
|
|
319
|
+
async executeHook(hook, context) {
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
const timeout = hook.timeout ?? DEFAULT_HOOK_TIMEOUT;
|
|
322
|
+
const env = this.buildEnvironment(context);
|
|
323
|
+
const jsonInput = this.buildJsonInput(context);
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
const child = spawn(hook.command, [], {
|
|
326
|
+
shell: true,
|
|
327
|
+
cwd: this.workspaceRoot,
|
|
328
|
+
env,
|
|
329
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
330
|
+
// stdin enabled for JSON input
|
|
331
|
+
});
|
|
332
|
+
let stdout = "";
|
|
333
|
+
let stderr = "";
|
|
334
|
+
let killed = false;
|
|
335
|
+
const timeoutId = setTimeout(() => {
|
|
336
|
+
killed = true;
|
|
337
|
+
child.kill("SIGTERM");
|
|
338
|
+
setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
339
|
+
}, timeout);
|
|
340
|
+
child.stdin?.write(jsonInput);
|
|
341
|
+
child.stdin?.end();
|
|
342
|
+
child.stdout?.on("data", (data) => {
|
|
343
|
+
stdout += data.toString();
|
|
344
|
+
});
|
|
345
|
+
child.stderr?.on("data", (data) => {
|
|
346
|
+
stderr += data.toString();
|
|
347
|
+
});
|
|
348
|
+
child.on("close", (code) => {
|
|
349
|
+
clearTimeout(timeoutId);
|
|
350
|
+
const duration = Date.now() - startTime;
|
|
351
|
+
const exitCode = code ?? 0;
|
|
352
|
+
const isBlockingError = exitCode === 2;
|
|
353
|
+
let response;
|
|
354
|
+
if (exitCode === 0) {
|
|
355
|
+
response = this.parseHookResponse(stdout);
|
|
356
|
+
}
|
|
357
|
+
const result = {
|
|
358
|
+
hook,
|
|
359
|
+
success: !killed && exitCode === 0,
|
|
360
|
+
stdout: stdout.trim() || void 0,
|
|
361
|
+
stderr: stderr.trim() || void 0,
|
|
362
|
+
error: killed ? `Hook timed out after ${timeout}ms` : isBlockingError ? stderr.trim() || "Hook blocked execution" : void 0,
|
|
363
|
+
duration,
|
|
364
|
+
exitCode,
|
|
365
|
+
blockingError: isBlockingError,
|
|
366
|
+
response
|
|
367
|
+
};
|
|
368
|
+
if (this.onHookOutput) {
|
|
369
|
+
this.onHookOutput(result);
|
|
370
|
+
}
|
|
371
|
+
resolve(result);
|
|
372
|
+
});
|
|
373
|
+
child.on("error", (err) => {
|
|
374
|
+
clearTimeout(timeoutId);
|
|
375
|
+
const duration = Date.now() - startTime;
|
|
376
|
+
const result = {
|
|
377
|
+
hook,
|
|
378
|
+
success: false,
|
|
379
|
+
error: err.message,
|
|
380
|
+
duration,
|
|
381
|
+
exitCode: -1
|
|
382
|
+
};
|
|
383
|
+
if (this.onHookOutput) {
|
|
384
|
+
this.onHookOutput(result);
|
|
385
|
+
}
|
|
386
|
+
resolve(result);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get hooks for an event, including alias handling
|
|
392
|
+
*/
|
|
393
|
+
getHooksForEvent(event) {
|
|
394
|
+
const hooks = this.getHooks().filter((h) => h.enabled !== false);
|
|
395
|
+
if (event === "stop") {
|
|
396
|
+
return hooks.filter((h) => h.event === "stop" || h.event === "post-response");
|
|
397
|
+
}
|
|
398
|
+
if (event === "post-response") {
|
|
399
|
+
return hooks.filter((h) => h.event === "stop" || h.event === "post-response");
|
|
400
|
+
}
|
|
401
|
+
return hooks.filter((h) => h.event === event);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Execute all hooks for an event
|
|
405
|
+
*
|
|
406
|
+
* Sync hooks are executed sequentially and block until complete.
|
|
407
|
+
* Async hooks are executed in parallel and don't block.
|
|
408
|
+
*/
|
|
409
|
+
async executeHooks(event, context) {
|
|
410
|
+
if (!this.isEnabled()) {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
const fullContext = {
|
|
414
|
+
...context,
|
|
415
|
+
event,
|
|
416
|
+
workspace: this.workspaceRoot
|
|
417
|
+
};
|
|
418
|
+
const hooks = this.getHooksForEvent(event).filter(
|
|
419
|
+
(h) => this.matchesFilter(h.filter, fullContext) && this.matchesMatcher(h, fullContext)
|
|
420
|
+
);
|
|
421
|
+
if (hooks.length === 0) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const syncHooks = hooks.filter((h) => !h.async);
|
|
425
|
+
const asyncHooks = hooks.filter((h) => h.async);
|
|
426
|
+
const results = [];
|
|
427
|
+
for (const hook of syncHooks) {
|
|
428
|
+
const result = await this.executeHook(hook, fullContext);
|
|
429
|
+
results.push(result);
|
|
430
|
+
if (result.response?.continue === false) {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (asyncHooks.length > 0) {
|
|
435
|
+
const asyncResults = await Promise.all(
|
|
436
|
+
asyncHooks.map((hook) => this.executeHook(hook, fullContext))
|
|
437
|
+
);
|
|
438
|
+
results.push(...asyncResults);
|
|
439
|
+
}
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Test a hook by executing it with a sample context
|
|
444
|
+
*/
|
|
445
|
+
async testHook(hook) {
|
|
446
|
+
const context = {
|
|
447
|
+
event: hook.event,
|
|
448
|
+
workspace: this.workspaceRoot,
|
|
449
|
+
tool: "test_tool",
|
|
450
|
+
toolCallId: "test_123",
|
|
451
|
+
args: { test: true },
|
|
452
|
+
success: true,
|
|
453
|
+
path: "test/file.ts",
|
|
454
|
+
changeType: "modify",
|
|
455
|
+
instruction: "Test instruction",
|
|
456
|
+
tokensUsed: 100
|
|
457
|
+
};
|
|
458
|
+
return this.executeHook(hook, context);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get a summary of hooks by event
|
|
462
|
+
*/
|
|
463
|
+
getSummary() {
|
|
464
|
+
const events = [
|
|
465
|
+
"pre-tool",
|
|
466
|
+
"post-tool",
|
|
467
|
+
"file-modified",
|
|
468
|
+
"pre-prompt",
|
|
469
|
+
"stop",
|
|
470
|
+
"post-response",
|
|
471
|
+
// Alias for 'stop'
|
|
472
|
+
"session-error",
|
|
473
|
+
"subagent-stop",
|
|
474
|
+
"session-start",
|
|
475
|
+
"session-end",
|
|
476
|
+
"permission-request",
|
|
477
|
+
"notification"
|
|
478
|
+
];
|
|
479
|
+
const summary = {};
|
|
480
|
+
for (const event of events) {
|
|
481
|
+
const eventHooks = this.getHooks().filter((h) => h.event === event);
|
|
482
|
+
summary[event] = {
|
|
483
|
+
total: eventHooks.length,
|
|
484
|
+
enabled: eventHooks.filter((h) => h.enabled !== false).length
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return summary;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export {
|
|
492
|
+
HookManager
|
|
493
|
+
};
|
|
494
|
+
/**
|
|
495
|
+
* Hook Manager - Executes lifecycle hooks based on config
|
|
496
|
+
* @license Apache-2.0
|
|
497
|
+
*/
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkXTHHDIBGcjs = require('./chunk-XTHHDIBG.cjs');
|
|
4
|
+
|
|
5
|
+
// src/session/SessionManager.ts
|
|
6
|
+
var _fsextra = require('fs-extra'); var _fsextra2 = _interopRequireDefault(_fsextra);
|
|
7
|
+
var _path = require('path'); var _path2 = _interopRequireDefault(_path);
|
|
8
|
+
var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
|
|
9
|
+
var SessionManager = class {
|
|
10
|
+
constructor(baseDir) {
|
|
11
|
+
this.currentSession = null;
|
|
12
|
+
this.index = null;
|
|
13
|
+
this.sessionsDir = _nullishCoalesce(baseDir, () => ( _chunkXTHHDIBGcjs.AUTOHAND_PATHS.sessions));
|
|
14
|
+
}
|
|
15
|
+
async initialize() {
|
|
16
|
+
await _fsextra2.default.ensureDir(this.sessionsDir);
|
|
17
|
+
await this.loadIndex();
|
|
18
|
+
}
|
|
19
|
+
async createSession(projectPath, model) {
|
|
20
|
+
const sessionId = this.generateSessionId();
|
|
21
|
+
const sessionDir = _path2.default.join(this.sessionsDir, sessionId);
|
|
22
|
+
await _fsextra2.default.ensureDir(sessionDir);
|
|
23
|
+
const metadata = {
|
|
24
|
+
sessionId,
|
|
25
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26
|
+
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27
|
+
projectPath: _path2.default.resolve(projectPath),
|
|
28
|
+
projectName: _path2.default.basename(projectPath),
|
|
29
|
+
model,
|
|
30
|
+
messageCount: 0,
|
|
31
|
+
status: "active"
|
|
32
|
+
};
|
|
33
|
+
const session = new Session(sessionDir, metadata);
|
|
34
|
+
await session.save();
|
|
35
|
+
this.currentSession = session;
|
|
36
|
+
await this.addToIndex(session.metadata);
|
|
37
|
+
return session;
|
|
38
|
+
}
|
|
39
|
+
async loadSession(sessionId) {
|
|
40
|
+
const sessionDir = _path2.default.join(this.sessionsDir, sessionId);
|
|
41
|
+
if (!await _fsextra2.default.pathExists(sessionDir)) {
|
|
42
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
43
|
+
}
|
|
44
|
+
const metadataPath = _path2.default.join(sessionDir, "metadata.json");
|
|
45
|
+
const metadata = await _fsextra2.default.readJson(metadataPath);
|
|
46
|
+
const session = new Session(sessionDir, metadata);
|
|
47
|
+
await session.load();
|
|
48
|
+
this.currentSession = session;
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
async listSessions(filter) {
|
|
52
|
+
await this.loadIndex();
|
|
53
|
+
if (!this.index) return [];
|
|
54
|
+
let sessions = this.index.sessions;
|
|
55
|
+
if (_optionalChain([filter, 'optionalAccess', _ => _.project])) {
|
|
56
|
+
const projectPath = _path2.default.resolve(filter.project);
|
|
57
|
+
const sessionIds = this.index.byProject[projectPath] || [];
|
|
58
|
+
sessions = sessions.filter((s) => sessionIds.includes(s.id));
|
|
59
|
+
}
|
|
60
|
+
if (_optionalChain([filter, 'optionalAccess', _2 => _2.since])) {
|
|
61
|
+
sessions = sessions.filter((s) => new Date(s.createdAt) >= filter.since);
|
|
62
|
+
}
|
|
63
|
+
const fullMetadata = [];
|
|
64
|
+
for (const s of sessions) {
|
|
65
|
+
const sessionDir = _path2.default.join(this.sessionsDir, s.id);
|
|
66
|
+
const metadataPath = _path2.default.join(sessionDir, "metadata.json");
|
|
67
|
+
if (await _fsextra2.default.pathExists(metadataPath)) {
|
|
68
|
+
const metadata = await _fsextra2.default.readJson(metadataPath);
|
|
69
|
+
fullMetadata.push(metadata);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return fullMetadata.sort(
|
|
73
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
async getLastSession(projectPath) {
|
|
77
|
+
const sessions = await this.listSessions(projectPath ? { project: projectPath } : void 0);
|
|
78
|
+
return sessions[0] || null;
|
|
79
|
+
}
|
|
80
|
+
async closeSession(summary) {
|
|
81
|
+
if (!this.currentSession) return;
|
|
82
|
+
this.currentSession.metadata.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
83
|
+
this.currentSession.metadata.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
this.currentSession.metadata.status = "completed";
|
|
85
|
+
if (summary) {
|
|
86
|
+
this.currentSession.metadata.summary = summary;
|
|
87
|
+
}
|
|
88
|
+
await this.currentSession.save();
|
|
89
|
+
await this.updateIndex(this.currentSession.metadata);
|
|
90
|
+
this.currentSession = null;
|
|
91
|
+
}
|
|
92
|
+
getCurrentSession() {
|
|
93
|
+
return this.currentSession;
|
|
94
|
+
}
|
|
95
|
+
generateSessionId() {
|
|
96
|
+
const timestamp = Date.now();
|
|
97
|
+
const uuid = _crypto2.default.randomUUID();
|
|
98
|
+
return `${uuid}-${timestamp}`;
|
|
99
|
+
}
|
|
100
|
+
async loadIndex() {
|
|
101
|
+
const indexPath = _path2.default.join(this.sessionsDir, "index.json");
|
|
102
|
+
if (await _fsextra2.default.pathExists(indexPath)) {
|
|
103
|
+
this.index = await _fsextra2.default.readJson(indexPath);
|
|
104
|
+
} else {
|
|
105
|
+
this.index = { sessions: [], byProject: {} };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async saveIndex() {
|
|
109
|
+
const indexPath = _path2.default.join(this.sessionsDir, "index.json");
|
|
110
|
+
await _fsextra2.default.writeJson(indexPath, this.index, { spaces: 2 });
|
|
111
|
+
}
|
|
112
|
+
async addToIndex(metadata) {
|
|
113
|
+
if (!this.index) await this.loadIndex();
|
|
114
|
+
if (!this.index) return;
|
|
115
|
+
this.index.sessions.push({
|
|
116
|
+
id: metadata.sessionId,
|
|
117
|
+
projectPath: metadata.projectPath,
|
|
118
|
+
createdAt: metadata.createdAt,
|
|
119
|
+
summary: metadata.summary
|
|
120
|
+
});
|
|
121
|
+
if (!this.index.byProject[metadata.projectPath]) {
|
|
122
|
+
this.index.byProject[metadata.projectPath] = [];
|
|
123
|
+
}
|
|
124
|
+
this.index.byProject[metadata.projectPath].push(metadata.sessionId);
|
|
125
|
+
await this.saveIndex();
|
|
126
|
+
}
|
|
127
|
+
async updateIndex(metadata) {
|
|
128
|
+
if (!this.index) return;
|
|
129
|
+
const session = this.index.sessions.find((s) => s.id === metadata.sessionId);
|
|
130
|
+
if (session) {
|
|
131
|
+
session.summary = metadata.summary;
|
|
132
|
+
}
|
|
133
|
+
await this.saveIndex();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var Session = class {
|
|
137
|
+
constructor(sessionDir, metadata) {
|
|
138
|
+
this.messages = [];
|
|
139
|
+
this.state = null;
|
|
140
|
+
this.sessionDir = sessionDir;
|
|
141
|
+
this.metadata = metadata;
|
|
142
|
+
}
|
|
143
|
+
async append(message) {
|
|
144
|
+
this.messages.push(message);
|
|
145
|
+
this.metadata.messageCount = this.messages.length;
|
|
146
|
+
this.metadata.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
147
|
+
const conversationPath = _path2.default.join(this.sessionDir, "conversation.jsonl");
|
|
148
|
+
await _fsextra2.default.appendFile(conversationPath, JSON.stringify(message) + "\n");
|
|
149
|
+
await this.save();
|
|
150
|
+
}
|
|
151
|
+
async appendTransient(message) {
|
|
152
|
+
const conversationPath = _path2.default.join(this.sessionDir, "conversation.jsonl");
|
|
153
|
+
await _fsextra2.default.appendFile(conversationPath, JSON.stringify(message) + "\n");
|
|
154
|
+
}
|
|
155
|
+
async updateState(state) {
|
|
156
|
+
this.state = state;
|
|
157
|
+
const statePath = _path2.default.join(this.sessionDir, "state.json");
|
|
158
|
+
await _fsextra2.default.writeJson(statePath, state, { spaces: 2 });
|
|
159
|
+
}
|
|
160
|
+
async save() {
|
|
161
|
+
const metadataPath = _path2.default.join(this.sessionDir, "metadata.json");
|
|
162
|
+
await _fsextra2.default.writeJson(metadataPath, this.metadata, { spaces: 2 });
|
|
163
|
+
}
|
|
164
|
+
async load() {
|
|
165
|
+
const conversationPath = _path2.default.join(this.sessionDir, "conversation.jsonl");
|
|
166
|
+
if (await _fsextra2.default.pathExists(conversationPath)) {
|
|
167
|
+
const content = await _fsextra2.default.readFile(conversationPath, "utf-8");
|
|
168
|
+
this.messages = content.trim().split("\n").filter((line) => line).map((line) => JSON.parse(line));
|
|
169
|
+
}
|
|
170
|
+
const statePath = _path2.default.join(this.sessionDir, "state.json");
|
|
171
|
+
if (await _fsextra2.default.pathExists(statePath)) {
|
|
172
|
+
this.state = await _fsextra2.default.readJson(statePath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
getMessages() {
|
|
176
|
+
return this.messages;
|
|
177
|
+
}
|
|
178
|
+
getState() {
|
|
179
|
+
return this.state;
|
|
180
|
+
}
|
|
181
|
+
async close(summary) {
|
|
182
|
+
this.metadata.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
183
|
+
this.metadata.status = "completed";
|
|
184
|
+
if (summary) {
|
|
185
|
+
this.metadata.summary = summary;
|
|
186
|
+
}
|
|
187
|
+
await this.save();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
exports.SessionManager = SessionManager; exports.Session = Session;
|
|
195
|
+
/**
|
|
196
|
+
* @license
|
|
197
|
+
* Copyright 2025 Autohand AI LLC
|
|
198
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
199
|
+
*/
|