auto-mode 0.10.0 → 0.11.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.
@@ -1127,3 +1127,100 @@ test('it identifies a subprocess failure without copying the exception', async (
1127
1127
  'auto-mode action unavailable: manual approval retained; subprocess failure',
1128
1128
  ]);
1129
1129
  });
1130
+
1131
+ test('it records a finished scope-creating Bash call with its result', async ($, on) => {
1132
+ let invocation: unknown;
1133
+ let stdin = '';
1134
+
1135
+ on('session.cwd', () => ({ value: '/repo' }));
1136
+ on('classic.SessionStart', () => ({}));
1137
+
1138
+ on('tool.call', () => ({
1139
+ result: { stdout: 'https://github.com/dev/app/pull/3\n', stderr: '' },
1140
+ text: 'https://github.com/dev/app/pull/3\n',
1141
+ }));
1142
+
1143
+ on('process.run', (_api, e) => {
1144
+ invocation = e;
1145
+ stdin = e.init?.stdin ?? '';
1146
+
1147
+ return {
1148
+ value: {
1149
+ exitCode: 0,
1150
+ stdout: '',
1151
+ stderr: '',
1152
+ isStdoutTruncated: false,
1153
+ isStderrTruncated: false,
1154
+ },
1155
+ };
1156
+ });
1157
+
1158
+ // oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
1159
+ await $.classic.SessionStart({
1160
+ source: 'startup',
1161
+ session_id: 'session-1',
1162
+ cwd: '/repo',
1163
+ transcript_path: '/repo/transcript.jsonl',
1164
+ });
1165
+
1166
+ const result = await $.tool.call({
1167
+ tool: 'Bash',
1168
+ tool_use_id: 'call-1',
1169
+ command: 'gh pr create --fill',
1170
+ });
1171
+
1172
+ expect(result).toMatchObject({ text: 'https://github.com/dev/app/pull/3\n' });
1173
+
1174
+ expect(invocation).toMatchObject({
1175
+ argv: ['auto-mode', 'record'],
1176
+ init: { timeoutMs: 8000, stdin: expect.any(String) },
1177
+ });
1178
+
1179
+ expect(JSON.parse(stdin)).toStrictEqual({
1180
+ sessionID: 'session-1',
1181
+ cwd: '/repo',
1182
+ startedAt: expect.any(Number),
1183
+ command: 'gh pr create --fill',
1184
+ resultText: 'https://github.com/dev/app/pull/3\n',
1185
+ });
1186
+ });
1187
+
1188
+ test('it records nothing for a Bash call that cannot create scope or a call that was denied', async ($, on) => {
1189
+ let calls = 0;
1190
+
1191
+ on('session.cwd', () => ({ value: '/repo' }));
1192
+ on('classic.SessionStart', () => ({}));
1193
+
1194
+ on('tool.call', (_api, e) =>
1195
+ e.command === 'git checkout -b x'
1196
+ ? { deny: 'no' }
1197
+ : { result: { stdout: '', stderr: '' }, text: '' },
1198
+ );
1199
+
1200
+ on('process.run', () => {
1201
+ calls += 1;
1202
+
1203
+ return {
1204
+ value: {
1205
+ exitCode: 0,
1206
+ stdout: '',
1207
+ stderr: '',
1208
+ isStdoutTruncated: false,
1209
+ isStderrTruncated: false,
1210
+ },
1211
+ };
1212
+ });
1213
+
1214
+ // oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
1215
+ await $.classic.SessionStart({
1216
+ source: 'startup',
1217
+ session_id: 'session-1',
1218
+ cwd: '/repo',
1219
+ transcript_path: '/repo/transcript.jsonl',
1220
+ });
1221
+
1222
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'call-1', command: 'bun test' });
1223
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'call-2', command: 'git checkout -b x' });
1224
+
1225
+ expect(calls).toBe(0);
1226
+ });
@@ -2,6 +2,15 @@ import { buildPromptContext } from './build-prompt-context.ts';
2
2
  import { parseDecision } from './parse-decision.ts';
3
3
  import type { ModOn, ModOptions, PromptContext } from './types.ts';
4
4
 
