@gianfrancopiana/openclaw-autoresearch 1.0.0 → 1.0.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 +21 -9
- package/RELEASING.md +43 -0
- package/extensions/openclaw-autoresearch/src/execute.ts +30 -81
- package/extensions/openclaw-autoresearch/src/git.ts +51 -14
- package/extensions/openclaw-autoresearch/src/tools/log-experiment.ts +2 -1
- package/extensions/openclaw-autoresearch/src/tools/run-experiment.ts +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -31,27 +31,37 @@ The design is file-first: any agent can pick up the repo-root files and continue
|
|
|
31
31
|
|
|
32
32
|
## Install
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Requires OpenClaw `2026.3.13` or newer.
|
|
35
|
+
|
|
36
|
+
Use OpenClaw's plugin installer:
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
|
-
openclaw plugins install
|
|
39
|
+
openclaw plugins install @gianfrancopiana/openclaw-autoresearch
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
If you're running from a local OpenClaw checkout, use:
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
|
-
|
|
44
|
-
npm pack
|
|
45
|
-
openclaw plugins install ./gianfrancopiana-openclaw-autoresearch-1.0.0.tgz
|
|
45
|
+
pnpm openclaw plugins install @gianfrancopiana/openclaw-autoresearch
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
For local plugin development, link your working copy instead of copying files:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
openclaw plugins install
|
|
51
|
+
openclaw plugins install --link /absolute/path/to/openclaw-autoresearch
|
|
52
|
+
# or from a local OpenClaw checkout:
|
|
53
|
+
# pnpm openclaw plugins install --link /absolute/path/to/openclaw-autoresearch
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a packaged local install, build the tarball and install that artifact:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
npm pack
|
|
61
|
+
openclaw plugins install ./gianfrancopiana-openclaw-autoresearch-<version>.tgz
|
|
52
62
|
```
|
|
53
63
|
|
|
54
|
-
The installer reads `package.json#openclaw.extensions`, loads the root [`index.ts`](index.ts), and discovers the manifest in [`openclaw.plugin.json`](openclaw.plugin.json).
|
|
64
|
+
The install command records the plugin, enables it, and exposes the plugin surfaces on restart. The installer reads `package.json#openclaw.extensions`, loads the root [`index.ts`](index.ts), and discovers the manifest in [`openclaw.plugin.json`](openclaw.plugin.json).
|
|
55
65
|
|
|
56
66
|
Verify:
|
|
57
67
|
|
|
@@ -100,6 +110,8 @@ npm run validate
|
|
|
100
110
|
npm run release:verify
|
|
101
111
|
```
|
|
102
112
|
|
|
113
|
+
Release instructions, including npm 2FA publishing, live in [`RELEASING.md`](RELEASING.md).
|
|
114
|
+
|
|
103
115
|
The local test shim supports typechecking and tests without a full OpenClaw host checkout. Runtime behavior depends on a real OpenClaw host.
|
|
104
116
|
|
|
105
117
|
## License
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- npm access to `@gianfrancopiana`
|
|
6
|
+
- either npm account 2FA enabled for publishing, or a granular access token with `Bypass 2FA` enabled
|
|
7
|
+
|
|
8
|
+
## Release
|
|
9
|
+
|
|
10
|
+
1. Update `package.json` and `openclaw.plugin.json` if you are changing the version.
|
|
11
|
+
2. Run the release checks:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run release:verify
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. Publish:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm publish --otp=123456
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Replace `123456` with the current code from your authenticator app.
|
|
25
|
+
|
|
26
|
+
4. Verify install:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
openclaw plugins install @gianfrancopiana/openclaw-autoresearch
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For a local OpenClaw checkout:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm openclaw plugins install @gianfrancopiana/openclaw-autoresearch
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Common failure
|
|
39
|
+
|
|
40
|
+
If `npm publish` fails with `E403` and mentions 2FA or bypass tokens, the current auth on this machine is not sufficient to publish. Either:
|
|
41
|
+
|
|
42
|
+
- re-run `npm publish --otp=<current-code>`
|
|
43
|
+
- or switch to a granular npm token with publish rights and `Bypass 2FA` enabled
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
3
|
const OUTPUT_TAIL_LINES = 80;
|
|
4
4
|
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
5
|
-
const
|
|
5
|
+
const NO_TIMEOUT_MS = 2_147_483_647;
|
|
6
|
+
|
|
7
|
+
type RunCommandWithTimeout = OpenClawPluginApi["runtime"]["system"]["runCommandWithTimeout"];
|
|
6
8
|
|
|
7
9
|
export type ExperimentExecutionResult = {
|
|
8
10
|
readonly command: string;
|
|
@@ -17,95 +19,42 @@ export type ExperimentExecutionResult = {
|
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export async function executeExperimentCommand(options: {
|
|
22
|
+
runCommandWithTimeout: RunCommandWithTimeout;
|
|
20
23
|
command: string;
|
|
21
24
|
cwd: string;
|
|
22
25
|
timeoutSeconds?: number;
|
|
23
26
|
signal?: AbortSignal;
|
|
24
27
|
}): Promise<ExperimentExecutionResult> {
|
|
25
28
|
const timeoutSeconds = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
26
|
-
const timeoutMs =
|
|
29
|
+
const timeoutMs =
|
|
30
|
+
timeoutSeconds > 0 ? Math.max(1, Math.floor(timeoutSeconds * 1_000)) : NO_TIMEOUT_MS;
|
|
27
31
|
const startedAt = Date.now();
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let stdout = "";
|
|
36
|
-
let stderr = "";
|
|
37
|
-
let timedOut = false;
|
|
38
|
-
let forceKillTimer: NodeJS.Timeout | undefined;
|
|
39
|
-
|
|
40
|
-
const timeoutTimer =
|
|
41
|
-
timeoutMs > 0
|
|
42
|
-
? setTimeout(() => {
|
|
43
|
-
timedOut = true;
|
|
44
|
-
child.kill("SIGTERM");
|
|
45
|
-
forceKillTimer = setTimeout(() => {
|
|
46
|
-
if (!child.killed) {
|
|
47
|
-
child.kill("SIGKILL");
|
|
48
|
-
}
|
|
49
|
-
}, FORCE_KILL_GRACE_MS);
|
|
50
|
-
}, timeoutMs)
|
|
51
|
-
: undefined;
|
|
52
|
-
|
|
53
|
-
const abortHandler = () => {
|
|
54
|
-
child.kill("SIGTERM");
|
|
55
|
-
forceKillTimer = setTimeout(() => {
|
|
56
|
-
if (!child.killed) {
|
|
57
|
-
child.kill("SIGKILL");
|
|
58
|
-
}
|
|
59
|
-
}, FORCE_KILL_GRACE_MS);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
if (options.signal) {
|
|
63
|
-
if (options.signal.aborted) {
|
|
64
|
-
abortHandler();
|
|
65
|
-
} else {
|
|
66
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
child.stdout.on("data", (chunk: string | Buffer) => {
|
|
71
|
-
stdout += chunk.toString();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
child.stderr.on("data", (chunk: string | Buffer) => {
|
|
75
|
-
stderr += chunk.toString();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
child.on("error", (error) => {
|
|
79
|
-
stderr += `${stderr ? "\n" : ""}${String(error.message || error)}`;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
child.on("close", (code) => {
|
|
83
|
-
if (timeoutTimer) {
|
|
84
|
-
clearTimeout(timeoutTimer);
|
|
85
|
-
}
|
|
86
|
-
if (forceKillTimer) {
|
|
87
|
-
clearTimeout(forceKillTimer);
|
|
88
|
-
}
|
|
89
|
-
if (options.signal) {
|
|
90
|
-
options.signal.removeEventListener("abort", abortHandler);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const durationSeconds = (Date.now() - startedAt) / 1_000;
|
|
94
|
-
const passed = code === 0 && !timedOut;
|
|
33
|
+
if (options.signal?.aborted) {
|
|
34
|
+
const error = new Error("Experiment run aborted before start.");
|
|
35
|
+
error.name = "AbortError";
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
95
38
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
durationSeconds,
|
|
100
|
-
passed,
|
|
101
|
-
crashed: !passed,
|
|
102
|
-
timedOut,
|
|
103
|
-
tailOutput: createOutputTail(stdout, stderr),
|
|
104
|
-
stdout,
|
|
105
|
-
stderr,
|
|
106
|
-
});
|
|
107
|
-
});
|
|
39
|
+
const result = await options.runCommandWithTimeout(["bash", "-c", options.command], {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
timeoutMs,
|
|
108
42
|
});
|
|
43
|
+
const durationSeconds = (Date.now() - startedAt) / 1_000;
|
|
44
|
+
const timedOut = result.termination === "timeout";
|
|
45
|
+
const passed = result.code === 0 && result.termination === "exit";
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
command: options.command,
|
|
49
|
+
exitCode: result.code,
|
|
50
|
+
durationSeconds,
|
|
51
|
+
passed,
|
|
52
|
+
crashed: !passed,
|
|
53
|
+
timedOut,
|
|
54
|
+
tailOutput: createOutputTail(result.stdout, result.stderr),
|
|
55
|
+
stdout: result.stdout,
|
|
56
|
+
stderr: result.stderr,
|
|
57
|
+
};
|
|
109
58
|
}
|
|
110
59
|
|
|
111
60
|
function createOutputTail(stdout: string, stderr: string): string {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
4
|
+
|
|
5
|
+
type RunCommandWithTimeout = OpenClawPluginApi["runtime"]["system"]["runCommandWithTimeout"];
|
|
2
6
|
|
|
3
7
|
export type GitCommandResult = {
|
|
4
8
|
readonly code: number | null;
|
|
@@ -15,24 +19,29 @@ export type GitKeepResult = {
|
|
|
15
19
|
readonly command: GitCommandResult;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
function runGitCommand(
|
|
19
|
-
|
|
22
|
+
async function runGitCommand(
|
|
23
|
+
runCommandWithTimeout: RunCommandWithTimeout,
|
|
24
|
+
cwd: string,
|
|
25
|
+
args: readonly string[],
|
|
26
|
+
): Promise<GitCommandResult> {
|
|
27
|
+
const result = await runCommandWithTimeout(["git", ...args], {
|
|
20
28
|
cwd,
|
|
21
|
-
|
|
29
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
22
30
|
});
|
|
23
|
-
|
|
24
31
|
const stdout = result.stdout ?? "";
|
|
25
32
|
const stderr = result.stderr ?? "";
|
|
33
|
+
const combinedOutput = `${stdout}${stderr}`.trim() || describeTermination(result.termination);
|
|
26
34
|
|
|
27
35
|
return {
|
|
28
|
-
code: result.
|
|
36
|
+
code: result.code,
|
|
29
37
|
stdout,
|
|
30
38
|
stderr,
|
|
31
|
-
combinedOutput
|
|
39
|
+
combinedOutput,
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
export function commitKeptExperiment(options: {
|
|
43
|
+
export async function commitKeptExperiment(options: {
|
|
44
|
+
runCommandWithTimeout: RunCommandWithTimeout;
|
|
36
45
|
cwd: string;
|
|
37
46
|
description: string;
|
|
38
47
|
metricName: string;
|
|
@@ -40,7 +49,7 @@ export function commitKeptExperiment(options: {
|
|
|
40
49
|
metrics: Record<string, number>;
|
|
41
50
|
commit: string;
|
|
42
51
|
status: "keep";
|
|
43
|
-
}): GitKeepResult {
|
|
52
|
+
}): Promise<GitKeepResult> {
|
|
44
53
|
const resultData: Record<string, unknown> = {
|
|
45
54
|
status: options.status,
|
|
46
55
|
[options.metricName || "metric"]: options.metric,
|
|
@@ -48,7 +57,10 @@ export function commitKeptExperiment(options: {
|
|
|
48
57
|
};
|
|
49
58
|
const commitMessage = `${options.description}\n\nResult: ${JSON.stringify(resultData)}`;
|
|
50
59
|
|
|
51
|
-
const repoRootResult = runGitCommand(options.cwd, [
|
|
60
|
+
const repoRootResult = await runGitCommand(options.runCommandWithTimeout, options.cwd, [
|
|
61
|
+
"rev-parse",
|
|
62
|
+
"--show-toplevel",
|
|
63
|
+
]);
|
|
52
64
|
if (repoRootResult.code !== 0 || repoRootResult.stdout.trim().length === 0) {
|
|
53
65
|
return {
|
|
54
66
|
attempted: true,
|
|
@@ -61,7 +73,7 @@ export function commitKeptExperiment(options: {
|
|
|
61
73
|
|
|
62
74
|
const repoRoot = repoRootResult.stdout.trim();
|
|
63
75
|
|
|
64
|
-
const addResult = runGitCommand(repoRoot, ["add", "-A"]);
|
|
76
|
+
const addResult = await runGitCommand(options.runCommandWithTimeout, repoRoot, ["add", "-A"]);
|
|
65
77
|
if (addResult.code !== 0) {
|
|
66
78
|
return {
|
|
67
79
|
attempted: true,
|
|
@@ -72,7 +84,11 @@ export function commitKeptExperiment(options: {
|
|
|
72
84
|
};
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
const diffResult = runGitCommand(repoRoot, [
|
|
87
|
+
const diffResult = await runGitCommand(options.runCommandWithTimeout, repoRoot, [
|
|
88
|
+
"diff",
|
|
89
|
+
"--cached",
|
|
90
|
+
"--quiet",
|
|
91
|
+
]);
|
|
76
92
|
if (diffResult.code === 0) {
|
|
77
93
|
return {
|
|
78
94
|
attempted: true,
|
|
@@ -92,7 +108,11 @@ export function commitKeptExperiment(options: {
|
|
|
92
108
|
};
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
const commitResult = runGitCommand(repoRoot, [
|
|
111
|
+
const commitResult = await runGitCommand(options.runCommandWithTimeout, repoRoot, [
|
|
112
|
+
"commit",
|
|
113
|
+
"-m",
|
|
114
|
+
commitMessage,
|
|
115
|
+
]);
|
|
96
116
|
if (commitResult.code !== 0) {
|
|
97
117
|
return {
|
|
98
118
|
attempted: true,
|
|
@@ -103,7 +123,11 @@ export function commitKeptExperiment(options: {
|
|
|
103
123
|
};
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
const revParseResult = runGitCommand(repoRoot, [
|
|
126
|
+
const revParseResult = await runGitCommand(options.runCommandWithTimeout, repoRoot, [
|
|
127
|
+
"rev-parse",
|
|
128
|
+
"--short=7",
|
|
129
|
+
"HEAD",
|
|
130
|
+
]);
|
|
107
131
|
const actualCommit =
|
|
108
132
|
revParseResult.code === 0 && revParseResult.stdout.trim().length >= 7
|
|
109
133
|
? revParseResult.stdout.trim().slice(0, 7)
|
|
@@ -130,3 +154,16 @@ function truncateOutput(output: string): string {
|
|
|
130
154
|
function formatExit(code: number | null): string {
|
|
131
155
|
return code === null ? "" : ` (exit ${code})`;
|
|
132
156
|
}
|
|
157
|
+
|
|
158
|
+
function describeTermination(termination: "exit" | "timeout" | "no-output-timeout" | "signal"): string {
|
|
159
|
+
switch (termination) {
|
|
160
|
+
case "timeout":
|
|
161
|
+
return "command timed out";
|
|
162
|
+
case "no-output-timeout":
|
|
163
|
+
return "command timed out waiting for output";
|
|
164
|
+
case "signal":
|
|
165
|
+
return "command terminated by signal";
|
|
166
|
+
default:
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -78,7 +78,8 @@ export function createLogExperimentTool(api: OpenClawPluginApi) {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
if (params.status === "keep") {
|
|
81
|
-
const gitResult = commitKeptExperiment({
|
|
81
|
+
const gitResult = await commitKeptExperiment({
|
|
82
|
+
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
|
|
82
83
|
cwd: cwd,
|
|
83
84
|
description: params.description,
|
|
84
85
|
metricName: state.metricName,
|
|
@@ -34,6 +34,7 @@ export function createRunExperimentTool(api: OpenClawPluginApi) {
|
|
|
34
34
|
let details;
|
|
35
35
|
try {
|
|
36
36
|
details = await executeExperimentCommand({
|
|
37
|
+
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
|
|
37
38
|
command: params.command,
|
|
38
39
|
cwd,
|
|
39
40
|
timeoutSeconds: params.timeout_seconds,
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gianfrancopiana/openclaw-autoresearch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Faithful OpenClaw port of pi-autoresearch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"skills/",
|
|
23
23
|
"docs/",
|
|
24
24
|
"README.md",
|
|
25
|
+
"RELEASING.md",
|
|
25
26
|
"LICENSE"
|
|
26
27
|
],
|
|
27
28
|
"license": "MIT",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
50
51
|
},
|
|
52
|
+
"openclawRuntime": ">=2026.3.13",
|
|
51
53
|
"openclaw": {
|
|
52
54
|
"extensions": [
|
|
53
55
|
"./index.ts"
|