@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 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
- For local development, link the repo directly into OpenClaw:
34
+ Requires OpenClaw `2026.3.13` or newer.
35
+
36
+ Use OpenClaw's plugin installer:
35
37
 
36
38
  ```bash
37
- openclaw plugins install -l /absolute/path/to/openclaw-autoresearch
39
+ openclaw plugins install @gianfrancopiana/openclaw-autoresearch
38
40
  ```
39
41
 
40
- For a packaged local install, build the tarball and install that artifact:
42
+ If you're running from a local OpenClaw checkout, use:
41
43
 
42
44
  ```bash
43
- npm install
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
- After publishing to npm, the same package can be installed with:
48
+ For local plugin development, link your working copy instead of copying files:
49
49
 
50
50
  ```bash
51
- openclaw plugins install @gianfrancopiana/openclaw-autoresearch
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 { spawn } from "node:child_process";
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 FORCE_KILL_GRACE_MS = 1_000;
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 = Math.max(0, timeoutSeconds) * 1_000;
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
- return await new Promise<ExperimentExecutionResult>((resolve) => {
30
- const child = spawn("bash", ["-c", options.command], {
31
- cwd: options.cwd,
32
- stdio: ["ignore", "pipe", "pipe"],
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
- resolve({
97
- command: options.command,
98
- exitCode: code,
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 { spawnSync } from "node:child_process";
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(cwd: string, args: readonly string[]): GitCommandResult {
19
- const result = spawnSync("git", [...args], {
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
- encoding: "utf8",
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.status,
36
+ code: result.code,
29
37
  stdout,
30
38
  stderr,
31
- combinedOutput: `${stdout}${stderr}`.trim(),
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, ["rev-parse", "--show-toplevel"]);
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, ["diff", "--cached", "--quiet"]);
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, ["commit", "-m", commitMessage]);
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, ["rev-parse", "--short=7", "HEAD"]);
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,
@@ -5,7 +5,7 @@
5
5
  "skills": [
6
6
  "./skills"
7
7
  ],
8
- "version": "1.0.0",
8
+ "version": "1.0.2",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gianfrancopiana/openclaw-autoresearch",
3
- "version": "1.0.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"