agent-yes 1.31.41
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 +504 -0
- package/dist/agent-yes.js +2 -0
- package/dist/amp-yes.js +2 -0
- package/dist/auggie-yes.js +2 -0
- package/dist/claude-yes.js +2 -0
- package/dist/cli.js +31474 -0
- package/dist/cli.js.map +483 -0
- package/dist/codex-yes.js +2 -0
- package/dist/copilot-yes.js +2 -0
- package/dist/cursor-yes.js +2 -0
- package/dist/gemini-yes.js +2 -0
- package/dist/grok-yes.js +2 -0
- package/dist/index.js +25148 -0
- package/dist/index.js.map +435 -0
- package/dist/qwen-yes.js +2 -0
- package/package.json +145 -0
- package/ts/ReadyManager.spec.ts +72 -0
- package/ts/ReadyManager.ts +16 -0
- package/ts/SUPPORTED_CLIS.ts +5 -0
- package/ts/catcher.spec.ts +259 -0
- package/ts/catcher.ts +35 -0
- package/ts/cli-idle.spec.ts +20 -0
- package/ts/cli.ts +30 -0
- package/ts/defineConfig.ts +12 -0
- package/ts/idleWaiter.spec.ts +55 -0
- package/ts/idleWaiter.ts +31 -0
- package/ts/index.ts +783 -0
- package/ts/logger.ts +17 -0
- package/ts/parseCliArgs.spec.ts +231 -0
- package/ts/parseCliArgs.ts +182 -0
- package/ts/postbuild.ts +29 -0
- package/ts/pty-fix.ts +155 -0
- package/ts/pty.ts +18 -0
- package/ts/removeControlCharacters.spec.ts +73 -0
- package/ts/removeControlCharacters.ts +8 -0
- package/ts/runningLock.spec.ts +485 -0
- package/ts/runningLock.ts +362 -0
- package/ts/session-integration.spec.ts +93 -0
- package/ts/sleep.ts +3 -0
- package/ts/utils.spec.ts +169 -0
- package/ts/utils.ts +23 -0
package/dist/qwen-yes.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-yes",
|
|
3
|
+
"version": "1.31.41",
|
|
4
|
+
"description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"anthropic",
|
|
8
|
+
"assistant",
|
|
9
|
+
"auto-response",
|
|
10
|
+
"automation",
|
|
11
|
+
"claude",
|
|
12
|
+
"cli",
|
|
13
|
+
"codex",
|
|
14
|
+
"copilot",
|
|
15
|
+
"cursor",
|
|
16
|
+
"gemini",
|
|
17
|
+
"grok",
|
|
18
|
+
"qwen",
|
|
19
|
+
"wrapper"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/snomiao/agent-yes#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/snomiao/agent-yes/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "snomiao <snomiao@gmail.com>",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/snomiao/agent-yes.git"
|
|
30
|
+
},
|
|
31
|
+
"directories": {
|
|
32
|
+
"doc": "docs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"scripts",
|
|
37
|
+
"ts/*.ts"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"module": "ts/index.ts",
|
|
41
|
+
"types": "./ts/index.ts",
|
|
42
|
+
"exports": {
|
|
43
|
+
"types": "./ts/index.ts",
|
|
44
|
+
"import": "./dist/index.js"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org/"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "bun build ./ts/cli.ts ./ts/index.ts --outdir=dist --target=node --sourcemap --external=@snomiao/bun-pty --external=bun-pty --external=node-pty --external=from-node-stream --external=bun",
|
|
52
|
+
"postbuild": "bun ./ts/postbuild.ts",
|
|
53
|
+
"demo": "bun run build && bun link && claude-yes -- demo",
|
|
54
|
+
"dev": "bun ts/index.ts",
|
|
55
|
+
"fmt": "oxlint --fix --fix-suggestions && oxfmt",
|
|
56
|
+
"_prepack": "bun run build",
|
|
57
|
+
"prepare": "husky",
|
|
58
|
+
"release": "standard-version && npm publish",
|
|
59
|
+
"release:beta": "standard-version && npm publish --tag beta",
|
|
60
|
+
"test": "bun test --coverage"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
64
|
+
"@snomiao/bun-pty": "^0.3.4",
|
|
65
|
+
"bun-pty": "^0.4.8",
|
|
66
|
+
"cpu-wait": "^0.0.10",
|
|
67
|
+
"execa": "^9.6.0",
|
|
68
|
+
"from-node-stream": "^0.1.2",
|
|
69
|
+
"ink": "^6.6.0",
|
|
70
|
+
"ms": "^2.1.3",
|
|
71
|
+
"openai": "^6.16.0",
|
|
72
|
+
"p-map": "^7.0.3",
|
|
73
|
+
"phpdie": "^1.7.0",
|
|
74
|
+
"rambda": "^10.3.2",
|
|
75
|
+
"sflow": "^1.20.2",
|
|
76
|
+
"strip-ansi-control-characters": "^2.0.0",
|
|
77
|
+
"terminal-render": "^1.2.2",
|
|
78
|
+
"tsa-composer": "^3.0.3",
|
|
79
|
+
"winston": "^3.19.0",
|
|
80
|
+
"yargs": "^18.0.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
84
|
+
"@semantic-release/exec": "^7.1.0",
|
|
85
|
+
"@semantic-release/git": "^10.0.1",
|
|
86
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
87
|
+
"@types/bun": "^1.2.18",
|
|
88
|
+
"@types/jest": "^30.0.0",
|
|
89
|
+
"@types/ms": "^2.1.0",
|
|
90
|
+
"@types/node": "^25.0.3",
|
|
91
|
+
"@types/yargs": "^17.0.33",
|
|
92
|
+
"husky": "^9.1.7",
|
|
93
|
+
"lint-staged": "^16.1.4",
|
|
94
|
+
"node-pty": "^1.1.0",
|
|
95
|
+
"oxfmt": "^0.23.0",
|
|
96
|
+
"oxlint": "^1.38.0",
|
|
97
|
+
"semantic-release": "^24.2.9",
|
|
98
|
+
"standard-version": "^9.5.0",
|
|
99
|
+
"vitest": "^3.2.4"
|
|
100
|
+
},
|
|
101
|
+
"peerDependencies": {
|
|
102
|
+
"node-pty": "latest",
|
|
103
|
+
"typescript": "^5.8.3"
|
|
104
|
+
},
|
|
105
|
+
"peerDependenciesMeta": {
|
|
106
|
+
"node-pty": {
|
|
107
|
+
"optional": true
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"engines": {
|
|
111
|
+
"node": ">=22.0.0"
|
|
112
|
+
},
|
|
113
|
+
"_release": {
|
|
114
|
+
"branches": [
|
|
115
|
+
"main"
|
|
116
|
+
],
|
|
117
|
+
"plugins": [
|
|
118
|
+
"@semantic-release/commit-analyzer",
|
|
119
|
+
"@semantic-release/release-notes-generator",
|
|
120
|
+
"@semantic-release/changelog",
|
|
121
|
+
"@semantic-release/npm",
|
|
122
|
+
[
|
|
123
|
+
"@semantic-release/exec",
|
|
124
|
+
{
|
|
125
|
+
"publishCmd": "npm pkg set name=claude-yes"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"@semantic-release/npm",
|
|
129
|
+
"@semantic-release/git",
|
|
130
|
+
"@semantic-release/github"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"bin": {
|
|
134
|
+
"qwen-yes": "./dist/qwen-yes.js",
|
|
135
|
+
"grok-yes": "./dist/grok-yes.js",
|
|
136
|
+
"claude-yes": "./dist/claude-yes.js",
|
|
137
|
+
"gemini-yes": "./dist/gemini-yes.js",
|
|
138
|
+
"codex-yes": "./dist/codex-yes.js",
|
|
139
|
+
"copilot-yes": "./dist/copilot-yes.js",
|
|
140
|
+
"cursor-yes": "./dist/cursor-yes.js",
|
|
141
|
+
"auggie-yes": "./dist/auggie-yes.js",
|
|
142
|
+
"agent-yes": "./dist/agent-yes.js",
|
|
143
|
+
"amp-yes": "./dist/amp-yes.js"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ReadyManager } from "./ReadyManager";
|
|
3
|
+
|
|
4
|
+
describe("ReadyManager", () => {
|
|
5
|
+
it("should start in not ready state", () => {
|
|
6
|
+
const manager = new ReadyManager();
|
|
7
|
+
expect(manager.wait()).toBeInstanceOf(Promise);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should resolve wait when ready is called", async () => {
|
|
11
|
+
const manager = new ReadyManager();
|
|
12
|
+
const waitPromise = manager.wait();
|
|
13
|
+
|
|
14
|
+
manager.ready();
|
|
15
|
+
|
|
16
|
+
await expect(waitPromise).resolves.toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should resolve immediately if already ready", async () => {
|
|
20
|
+
const manager = new ReadyManager();
|
|
21
|
+
manager.ready();
|
|
22
|
+
|
|
23
|
+
const result = manager.wait();
|
|
24
|
+
expect(result).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should handle multiple waiters", async () => {
|
|
28
|
+
const manager = new ReadyManager();
|
|
29
|
+
const wait1 = manager.wait();
|
|
30
|
+
const wait2 = manager.wait();
|
|
31
|
+
const wait3 = manager.wait();
|
|
32
|
+
|
|
33
|
+
manager.ready();
|
|
34
|
+
|
|
35
|
+
await Promise.all([
|
|
36
|
+
expect(wait1).resolves.toBeUndefined(),
|
|
37
|
+
expect(wait2).resolves.toBeUndefined(),
|
|
38
|
+
expect(wait3).resolves.toBeUndefined(),
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should reset to not ready when unready is called", async () => {
|
|
43
|
+
const manager = new ReadyManager();
|
|
44
|
+
manager.ready();
|
|
45
|
+
manager.unready();
|
|
46
|
+
|
|
47
|
+
expect(manager.wait()).toBeInstanceOf(Promise);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle ready with no waiting queue", () => {
|
|
51
|
+
const manager = new ReadyManager();
|
|
52
|
+
manager.ready(); // Should not throw even if no one is waiting
|
|
53
|
+
expect(manager.wait()).toBeUndefined(); // Should be ready now
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle multiple ready/unready cycles", async () => {
|
|
57
|
+
const manager = new ReadyManager();
|
|
58
|
+
|
|
59
|
+
// First cycle
|
|
60
|
+
const wait1 = manager.wait();
|
|
61
|
+
manager.ready();
|
|
62
|
+
await wait1;
|
|
63
|
+
|
|
64
|
+
// Reset
|
|
65
|
+
manager.unready();
|
|
66
|
+
|
|
67
|
+
// Second cycle
|
|
68
|
+
const wait2 = manager.wait();
|
|
69
|
+
manager.ready();
|
|
70
|
+
await expect(wait2).resolves.toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class ReadyManager {
|
|
2
|
+
isReady = false;
|
|
3
|
+
private readyQueue: (() => void)[] = [];
|
|
4
|
+
wait() {
|
|
5
|
+
if (this.isReady) return;
|
|
6
|
+
return new Promise<void>((resolve) => this.readyQueue.push(resolve));
|
|
7
|
+
}
|
|
8
|
+
unready() {
|
|
9
|
+
this.isReady = false;
|
|
10
|
+
}
|
|
11
|
+
ready() {
|
|
12
|
+
this.isReady = true;
|
|
13
|
+
if (!this.readyQueue.length) return; // check len for performance
|
|
14
|
+
this.readyQueue.splice(0).map((resolve) => resolve());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { catcher } from "./catcher";
|
|
3
|
+
|
|
4
|
+
describe("catcher", () => {
|
|
5
|
+
describe("curried overload", () => {
|
|
6
|
+
it("should return a function when called with only catchFn", () => {
|
|
7
|
+
const catchFn = () => "error";
|
|
8
|
+
const result = catcher(catchFn);
|
|
9
|
+
expect(typeof result).toBe("function");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should catch errors and call catchFn with error, function, and args", () => {
|
|
13
|
+
let catchedError: unknown;
|
|
14
|
+
let catchedFn: unknown;
|
|
15
|
+
let catchedArgs: unknown[];
|
|
16
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
17
|
+
catchedError = error;
|
|
18
|
+
catchedFn = fn;
|
|
19
|
+
catchedArgs = args;
|
|
20
|
+
return "caught";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let calledArgs: unknown[] = [];
|
|
24
|
+
const errorFn = (...args: unknown[]) => {
|
|
25
|
+
calledArgs = args;
|
|
26
|
+
throw new Error("test error");
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const wrappedFn = catcher(catchFn)(errorFn);
|
|
30
|
+
const result = wrappedFn("arg1", "arg2");
|
|
31
|
+
|
|
32
|
+
expect(result).toBe("caught");
|
|
33
|
+
expect(catchedError).toBeInstanceOf(Error);
|
|
34
|
+
expect(catchedFn).toBe(errorFn);
|
|
35
|
+
expect(catchedArgs).toEqual(["arg1", "arg2"]);
|
|
36
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return normal result when no error occurs", () => {
|
|
40
|
+
let catchCalled = false;
|
|
41
|
+
const catchFn = () => {
|
|
42
|
+
catchCalled = true;
|
|
43
|
+
return "error";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let calledArgs: unknown[] = [];
|
|
47
|
+
const normalFn = (...args: unknown[]) => {
|
|
48
|
+
calledArgs = args;
|
|
49
|
+
return "success";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const wrappedFn = catcher(catchFn)(normalFn);
|
|
53
|
+
const result = wrappedFn("arg1", "arg2");
|
|
54
|
+
|
|
55
|
+
expect(result).toBe("success");
|
|
56
|
+
expect(catchCalled).toBe(false);
|
|
57
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("direct overload", () => {
|
|
62
|
+
it("should catch errors and call catchFn with error, function, and args directly", () => {
|
|
63
|
+
let catchedError: unknown;
|
|
64
|
+
let catchedFn: unknown;
|
|
65
|
+
let catchedArgs: unknown[];
|
|
66
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
67
|
+
catchedError = error;
|
|
68
|
+
catchedFn = fn;
|
|
69
|
+
catchedArgs = args;
|
|
70
|
+
return "caught";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let calledArgs: unknown[] = [];
|
|
74
|
+
const errorFn = (...args: unknown[]) => {
|
|
75
|
+
calledArgs = args;
|
|
76
|
+
throw new Error("test error");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const wrappedFn = catcher(catchFn, errorFn);
|
|
80
|
+
const result = wrappedFn("arg1", "arg2");
|
|
81
|
+
|
|
82
|
+
expect(result).toBe("caught");
|
|
83
|
+
expect(catchedError).toBeInstanceOf(Error);
|
|
84
|
+
expect(catchedFn).toBe(errorFn);
|
|
85
|
+
expect(catchedArgs).toEqual(["arg1", "arg2"]);
|
|
86
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return normal result when no error occurs directly", () => {
|
|
90
|
+
let catchCalled = false;
|
|
91
|
+
const catchFn = () => {
|
|
92
|
+
catchCalled = true;
|
|
93
|
+
return "error";
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let calledArgs: unknown[] = [];
|
|
97
|
+
const normalFn = (...args: unknown[]) => {
|
|
98
|
+
calledArgs = args;
|
|
99
|
+
return "success";
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const wrappedFn = catcher(catchFn, normalFn);
|
|
103
|
+
const result = wrappedFn("arg1", "arg2");
|
|
104
|
+
|
|
105
|
+
expect(result).toBe("success");
|
|
106
|
+
expect(catchCalled).toBe(false);
|
|
107
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("error handling", () => {
|
|
112
|
+
it("should handle different error types and pass function context", () => {
|
|
113
|
+
const results: unknown[] = [];
|
|
114
|
+
const functions: unknown[] = [];
|
|
115
|
+
const catchFn = (error: unknown, fn: unknown, ..._args: unknown[]) => {
|
|
116
|
+
results.push(error);
|
|
117
|
+
functions.push(fn);
|
|
118
|
+
return "handled";
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// String error
|
|
122
|
+
const stringErrorFn = () => {
|
|
123
|
+
throw "string error";
|
|
124
|
+
};
|
|
125
|
+
const wrappedStringFn = catcher(catchFn, stringErrorFn);
|
|
126
|
+
expect(wrappedStringFn()).toBe("handled");
|
|
127
|
+
expect(results[0]).toBe("string error");
|
|
128
|
+
expect(functions[0]).toBe(stringErrorFn);
|
|
129
|
+
|
|
130
|
+
// Object error
|
|
131
|
+
const objectError = { message: "object error" };
|
|
132
|
+
const objectErrorFn = () => {
|
|
133
|
+
throw objectError;
|
|
134
|
+
};
|
|
135
|
+
const wrappedObjectFn = catcher(catchFn, objectErrorFn);
|
|
136
|
+
expect(wrappedObjectFn()).toBe("handled");
|
|
137
|
+
expect(results[1]).toBe(objectError);
|
|
138
|
+
expect(functions[1]).toBe(objectErrorFn);
|
|
139
|
+
|
|
140
|
+
// null error
|
|
141
|
+
const nullErrorFn = () => {
|
|
142
|
+
throw null;
|
|
143
|
+
};
|
|
144
|
+
const wrappedNullFn = catcher(catchFn, nullErrorFn);
|
|
145
|
+
expect(wrappedNullFn()).toBe("handled");
|
|
146
|
+
expect(results[2]).toBe(null);
|
|
147
|
+
expect(functions[2]).toBe(nullErrorFn);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should preserve function parameters and pass them to catchFn", () => {
|
|
151
|
+
let caughtError: unknown;
|
|
152
|
+
let caughtFn: unknown;
|
|
153
|
+
let caughtArgs: unknown[];
|
|
154
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
155
|
+
caughtError = error;
|
|
156
|
+
caughtFn = fn;
|
|
157
|
+
caughtArgs = args;
|
|
158
|
+
return "caught";
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let testArgs: [number, string, boolean] | undefined;
|
|
162
|
+
const testFn = (a: number, b: string, c: boolean) => {
|
|
163
|
+
testArgs = [a, b, c];
|
|
164
|
+
if (a > 5) throw new Error("too big");
|
|
165
|
+
return `${a}-${b}-${c}`;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const wrappedFn = catcher(catchFn, testFn);
|
|
169
|
+
|
|
170
|
+
// Normal execution
|
|
171
|
+
expect(wrappedFn(3, "test", true)).toBe("3-test-true");
|
|
172
|
+
expect(testArgs).toEqual([3, "test", true]);
|
|
173
|
+
|
|
174
|
+
// Error execution
|
|
175
|
+
expect(wrappedFn(10, "error", false)).toBe("caught");
|
|
176
|
+
expect(testArgs).toEqual([10, "error", false]);
|
|
177
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
178
|
+
expect(caughtFn).toBe(testFn);
|
|
179
|
+
expect(caughtArgs).toEqual([10, "error", false]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should handle functions with no parameters", () => {
|
|
183
|
+
let caughtError: unknown;
|
|
184
|
+
let caughtFn: unknown;
|
|
185
|
+
let caughtArgs: unknown[];
|
|
186
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
187
|
+
caughtError = error;
|
|
188
|
+
caughtFn = fn;
|
|
189
|
+
caughtArgs = args;
|
|
190
|
+
return "no params caught";
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let called = false;
|
|
194
|
+
const noParamsFn = () => {
|
|
195
|
+
called = true;
|
|
196
|
+
throw new Error("no params error");
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const wrappedFn = catcher(catchFn, noParamsFn);
|
|
200
|
+
const result = wrappedFn();
|
|
201
|
+
|
|
202
|
+
expect(result).toBe("no params caught");
|
|
203
|
+
expect(called).toBe(true);
|
|
204
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
205
|
+
expect(caughtFn).toBe(noParamsFn);
|
|
206
|
+
expect(caughtArgs).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should handle functions returning different types", () => {
|
|
210
|
+
const catchFn = () => null;
|
|
211
|
+
|
|
212
|
+
// Function returning number
|
|
213
|
+
const numberFn = catcher(catchFn, () => 42);
|
|
214
|
+
expect(numberFn()).toBe(42);
|
|
215
|
+
|
|
216
|
+
// Function returning object
|
|
217
|
+
const obj = { key: "value" };
|
|
218
|
+
const objectFn = catcher(catchFn, () => obj);
|
|
219
|
+
expect(objectFn()).toBe(obj);
|
|
220
|
+
|
|
221
|
+
// Function returning undefined
|
|
222
|
+
const undefinedFn = catcher(catchFn, () => undefined);
|
|
223
|
+
expect(undefinedFn()).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("type safety", () => {
|
|
228
|
+
it("should maintain function signature", () => {
|
|
229
|
+
const catchFn = (_error: unknown, _fn: unknown, ..._args: unknown[]) => "error";
|
|
230
|
+
const originalFn = (a: number, b: string): string => `${a}-${b}`;
|
|
231
|
+
|
|
232
|
+
const wrappedFn = catcher(catchFn, originalFn);
|
|
233
|
+
|
|
234
|
+
// This should be type-safe
|
|
235
|
+
const result: string = wrappedFn(1, "test");
|
|
236
|
+
expect(result).toBe("1-test");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should pass function reference and arguments to catchFn", () => {
|
|
240
|
+
let capturedFn: unknown;
|
|
241
|
+
let capturedArgs: unknown[];
|
|
242
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
243
|
+
capturedFn = fn;
|
|
244
|
+
capturedArgs = args;
|
|
245
|
+
return "handled";
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const testFn = (_x: number, _y: string) => {
|
|
249
|
+
throw new Error("test");
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const wrappedFn = catcher(catchFn, testFn);
|
|
253
|
+
wrappedFn(42, "hello");
|
|
254
|
+
|
|
255
|
+
expect(capturedFn).toBe(testFn);
|
|
256
|
+
expect(capturedArgs).toEqual([42, "hello"]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
package/ts/catcher.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// curried overload
|
|
2
|
+
export function catcher<F extends (...args: any[]) => any, R>(
|
|
3
|
+
catchFn: (error: unknown, fn: F, ...args: Parameters<F>) => R,
|
|
4
|
+
): (fn: F) => (...args: Parameters<F>) => ReturnType<F> | R;
|
|
5
|
+
|
|
6
|
+
// direct overload
|
|
7
|
+
export function catcher<F extends (...args: any[]) => any, R>(
|
|
8
|
+
catchFn: (error: unknown, fn: F, ...args: Parameters<F>) => R,
|
|
9
|
+
fn: F,
|
|
10
|
+
): (...args: Parameters<F>) => ReturnType<F> | R;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A utility function to wrap another function with a try-catch block.
|
|
14
|
+
* If an error occurs during the execution of the function, the provided
|
|
15
|
+
* catchFn is called with the error, the original function, and its arguments.
|
|
16
|
+
*
|
|
17
|
+
* This function supports both direct invocation and curried usage.
|
|
18
|
+
*
|
|
19
|
+
* @param catchFn - The function to call when an error occurs.
|
|
20
|
+
* @param fn - The function to wrap (optional for curried usage).
|
|
21
|
+
* @returns A new function that wraps the original function with error handling.
|
|
22
|
+
*/
|
|
23
|
+
export function catcher<F extends (...args: any[]) => any, R>(
|
|
24
|
+
catchFn: (error: unknown, fn: F, ...args: Parameters<F>) => R,
|
|
25
|
+
fn?: F,
|
|
26
|
+
) {
|
|
27
|
+
if (!fn) return (fn: F) => catcher(catchFn, fn) as any;
|
|
28
|
+
return (...args: Parameters<F>) => {
|
|
29
|
+
try {
|
|
30
|
+
return fn(...args);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return catchFn(error, fn, ...args);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { fromStdio } from "from-node-stream";
|
|
3
|
+
import sflow from "sflow";
|
|
4
|
+
import { describe, it } from "vitest";
|
|
5
|
+
import { sleepms } from "./utils";
|
|
6
|
+
|
|
7
|
+
describe("CLI idle tests", () => {
|
|
8
|
+
// 2025-08-11 ok
|
|
9
|
+
it.skip("CLI --exit-on-idle flag with custom timeout", async () => {
|
|
10
|
+
const p = exec(
|
|
11
|
+
`bunx tsx ./ts/cli.ts claude --verbose --logFile=./cli-idle.log --exit-on-idle=3s "say hello and wait"`,
|
|
12
|
+
);
|
|
13
|
+
const tr = new TransformStream<string, string>();
|
|
14
|
+
const output = await sflow(tr.readable).by(fromStdio(p)).log().text();
|
|
15
|
+
console.log(output);
|
|
16
|
+
expect(output).toContain("hello");
|
|
17
|
+
await sleepms(1000); // wait for process exit
|
|
18
|
+
expect(p.exitCode).toBe(0);
|
|
19
|
+
}, 20e3);
|
|
20
|
+
});
|
package/ts/cli.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { argv } from "process";
|
|
3
|
+
import cliYesConfig from "../agent-yes.config.ts";
|
|
4
|
+
import { parseCliArgs } from "./parseCliArgs.ts";
|
|
5
|
+
import { logger } from "./logger.ts";
|
|
6
|
+
|
|
7
|
+
// Import the CLI module
|
|
8
|
+
|
|
9
|
+
// Parse CLI arguments
|
|
10
|
+
const config = parseCliArgs(process.argv);
|
|
11
|
+
|
|
12
|
+
// Validate CLI name
|
|
13
|
+
if (!config.cli) {
|
|
14
|
+
logger.error(process.argv)
|
|
15
|
+
logger.error("Error: No CLI name provided.");
|
|
16
|
+
throw new Error(`missing cli def, available clis: ${Object.keys((await cliYesConfig).clis).join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// console.log(`Using CLI: ${config.cli}`);
|
|
20
|
+
|
|
21
|
+
if (config.verbose) {
|
|
22
|
+
process.env.VERBOSE = "true"; // enable verbose logging in yesLog.ts
|
|
23
|
+
console.log(config);
|
|
24
|
+
console.log(argv);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { default: cliYes } = await import("./index.ts");
|
|
28
|
+
const { exitCode } = await cliYes(config);
|
|
29
|
+
console.log("exiting process");
|
|
30
|
+
process.exit(exitCode ?? 1);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentCliConfig, AgentYesConfig } from "./index.ts";
|
|
2
|
+
|
|
3
|
+
type Awaitable<T> = T | Promise<T>;
|
|
4
|
+
export async function defineCliYesConfig<T extends AgentYesConfig>(
|
|
5
|
+
cfg: Awaitable<T> | ((original: T) => Awaitable<T>),
|
|
6
|
+
) {
|
|
7
|
+
if (typeof cfg === "function") cfg = await cfg({ clis: {} } as T);
|
|
8
|
+
|
|
9
|
+
return cfg as unknown as Omit<AgentYesConfig, "clis"> & {
|
|
10
|
+
clis: Record<string, AgentCliConfig>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { IdleWaiter } from "./idleWaiter";
|
|
3
|
+
|
|
4
|
+
describe("IdleWaiter", () => {
|
|
5
|
+
it("should initialize with current time", () => {
|
|
6
|
+
const waiter = new IdleWaiter();
|
|
7
|
+
expect(waiter.lastActivityTime).toBeCloseTo(Date.now(), -2);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should update lastActivityTime when ping is called", () => {
|
|
11
|
+
const waiter = new IdleWaiter();
|
|
12
|
+
const initialTime = waiter.lastActivityTime;
|
|
13
|
+
|
|
14
|
+
// Wait a small amount
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
while (Date.now() - start < 10) {
|
|
17
|
+
// busy wait
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
waiter.ping();
|
|
21
|
+
expect(waiter.lastActivityTime).toBeGreaterThan(initialTime);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return this when ping is called for chaining", () => {
|
|
25
|
+
const waiter = new IdleWaiter();
|
|
26
|
+
expect(waiter.ping()).toBe(waiter);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should resolve wait immediately when already idle", async () => {
|
|
30
|
+
const waiter = new IdleWaiter();
|
|
31
|
+
|
|
32
|
+
// Wait enough time to be considered idle
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
while (Date.now() - start < 50) {
|
|
35
|
+
// busy wait
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// This should resolve quickly since enough time has passed
|
|
39
|
+
const waitPromise = waiter.wait(10);
|
|
40
|
+
await expect(waitPromise).resolves.toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should respect custom check interval", () => {
|
|
44
|
+
const waiter = new IdleWaiter();
|
|
45
|
+
waiter.checkInterval = 200;
|
|
46
|
+
|
|
47
|
+
expect(waiter.checkInterval).toBe(200);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should have ping method that chains", () => {
|
|
51
|
+
const waiter = new IdleWaiter();
|
|
52
|
+
const result = waiter.ping().ping().ping();
|
|
53
|
+
expect(result).toBe(waiter);
|
|
54
|
+
});
|
|
55
|
+
});
|
package/ts/idleWaiter.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A utility class to wait for idle periods based on activity pings.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const idleWaiter = new IdleWaiter();
|
|
6
|
+
*
|
|
7
|
+
* // Somewhere in your code, when activity occurs:
|
|
8
|
+
* idleWaiter.ping();
|
|
9
|
+
*
|
|
10
|
+
* // To wait for an idle period of 5 seconds:
|
|
11
|
+
* await idleWaiter.wait(5000);
|
|
12
|
+
* console.log('System has been idle for 5 seconds');
|
|
13
|
+
*/
|
|
14
|
+
export class IdleWaiter {
|
|
15
|
+
lastActivityTime = Date.now();
|
|
16
|
+
checkInterval = 100; // Default check interval in milliseconds
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.ping();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ping() {
|
|
23
|
+
this.lastActivityTime = Date.now();
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async wait(ms: number) {
|
|
28
|
+
while (this.lastActivityTime >= Date.now() - ms)
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, this.checkInterval));
|
|
30
|
+
}
|
|
31
|
+
}
|