5
+ // Commands that can create a worktree, a branch, or a PR. Any other call skips
6
+ // the record subprocess, so ordinary calls pay nothing for the session scope.
7
+ const SCOPE_COMMAND =
8
+ /\bgit\s[^\n]*?\b(?:worktree\s+add|checkout|switch|branch)\b|\bgh\s+pr\s+create\b/u;
9
+
10
+ // The record may look up a created PR's head branch with gh, which the CLI
11
+ // bounds at 5 seconds.
12
+ const RECORD_TIMEOUT_MS = 8000;
13
+
5
14
  export function register(on: ModOn, options: ModOptions): void {
6
15
  let sessionID: string | null = null;
7
16
 
@@ -52,18 +61,43 @@ export function register(on: ModOn, options: ModOptions): void {
52
61
  return result;
53
62
  });
54
63
 
55
- on('tool.call', async (_api, e, next) => {
64
+ on('tool.call', async ($, e, next) => {
56
65
  if (e.tool_use_id === undefined) {
57
66
  return next(e);
58
67
  }
59
68
 
60
69
  activeCalls.set(e.tool_use_id, e.agentId ?? null);
61
70
 
71
+ const command =
72
+ e.tool === 'Bash' && typeof e.command === 'string' && SCOPE_COMMAND.test(e.command)
73
+ ? e.command
74
+ : null;
75
+
76
+ const startedAt = Date.now();
77
+ const cwd = command === null || sessionID === null ? null : await $.session.cwd();
78
+ let result;
79
+
62
80
  try {
63
- return await next(e);
81
+ result = await next(e);
64
82
  } finally {
65
83
  activeCalls.delete(e.tool_use_id);
66
84
  }
85
+
86
+ if (command !== null && cwd !== null && sessionID !== null && result.deny === undefined) {
87
+ const executable = typeof options.command === 'string' ? options.command : 'auto-mode';
88
+ const request = { sessionID, cwd, startedAt, command, resultText: result.text ?? '' };
89
+
90
+ try {
91
+ await $.process.run([executable, 'record'], {
92
+ timeoutMs: RECORD_TIMEOUT_MS,
93
+ stdin: JSON.stringify(request),
94
+ });
95
+ } catch {
96
+ $.ui.log('auto-mode: session scope not recorded; subprocess failure', { to: 'debug' });
97
+ }
98
+ }
99
+
100
+ return result;
67
101
  });
68
102
 
69
103
  on('tool.check', async ($, e, next) => {
@@ -35,6 +35,14 @@ interface CallInput {
35
35
  readonly tool: string;
36
36
  readonly tool_use_id?: string;
37
37
  readonly agentId?: string;
38
+ readonly command?: unknown;
39
+ }
40
+
41
+ interface CallResult {
42
+ readonly result?: unknown;
43
+ readonly deny?: string;
44
+ readonly text?: string;
45
+ readonly isError?: true;
38
46
  }
39
47
 
40
48
  export interface UserTask {
@@ -70,7 +78,7 @@ interface ModEvents {
70
78
  readonly input: SpawnInput;
71
79
  readonly result: { readonly agentId?: string; readonly model?: string; readonly deny?: string };
72
80
  };
73
- readonly 'tool.call': { readonly input: CallInput; readonly result: object };
81
+ readonly 'tool.call': { readonly input: CallInput; readonly result: CallResult };
74
82
  readonly 'tool.check': { readonly input: CheckInput; readonly result: PermissionDecision };
75
83
  readonly 'process.run': {
76
84
  readonly input: ProcessInput;
@@ -100,7 +108,10 @@ export interface ModAPI {
100
108
  readonly run: (argv: readonly string[], init?: ProcessInput['init']) => Promise<ProcessResult>;
101
109
  };
102
110
  readonly prompt: { readonly submit: (input: PromptInput) => Promise<object> };
103
- readonly tool: { readonly check: (input: CheckInput) => Promise<PermissionDecision> };
111
+ readonly tool: {
112
+ readonly check: (input: CheckInput) => Promise<PermissionDecision>;
113
+ readonly call: (input: CallInput) => Promise<CallResult>;
114
+ };
104
115
  readonly classic: {
105
116
  readonly SessionStart: (
106
117
  input: Partial<SessionContext> & { readonly source: string },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A permission classifier library and Claude Code mod, so agents that are not Claude can run unattended",
5
5
  "keywords": [
6
6
  "agent",