pi-better-harness 0.1.7 → 0.1.9
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 +4 -0
- package/node_modules/pi-better-background-tasks/LICENSE +21 -0
- package/node_modules/pi-better-background-tasks/README.md +57 -0
- package/node_modules/pi-better-background-tasks/package.json +58 -0
- package/node_modules/pi-better-background-tasks/src/conditions.ts +92 -0
- package/node_modules/pi-better-background-tasks/src/goal-provider.ts +116 -0
- package/node_modules/pi-better-background-tasks/src/index.ts +23 -0
- package/node_modules/pi-better-background-tasks/src/logs.ts +84 -0
- package/node_modules/pi-better-background-tasks/src/navigator-provider.ts +223 -0
- package/node_modules/pi-better-background-tasks/src/process.ts +99 -0
- package/node_modules/pi-better-background-tasks/src/registry.ts +77 -0
- package/node_modules/pi-better-background-tasks/src/runtime.ts +387 -0
- package/node_modules/pi-better-background-tasks/src/shared-log-utils.ts +104 -0
- package/node_modules/pi-better-background-tasks/src/shared-navigator.ts +1140 -0
- package/node_modules/pi-better-background-tasks/src/tools.ts +478 -0
- package/node_modules/pi-better-background-tasks/src/types.ts +87 -0
- package/node_modules/pi-better-goal/LICENSE +21 -0
- package/node_modules/pi-better-goal/README.md +58 -0
- package/node_modules/pi-better-goal/package.json +69 -0
- package/node_modules/pi-better-goal/src/activity.ts +158 -0
- package/node_modules/pi-better-goal/src/goal-clock.ts +76 -0
- package/node_modules/pi-better-goal/src/goal-state.ts +217 -0
- package/node_modules/pi-better-goal/src/index.ts +546 -0
- package/node_modules/pi-better-goal/src/subagents.ts +134 -0
- package/node_modules/pi-better-goal/src/types.ts +81 -0
- package/node_modules/pi-better-subagents/LICENSE +21 -0
- package/node_modules/pi-better-subagents/README.md +59 -0
- package/node_modules/pi-better-subagents/batch.mjs +208 -0
- package/node_modules/pi-better-subagents/capacity.mjs +112 -0
- package/node_modules/pi-better-subagents/cleanup.ts +403 -0
- package/node_modules/pi-better-subagents/completion.mjs +165 -0
- package/node_modules/pi-better-subagents/completion.ts +11 -0
- package/node_modules/pi-better-subagents/config.json +14 -0
- package/node_modules/pi-better-subagents/config.ts +114 -0
- package/node_modules/pi-better-subagents/extensions.mjs +147 -0
- package/node_modules/pi-better-subagents/extensions.ts +19 -0
- package/node_modules/pi-better-subagents/finalization.ts +145 -0
- package/node_modules/pi-better-subagents/git-remotes.ts +413 -0
- package/node_modules/pi-better-subagents/git-workspace.ts +430 -0
- package/node_modules/pi-better-subagents/health-observation.ts +815 -0
- package/node_modules/pi-better-subagents/health-surface.mjs +276 -0
- package/node_modules/pi-better-subagents/health.ts +344 -0
- package/node_modules/pi-better-subagents/index.ts +1378 -0
- package/node_modules/pi-better-subagents/lifecycle.ts +333 -0
- package/node_modules/pi-better-subagents/list.mjs +123 -0
- package/node_modules/pi-better-subagents/list.ts +17 -0
- package/node_modules/pi-better-subagents/log-cursor.ts +192 -0
- package/node_modules/pi-better-subagents/navigator.mjs +1188 -0
- package/node_modules/pi-better-subagents/navigator.ts +38 -0
- package/node_modules/pi-better-subagents/package.json +51 -0
- package/node_modules/pi-better-subagents/parse.ts +1262 -0
- package/node_modules/pi-better-subagents/registry.ts +250 -0
- package/node_modules/pi-better-subagents/sandbox.ts +164 -0
- package/node_modules/pi-better-subagents/shared-log-utils.ts +104 -0
- package/node_modules/pi-better-subagents/shared-navigator.ts +1140 -0
- package/node_modules/pi-better-subagents/spawn.ts +78 -0
- package/node_modules/pi-better-subagents/stop.ts +155 -0
- package/node_modules/pi-better-subagents/tools.ts +399 -0
- package/node_modules/pi-better-subagents/widget.mjs +250 -0
- package/node_modules/pi-better-subagents/widget.ts +30 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ Use `pi-better-harness` when you want the full background-work bundle for Pi in
|
|
|
12
12
|
|
|
13
13
|
`pi-better-read-aloud` is intentionally not included yet.
|
|
14
14
|
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
17
|
+
<p><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/pi-better-harness.png" alt="pi-better-harness rendered in Pi" width="49%" /><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/overview/pi-better-harness.png" alt="pi-better-harness package overview" width="49%" /></p>
|
|
18
|
+
|
|
15
19
|
## Install
|
|
16
20
|
|
|
17
21
|
```sh
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 1aboveio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# pi-better-background-tasks
|
|
2
|
+
|
|
3
|
+
`pi-better-background-tasks` is a Pi extension for durable background shell tasks, watchers, logs, and status inspection.
|
|
4
|
+
|
|
5
|
+
## Quick Answer
|
|
6
|
+
|
|
7
|
+
Use `pi-better-background-tasks` when a command should keep running while the foreground Pi session stays free. It is best for dev servers, long scripts, queue watchers, deploy checks, log tails, and other command-driven work.
|
|
8
|
+
|
|
9
|
+
## Screenshots
|
|
10
|
+
|
|
11
|
+
<p><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/pi-better-background-tasks.png" alt="pi-better-background-tasks rendered in Pi" width="49%" /><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/overview/pi-better-background-tasks.png" alt="pi-better-background-tasks package overview" width="49%" /></p>
|
|
12
|
+
|
|
13
|
+
## Core Features
|
|
14
|
+
|
|
15
|
+
- Start long-running commands without blocking the current turn.
|
|
16
|
+
- Watch commands until success, failure, or timeout.
|
|
17
|
+
- Keep task metadata and logs available across reloads.
|
|
18
|
+
- Show active work in Pi's background-work navigator.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pi install npm:pi-better-background-tasks
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Try it for one run:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pi -e npm:pi-better-background-tasks
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## When To Use
|
|
33
|
+
|
|
34
|
+
Use this package for shell commands that need logs, status, cancellation, or completion notifications across a Pi turn.
|
|
35
|
+
|
|
36
|
+
Do not use it for short commands where the foreground session should wait for the result directly.
|
|
37
|
+
|
|
38
|
+
## Compatibility
|
|
39
|
+
|
|
40
|
+
| Requirement | Support |
|
|
41
|
+
|-------------|---------|
|
|
42
|
+
| Pi | Required |
|
|
43
|
+
| Install method | `pi install npm:pi-better-background-tasks` |
|
|
44
|
+
| Development runtime | Node.js 22+ |
|
|
45
|
+
|
|
46
|
+
## Update Or Remove
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pi update npm:pi-better-background-tasks
|
|
50
|
+
pi remove npm:pi-better-background-tasks
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## More Detail
|
|
54
|
+
|
|
55
|
+
- Repository: https://github.com/1aboveio/pi-better-harness
|
|
56
|
+
- Detailed notes: https://github.com/1aboveio/pi-better-harness/blob/main/packages/pi-better-background-tasks/docs/usage.md
|
|
57
|
+
- License: https://github.com/1aboveio/pi-better-harness/blob/main/LICENSE
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-better-background-tasks",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi",
|
|
11
|
+
"background-tasks",
|
|
12
|
+
"watchers"
|
|
13
|
+
],
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./src/index.ts"
|
|
17
|
+
],
|
|
18
|
+
"image": "https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/pi-better-background-tasks.png"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/1aboveio/pi-better-harness.git",
|
|
23
|
+
"directory": "packages/pi-better-background-tasks"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/1aboveio/pi-better-harness/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/1aboveio/pi-better-harness/tree/main/packages/pi-better-background-tasks#readme",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"pretest": "node ../../scripts/sync-shared-log-utils.mjs",
|
|
34
|
+
"prepack": "node ../../scripts/sync-shared-log-utils.mjs",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:unit": "vitest run src/conditions.test.ts src/runtime.test.ts",
|
|
38
|
+
"test:e2e": "vitest run src/e2e.test.ts",
|
|
39
|
+
"test:cross-session": "vitest run src/e2e.test.ts -t \"different session\"",
|
|
40
|
+
"test:golden": "vitest run src/golden-path.test.ts",
|
|
41
|
+
"test:integration": "npm run test:e2e && npm run test:golden"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src/**/*.ts",
|
|
45
|
+
"!src/**/*.test.ts",
|
|
46
|
+
"README.md"
|
|
47
|
+
],
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
50
|
+
"typebox": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.10.0",
|
|
54
|
+
"typebox": "^1.1.38",
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CommandResult, Condition } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface ConditionMatch {
|
|
4
|
+
matched: boolean;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function evaluateCondition(condition: Condition, result: CommandResult): ConditionMatch {
|
|
10
|
+
switch (condition.type) {
|
|
11
|
+
case "exit_code":
|
|
12
|
+
return { matched: result.exitCode === condition.equals, value: result.exitCode };
|
|
13
|
+
case "stdout_contains":
|
|
14
|
+
return { matched: result.stdout.includes(condition.value) };
|
|
15
|
+
case "stderr_contains":
|
|
16
|
+
return { matched: result.stderr.includes(condition.value) };
|
|
17
|
+
case "json_path_equals": {
|
|
18
|
+
const resolved = resolveJsonPath(result.stdout, condition.path);
|
|
19
|
+
if (!resolved.ok) return { matched: false, error: resolved.error };
|
|
20
|
+
return { matched: deepEqual(resolved.value, condition.value), value: resolved.value };
|
|
21
|
+
}
|
|
22
|
+
case "json_path_exists": {
|
|
23
|
+
const resolved = resolveJsonPath(result.stdout, condition.path);
|
|
24
|
+
return resolved.ok ? { matched: true, value: resolved.value } : { matched: false, error: resolved.error };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveJsonPath(jsonText: string, path: string): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
30
|
+
let current: unknown;
|
|
31
|
+
try {
|
|
32
|
+
current = JSON.parse(jsonText);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { ok: false, error: `stdout is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tokens = parseJsonPath(path);
|
|
38
|
+
if (!tokens) return { ok: false, error: `unsupported JSON path: ${path}` };
|
|
39
|
+
|
|
40
|
+
for (const token of tokens) {
|
|
41
|
+
if (typeof token === "number") {
|
|
42
|
+
if (!Array.isArray(current) || token < 0 || token >= current.length) {
|
|
43
|
+
return { ok: false, error: `JSON path not found: ${path}` };
|
|
44
|
+
}
|
|
45
|
+
current = current[token];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (current === null || typeof current !== "object" || !(token in current)) {
|
|
49
|
+
return { ok: false, error: `JSON path not found: ${path}` };
|
|
50
|
+
}
|
|
51
|
+
current = (current as Record<string, unknown>)[token];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { ok: true, value: current };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseJsonPath(path: string): Array<string | number> | undefined {
|
|
58
|
+
if (!path.startsWith("$")) return undefined;
|
|
59
|
+
const tokens: Array<string | number> = [];
|
|
60
|
+
let i = 1;
|
|
61
|
+
while (i < path.length) {
|
|
62
|
+
if (path[i] === ".") {
|
|
63
|
+
i += 1;
|
|
64
|
+
const start = i;
|
|
65
|
+
while (i < path.length && /[A-Za-z0-9_$-]/.test(path[i]!)) i += 1;
|
|
66
|
+
if (i === start) return undefined;
|
|
67
|
+
tokens.push(path.slice(start, i));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (path[i] === "[") {
|
|
71
|
+
const end = path.indexOf("]", i);
|
|
72
|
+
if (end === -1) return undefined;
|
|
73
|
+
const raw = path.slice(i + 1, end).trim();
|
|
74
|
+
const quoted = raw.match(/^['\"](.+)['\"]$/);
|
|
75
|
+
if (quoted) {
|
|
76
|
+
tokens.push(quoted[1]!);
|
|
77
|
+
} else if (/^\d+$/.test(raw)) {
|
|
78
|
+
tokens.push(Number(raw));
|
|
79
|
+
} else {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
i = end + 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return tokens;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
91
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
92
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { listMetas } from "./registry.js";
|
|
3
|
+
import type { BackgroundTaskMeta } from "./types.js";
|
|
4
|
+
import { isTerminalStatus } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const GOAL_READY_EVENT = "pi-better-goal:ready";
|
|
7
|
+
const GOAL_REGISTER_PROVIDER_EVENT = "pi-better-goal:register-provider";
|
|
8
|
+
const readySubscriptions = new WeakSet<ExtensionAPI>();
|
|
9
|
+
|
|
10
|
+
export interface GoalBackgroundWorkItem {
|
|
11
|
+
id: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
status: string;
|
|
14
|
+
active: boolean;
|
|
15
|
+
unhealthy?: boolean;
|
|
16
|
+
terminal?: boolean;
|
|
17
|
+
attention?: boolean;
|
|
18
|
+
startedAt?: number;
|
|
19
|
+
endedAt?: number;
|
|
20
|
+
details?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GoalBackgroundProviderSnapshot {
|
|
24
|
+
providerId: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
items: GoalBackgroundWorkItem[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectBackgroundTaskGoalActivity(
|
|
30
|
+
metas: BackgroundTaskMeta[] = listMetas(),
|
|
31
|
+
ctx?: ExtensionContext,
|
|
32
|
+
now = Date.now(),
|
|
33
|
+
): GoalBackgroundProviderSnapshot {
|
|
34
|
+
return {
|
|
35
|
+
providerId: "background-tasks",
|
|
36
|
+
label: "Background Tasks",
|
|
37
|
+
items: metas
|
|
38
|
+
.filter((meta) => isVisibleGoalActivity(meta, ctx, now))
|
|
39
|
+
.map(backgroundTaskToGoalItem),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerBackgroundTasksGoalProvider(pi: ExtensionAPI): void {
|
|
44
|
+
const emitProvider = (): void => {
|
|
45
|
+
pi.events?.emit(GOAL_REGISTER_PROVIDER_EVENT, {
|
|
46
|
+
id: "background-tasks",
|
|
47
|
+
label: "Background Tasks",
|
|
48
|
+
getActivity: (ctx: ExtensionContext) => collectBackgroundTaskGoalActivity(listMetas(), ctx),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!readySubscriptions.has(pi)) {
|
|
53
|
+
pi.events?.on?.(GOAL_READY_EVENT, emitProvider);
|
|
54
|
+
readySubscriptions.add(pi);
|
|
55
|
+
}
|
|
56
|
+
emitProvider();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const TERMINAL_GOAL_ACTIVITY_RETENTION_MS = 30_000;
|
|
60
|
+
|
|
61
|
+
function isVisibleGoalActivity(meta: BackgroundTaskMeta, ctx: ExtensionContext | undefined, now: number): boolean {
|
|
62
|
+
if (meta.dismissedAt !== undefined) return false;
|
|
63
|
+
if (ctx && !belongsToGoalActivitySession(meta, ctx)) return false;
|
|
64
|
+
if (meta.status === "running") return true;
|
|
65
|
+
if (!ctx) return true;
|
|
66
|
+
if (typeof meta.endedAt !== "number") return true;
|
|
67
|
+
return now - meta.endedAt < TERMINAL_GOAL_ACTIVITY_RETENTION_MS;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function belongsToGoalActivitySession(meta: BackgroundTaskMeta, ctx: ExtensionContext): boolean {
|
|
71
|
+
const active = getGoalActivityOrigin(ctx);
|
|
72
|
+
const origin = meta.callbackOrigin;
|
|
73
|
+
if (origin) {
|
|
74
|
+
if (origin.cwd !== active.cwd) return false;
|
|
75
|
+
if (origin.sessionId || active.sessionId) return origin.sessionId === active.sessionId;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (active.sessionId) return false;
|
|
79
|
+
return meta.cwd === active.cwd;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getGoalActivityOrigin(ctx: ExtensionContext) {
|
|
83
|
+
let sessionId: string | undefined;
|
|
84
|
+
try {
|
|
85
|
+
sessionId = ctx.sessionManager?.getSessionId();
|
|
86
|
+
} catch {
|
|
87
|
+
sessionId = undefined;
|
|
88
|
+
}
|
|
89
|
+
return { cwd: ctx.cwd, sessionId };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function backgroundTaskToGoalItem(meta: BackgroundTaskMeta): GoalBackgroundWorkItem {
|
|
93
|
+
const active = meta.status === "running";
|
|
94
|
+
const terminal = isTerminalStatus(meta.status);
|
|
95
|
+
const attention = terminal && meta.status !== "succeeded";
|
|
96
|
+
const item: GoalBackgroundWorkItem = {
|
|
97
|
+
id: meta.id,
|
|
98
|
+
status: meta.status,
|
|
99
|
+
active,
|
|
100
|
+
terminal,
|
|
101
|
+
attention,
|
|
102
|
+
startedAt: meta.startedAt,
|
|
103
|
+
details: {
|
|
104
|
+
kind: meta.kind,
|
|
105
|
+
callback: meta.callback !== false,
|
|
106
|
+
cwd: meta.cwd,
|
|
107
|
+
command: meta.command,
|
|
108
|
+
argv: meta.argv,
|
|
109
|
+
logPath: meta.logPath,
|
|
110
|
+
spawnPid: meta.spawnPid,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
if (meta.name !== undefined) item.label = meta.name;
|
|
114
|
+
if (meta.endedAt !== undefined) item.endedAt = meta.endedAt;
|
|
115
|
+
return item;
|
|
116
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { disposeBackgroundWorkNavigator } from "./shared-navigator.ts";
|
|
3
|
+
import { registerBackgroundTasksGoalProvider } from "./goal-provider.js";
|
|
4
|
+
import { clearBackgroundTasksNavigatorSession, ensureBackgroundTasksNavigator, ensureBackgroundTasksNavigatorProvider } from "./navigator-provider.js";
|
|
5
|
+
import { registerTools } from "./tools.js";
|
|
6
|
+
|
|
7
|
+
export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
8
|
+
registerBackgroundTasksGoalProvider(pi);
|
|
9
|
+
ensureBackgroundTasksNavigatorProvider(pi);
|
|
10
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
11
|
+
registerBackgroundTasksGoalProvider(pi);
|
|
12
|
+
ensureBackgroundTasksNavigator(ctx);
|
|
13
|
+
});
|
|
14
|
+
pi.on("session_before_switch", async () => {
|
|
15
|
+
clearBackgroundTasksNavigatorSession();
|
|
16
|
+
disposeBackgroundWorkNavigator();
|
|
17
|
+
});
|
|
18
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
19
|
+
clearBackgroundTasksNavigatorSession();
|
|
20
|
+
disposeBackgroundWorkNavigator(ctx);
|
|
21
|
+
});
|
|
22
|
+
registerTools(pi);
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, openSync, readSync, statSync, truncateSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { readBoundedTail, tailTerminalDisplay, terminalDisplayRows } from "./shared-log-utils.js";
|
|
5
|
+
import type { CommandResult } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MAX_LOG_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
export const MAX_LOG_TAIL_READ_BYTES = 512 * 1024;
|
|
9
|
+
const RETAINED_LOG_FRACTION = 0.75;
|
|
10
|
+
|
|
11
|
+
export function appendWatchResult(logPath: string, result: CommandResult): void {
|
|
12
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
13
|
+
const header = `\n--- check ${new Date(result.startedAt).toISOString()} exit=${result.exitCode ?? "null"} signal=${result.signal ?? "null"} duration_ms=${result.endedAt - result.startedAt} ---\n`;
|
|
14
|
+
appendFileSync(logPath, header);
|
|
15
|
+
if (result.stdout) appendFileSync(logPath, result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
16
|
+
if (result.stderr) appendFileSync(logPath, `[stderr]\n${result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function appendLine(logPath: string, line: string): void {
|
|
20
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
21
|
+
appendFileSync(logPath, `${line}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readLog(logPath: string, tailLines?: number): { text: string; truncated: boolean } {
|
|
25
|
+
const requestedRows = tailLines && tailLines > 0 ? Math.floor(tailLines) : undefined;
|
|
26
|
+
const tail = readBoundedTail(logPath, MAX_LOG_TAIL_READ_BYTES);
|
|
27
|
+
if (!tail.text) return { text: "", truncated: tail.truncated };
|
|
28
|
+
if (!requestedRows) return { text: tail.text, truncated: tail.truncated };
|
|
29
|
+
const rows = terminalDisplayRows(tail.text);
|
|
30
|
+
return {
|
|
31
|
+
text: tailTerminalDisplay(tail.text, requestedRows),
|
|
32
|
+
truncated: tail.truncated || rows.length > requestedRows,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveMaxLogBytes(value: number | undefined): number {
|
|
37
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return DEFAULT_MAX_LOG_BYTES;
|
|
38
|
+
return Math.max(64 * 1024, Math.floor(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compact in place so detached children keep writing to the same inode. A
|
|
43
|
+
* rename-based rotation would leave their inherited descriptor unbounded.
|
|
44
|
+
*/
|
|
45
|
+
export function retainLogTail(logPath: string, maxBytes: number): { discardedBytes: number } | undefined {
|
|
46
|
+
let size: number;
|
|
47
|
+
try {
|
|
48
|
+
size = statSync(logPath).size;
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (size <= maxBytes) return undefined;
|
|
53
|
+
|
|
54
|
+
const start = Math.max(0, size - Math.floor(maxBytes * RETAINED_LOG_FRACTION));
|
|
55
|
+
let fd: number | undefined;
|
|
56
|
+
let retained: Buffer;
|
|
57
|
+
try {
|
|
58
|
+
fd = openSync(logPath, "r");
|
|
59
|
+
retained = Buffer.allocUnsafe(size - start);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset < retained.length) {
|
|
62
|
+
const read = readSync(fd, retained, offset, retained.length - offset, start + offset);
|
|
63
|
+
if (read <= 0) break;
|
|
64
|
+
offset += read;
|
|
65
|
+
}
|
|
66
|
+
retained = retained.subarray(0, offset);
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
} finally {
|
|
70
|
+
if (fd !== undefined) {
|
|
71
|
+
try { closeSync(fd); } catch { /* best effort */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const discardedBytes = Math.max(0, size - retained.length);
|
|
76
|
+
try {
|
|
77
|
+
truncateSync(logPath, 0);
|
|
78
|
+
if (retained.length) appendFileSync(logPath, retained);
|
|
79
|
+
appendFileSync(logPath, `--- log retention discarded ${discardedBytes} bytes; newest output retained ---\n`);
|
|
80
|
+
return { discardedBytes };
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|