@jaggerxtrm/pi-extensions 0.7.8
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/MIGRATION_NOTES.md +39 -0
- package/README.md +43 -0
- package/extensions/README.md +5 -0
- package/extensions/auto-session-name/index.ts +29 -0
- package/extensions/auto-session-name/package.json +16 -0
- package/extensions/auto-update/index.ts +71 -0
- package/extensions/auto-update/package.json +16 -0
- package/extensions/beads/index.ts +232 -0
- package/extensions/beads/package.json +16 -0
- package/extensions/compact-header/index.ts +69 -0
- package/extensions/compact-header/package.json +16 -0
- package/extensions/custom-footer/index.ts +398 -0
- package/extensions/custom-footer/package.json +16 -0
- package/extensions/custom-provider-qwen-cli/index.ts +363 -0
- package/extensions/custom-provider-qwen-cli/package.json +16 -0
- package/extensions/git-checkpoint/index.ts +53 -0
- package/extensions/git-checkpoint/package.json +16 -0
- package/extensions/lsp-bootstrap/index.ts +134 -0
- package/extensions/lsp-bootstrap/package.json +17 -0
- package/extensions/pi-serena-compact/index.ts +121 -0
- package/extensions/pi-serena-compact/package.json +23 -0
- package/extensions/quality-gates/index.ts +66 -0
- package/extensions/quality-gates/package.json +16 -0
- package/extensions/service-skills/index.ts +108 -0
- package/extensions/service-skills/package.json +16 -0
- package/extensions/session-flow/index.ts +96 -0
- package/extensions/session-flow/package.json +16 -0
- package/extensions/xtrm-loader/index.ts +152 -0
- package/extensions/xtrm-loader/package.json +16 -0
- package/extensions/xtrm-ui/format.ts +282 -0
- package/extensions/xtrm-ui/index.ts +1112 -0
- package/extensions/xtrm-ui/package.json +21 -0
- package/package.json +46 -0
- package/src/core/README.md +9 -0
- package/src/core/adapter.ts +52 -0
- package/src/core/guard-rules.ts +100 -0
- package/src/core/index.ts +5 -0
- package/src/core/lib.ts +3 -0
- package/src/core/logger.ts +45 -0
- package/src/core/runner.ts +71 -0
- package/src/core/session-state.ts +59 -0
- package/src/extensions/auto-session-name.ts +3 -0
- package/src/extensions/auto-update.ts +3 -0
- package/src/extensions/beads.ts +3 -0
- package/src/extensions/compact-header.ts +3 -0
- package/src/extensions/custom-footer.ts +3 -0
- package/src/extensions/custom-provider-qwen-cli.ts +3 -0
- package/src/extensions/git-checkpoint.ts +3 -0
- package/src/extensions/lsp-bootstrap.ts +3 -0
- package/src/extensions/pi-serena-compact.ts +3 -0
- package/src/extensions/quality-gates.ts +3 -0
- package/src/extensions/service-skills.ts +3 -0
- package/src/extensions/session-flow.ts +3 -0
- package/src/extensions/xtrm-loader.ts +3 -0
- package/src/extensions/xtrm-ui.ts +3 -0
- package/src/index.ts +9 -0
- package/src/registry.ts +52 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/legacy-path-map.ts +23 -0
- package/themes/xtrm-ui/pidex-dark.json +85 -0
- package/themes/xtrm-ui/pidex-light.json +85 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Pi extension source migration notes (P2)
|
|
2
|
+
|
|
3
|
+
## Legacy → new source map
|
|
4
|
+
|
|
5
|
+
| Legacy path | New path | Notes |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `packages/pi-extensions/extensions/auto-session-name` | `packages/pi-extensions/extensions/auto-session-name` | extension source moved unchanged |
|
|
8
|
+
| `packages/pi-extensions/extensions/auto-update` | `packages/pi-extensions/extensions/auto-update` | extension source moved unchanged |
|
|
9
|
+
| `packages/pi-extensions/extensions/beads` | `packages/pi-extensions/extensions/beads` | now imports `../../src/core` |
|
|
10
|
+
| `packages/pi-extensions/extensions/compact-header` | `packages/pi-extensions/extensions/compact-header` | extension source moved unchanged |
|
|
11
|
+
| `packages/pi-extensions/extensions/custom-footer` | `packages/pi-extensions/extensions/custom-footer` | now imports `../../src/core` |
|
|
12
|
+
| `packages/pi-extensions/extensions/custom-provider-qwen-cli` | `packages/pi-extensions/extensions/custom-provider-qwen-cli` | extension source moved unchanged |
|
|
13
|
+
| `packages/pi-extensions/extensions/git-checkpoint` | `packages/pi-extensions/extensions/git-checkpoint` | extension source moved unchanged |
|
|
14
|
+
| `packages/pi-extensions/extensions/lsp-bootstrap` | `packages/pi-extensions/extensions/lsp-bootstrap` | extension source moved unchanged |
|
|
15
|
+
| `packages/pi-extensions/extensions/pi-serena-compact` | `packages/pi-extensions/extensions/pi-serena-compact` | extension source moved unchanged |
|
|
16
|
+
| `packages/pi-extensions/extensions/quality-gates` | `packages/pi-extensions/extensions/quality-gates` | now imports `../../src/core` |
|
|
17
|
+
| `packages/pi-extensions/extensions/service-skills` | `packages/pi-extensions/extensions/service-skills` | now imports `../../src/core` |
|
|
18
|
+
| `packages/pi-extensions/extensions/session-flow` | `packages/pi-extensions/extensions/session-flow` | now imports `../../src/core` |
|
|
19
|
+
| `packages/pi-extensions/extensions/xtrm-loader` | `packages/pi-extensions/extensions/xtrm-loader` | now imports `../../src/core` |
|
|
20
|
+
| `packages/pi-extensions/extensions/xtrm-ui` | `packages/pi-extensions/extensions/xtrm-ui` | theme assets moved to package-level `themes/xtrm-ui` |
|
|
21
|
+
| `packages/pi-extensions/src/core` | `packages/pi-extensions/src/core` | internal helpers; no separate `@xtrm/pi-core` package required |
|
|
22
|
+
|
|
23
|
+
## Asset migration
|
|
24
|
+
|
|
25
|
+
- `xtrm-ui/themes/*.json` moved to `packages/pi-extensions/themes/xtrm-ui/*.json`.
|
|
26
|
+
- `xtrm-ui` now discovers themes from `join(__dirname, "../../themes/xtrm-ui")`.
|
|
27
|
+
|
|
28
|
+
## Follow-up updates required in later phases
|
|
29
|
+
|
|
30
|
+
1. **Installer/runtime sync paths**
|
|
31
|
+
- Replace hardcoded `packages/pi-extensions/extensions/**` references with `packages/pi-extensions/extensions/**` in install/runtime copy logic.
|
|
32
|
+
2. **Registry generation**
|
|
33
|
+
- Update `scripts/gen-registry.mjs` asset sources once package path is the canonical source-of-truth.
|
|
34
|
+
3. **Tests and fixtures**
|
|
35
|
+
- Update tests asserting extension source paths (currently expecting `packages/pi-extensions/extensions`).
|
|
36
|
+
4. **Policies/docs references**
|
|
37
|
+
- Update docs/policies that still mention `packages/pi-extensions/extensions` after runtime switch lands.
|
|
38
|
+
5. **Packaging entrypoint wiring**
|
|
39
|
+
- Wire `packages/pi-extensions/src/index.ts` into Pi package install flow and extension registration.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @jaggerxtrm/pi-extensions
|
|
2
|
+
|
|
3
|
+
Unified runtime package for xtrm-managed Pi extensions.
|
|
4
|
+
|
|
5
|
+
## Publish contract
|
|
6
|
+
|
|
7
|
+
- Package is published as `@jaggerxtrm/pi-extensions` (public npm package).
|
|
8
|
+
- No build step is required. Pi loads raw TypeScript extension entrypoints at runtime.
|
|
9
|
+
- `prepublishOnly` runs `verify:runtime` to ensure required runtime assets exist:
|
|
10
|
+
- `src/index.ts`
|
|
11
|
+
- `src/registry.ts`
|
|
12
|
+
- `extensions/`
|
|
13
|
+
- `themes/`
|
|
14
|
+
- Files shipped to npm are controlled by `files` in `package.json`.
|
|
15
|
+
|
|
16
|
+
## Release workflow
|
|
17
|
+
|
|
18
|
+
From repository root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run release:pi-extensions
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To publish both root `xtrm-tools` and this package in one pass:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run release:all
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install contract
|
|
31
|
+
|
|
32
|
+
Managed project runtime install path:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install npm:@jaggerxtrm/pi-extensions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Pi discovers this package through:
|
|
39
|
+
|
|
40
|
+
- `keywords: ["pi-package"]`
|
|
41
|
+
- `pi.extensions: ["./src/index.ts"]`
|
|
42
|
+
|
|
43
|
+
After install, keep `.pi/settings.json` package wiring pointed at `npm:@jaggerxtrm/pi-extensions`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-pi Auto Session Name Extension
|
|
3
|
+
*
|
|
4
|
+
* Automatically names sessions based on the first user message.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
export default function (pi: ExtensionAPI) {
|
|
9
|
+
let named = false;
|
|
10
|
+
|
|
11
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
12
|
+
named = !!pi.getSessionName();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
pi.on("agent_end", async (event) => {
|
|
16
|
+
if (named) return;
|
|
17
|
+
const userMsg = event.messages.find((m) => m.role === "user");
|
|
18
|
+
if (!userMsg) return;
|
|
19
|
+
const text = typeof userMsg.content === "string"
|
|
20
|
+
? userMsg.content
|
|
21
|
+
: userMsg.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join(" ");
|
|
22
|
+
if (!text) return;
|
|
23
|
+
const name = text.slice(0, 60).replace(/\n/g, " ").trim();
|
|
24
|
+
if (name) {
|
|
25
|
+
pi.setSessionName(name);
|
|
26
|
+
named = true;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-auto-session-name",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "xtrm Pi extension: auto-session-name",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi",
|
|
11
|
+
"extension",
|
|
12
|
+
"xtrm"
|
|
13
|
+
],
|
|
14
|
+
"author": "xtrm",
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-pi Auto Update — check for new oh-pi version on session start
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24h
|
|
11
|
+
const STAMP_FILE = join(homedir(), ".pi", "agent", ".update-check");
|
|
12
|
+
|
|
13
|
+
function readStamp(): number {
|
|
14
|
+
try { return Number(readFileSync(STAMP_FILE, "utf8").trim()) || 0; } catch { return 0; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeStamp() {
|
|
18
|
+
try { writeFileSync(STAMP_FILE, String(Date.now())); } catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getLatestVersion(): string | null {
|
|
22
|
+
try {
|
|
23
|
+
return execSync("npm view oh-pi version", { encoding: "utf8", timeout: 8000 }).trim();
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCurrentVersion(): string | null {
|
|
28
|
+
// Read from the installed package.json
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
31
|
+
if (existsSync(pkgPath)) {
|
|
32
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
// Fallback: npm list
|
|
36
|
+
try {
|
|
37
|
+
const out = JSON.parse(execSync("npm list -g oh-pi --json --depth=0", { encoding: "utf8", timeout: 8000 }));
|
|
38
|
+
return out.dependencies?.["oh-pi"]?.version ?? null;
|
|
39
|
+
} catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isNewer(latest: string, current: string): boolean {
|
|
43
|
+
const a = latest.split(".").map(Number);
|
|
44
|
+
const b = current.split(".").map(Number);
|
|
45
|
+
for (let i = 0; i < 3; i++) {
|
|
46
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
|
|
47
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function (pi: ExtensionAPI) {
|
|
53
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
54
|
+
// Non-blocking: run check in background
|
|
55
|
+
setTimeout(async () => {
|
|
56
|
+
try {
|
|
57
|
+
if (Date.now() - readStamp() < CHECK_INTERVAL) return;
|
|
58
|
+
writeStamp();
|
|
59
|
+
|
|
60
|
+
const current = getCurrentVersion();
|
|
61
|
+
const latest = getLatestVersion();
|
|
62
|
+
if (!current || !latest || !isNewer(latest, current)) return;
|
|
63
|
+
|
|
64
|
+
const msg = `oh-pi ${latest} available (current: ${current}). Run: npx oh-pi@latest`;
|
|
65
|
+
if (ctx.hasUI) {
|
|
66
|
+
ctx.ui.toast?.(msg) ?? console.log(`\n💡 ${msg}\n`);
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
}, 2000);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-auto-update",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "xtrm Pi extension: auto-update",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi",
|
|
11
|
+
"extension",
|
|
12
|
+
"xtrm"
|
|
13
|
+
],
|
|
14
|
+
"author": "xtrm",
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { SubprocessRunner, EventAdapter } from "../../src/core";
|
|
4
|
+
|
|
5
|
+
export default function (pi: ExtensionAPI) {
|
|
6
|
+
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
7
|
+
|
|
8
|
+
let cachedSessionId: string | null = null;
|
|
9
|
+
let memoryGateFired = false;
|
|
10
|
+
|
|
11
|
+
// Resolve a stable session ID across event types.
|
|
12
|
+
const getSessionId = (ctx: any): string => {
|
|
13
|
+
const fromManager = ctx?.sessionManager?.getSessionId?.();
|
|
14
|
+
const fromContext = ctx?.sessionId ?? ctx?.session_id;
|
|
15
|
+
const resolved = fromManager || fromContext || cachedSessionId || process.pid.toString();
|
|
16
|
+
if (resolved && !cachedSessionId) cachedSessionId = resolved;
|
|
17
|
+
return resolved;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
21
|
+
const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
|
|
22
|
+
if (result.code !== 0) return null;
|
|
23
|
+
const claim = result.stdout.trim();
|
|
24
|
+
return claim.length > 0 ? claim : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const clearClaimMarker = async (sessionId: string, cwd: string) => {
|
|
28
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isIssueInProgress = async (issueId: string, cwd: string): Promise<boolean | null> => {
|
|
32
|
+
const result = await SubprocessRunner.run("bd", ["show", issueId, "--json"], { cwd });
|
|
33
|
+
if (result.code !== 0 || !result.stdout.trim()) return null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(result.stdout);
|
|
36
|
+
const issue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
37
|
+
if (!issue?.status) return null;
|
|
38
|
+
return issue.status === "in_progress";
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getActiveClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
45
|
+
const claim = await getSessionClaim(sessionId, cwd);
|
|
46
|
+
if (!claim) return null;
|
|
47
|
+
|
|
48
|
+
const inProgress = await isIssueInProgress(claim, cwd);
|
|
49
|
+
if (inProgress === false) {
|
|
50
|
+
await clearClaimMarker(sessionId, cwd);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return claim;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getClosedThisSession = async (sessionId: string, cwd: string): Promise<string | null> => {
|
|
58
|
+
const result = await SubprocessRunner.run("bd", ["kv", "get", `closed-this-session:${sessionId}`], { cwd });
|
|
59
|
+
if (result.code !== 0) return null;
|
|
60
|
+
const issue = result.stdout.trim();
|
|
61
|
+
return issue.length > 0 ? issue : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const clearSessionMarkers = async (sessionId: string, cwd: string) => {
|
|
65
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
|
|
66
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const hasTrackableWork = async (cwd: string): Promise<boolean> => {
|
|
70
|
+
const result = await SubprocessRunner.run("bd", ["list"], { cwd });
|
|
71
|
+
if (result.code === 0) {
|
|
72
|
+
const counts = EventAdapter.parseBdCounts(result.stdout);
|
|
73
|
+
if (counts) return (counts.open + counts.inProgress) > 0;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const stripQuoted = (command: string): string => command.replace(/'[^']*'|"[^"]*"/g, "");
|
|
79
|
+
const isSpecialistsSubprocessCommand = (commandUnquoted: string): boolean =>
|
|
80
|
+
/\bspecialists\s+(run|resume|result|feed|stop|status)\b/.test(commandUnquoted);
|
|
81
|
+
|
|
82
|
+
const getClosedIssueIdFromCommand = (commandUnquoted: string): string | null => {
|
|
83
|
+
const match = commandUnquoted.match(/\bbd\s+close\s+(\S+)/);
|
|
84
|
+
const issueId = match?.[1]?.trim();
|
|
85
|
+
if (!issueId || issueId.startsWith("-")) return null;
|
|
86
|
+
return issueId;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const hasIssueMemoryAck = async (issueId: string, cwd: string): Promise<boolean> => {
|
|
90
|
+
const result = await SubprocessRunner.run("bd", ["kv", "get", `memory-acked:${issueId}`], { cwd });
|
|
91
|
+
return result.code === 0 && result.stdout.trim().length > 0;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const closeMemoryBlockReason = (issueId: string): string =>
|
|
95
|
+
`MEMORY_GATE_BLOCK issue=${issueId} run="bd remember '<insight>' && bd kv set 'memory-acked:${issueId}' 'saved:<key>'" or="bd kv set 'memory-acked:${issueId}' 'nothing novel:<reason>'" then="bd close ${issueId} --reason='<reason>'"`;
|
|
96
|
+
|
|
97
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
cachedSessionId = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId ?? ctx?.session_id ?? cachedSessionId;
|
|
99
|
+
return undefined;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
103
|
+
const cwd = getCwd(ctx);
|
|
104
|
+
if (!EventAdapter.isBeadsProject(cwd)) return undefined;
|
|
105
|
+
const sessionId = getSessionId(ctx);
|
|
106
|
+
|
|
107
|
+
if (EventAdapter.isMutatingFileTool(event)) {
|
|
108
|
+
const claim = await getActiveClaim(sessionId, cwd);
|
|
109
|
+
if (!claim) {
|
|
110
|
+
const hasWork = await hasTrackableWork(cwd);
|
|
111
|
+
if (hasWork) {
|
|
112
|
+
if (ctx.hasUI) {
|
|
113
|
+
ctx.ui.notify("Beads: Edit blocked. Claim an issue first.", "warning");
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
block: true,
|
|
117
|
+
reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isToolCallEventType("bash", event)) {
|
|
124
|
+
const command = event.input.command ?? "";
|
|
125
|
+
const commandUnquoted = stripQuoted(command);
|
|
126
|
+
|
|
127
|
+
if (isSpecialistsSubprocessCommand(commandUnquoted)) return undefined;
|
|
128
|
+
|
|
129
|
+
const closedIssueId = getClosedIssueIdFromCommand(commandUnquoted);
|
|
130
|
+
if (closedIssueId) {
|
|
131
|
+
const acked = await hasIssueMemoryAck(closedIssueId, cwd);
|
|
132
|
+
if (!acked) {
|
|
133
|
+
return {
|
|
134
|
+
block: true,
|
|
135
|
+
reason: closeMemoryBlockReason(closedIssueId),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (/\bgit\s+commit\b/.test(commandUnquoted)) {
|
|
141
|
+
const claim = await getActiveClaim(sessionId, cwd);
|
|
142
|
+
if (claim) {
|
|
143
|
+
return {
|
|
144
|
+
block: true,
|
|
145
|
+
reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return undefined;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
155
|
+
if (!isBashToolResult(event)) return undefined;
|
|
156
|
+
|
|
157
|
+
const command = event.input.command || "";
|
|
158
|
+
const sessionId = getSessionId(ctx);
|
|
159
|
+
const cwd = getCwd(ctx);
|
|
160
|
+
|
|
161
|
+
// Auto-claim on bd update --claim regardless of exit code.
|
|
162
|
+
if (/\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
|
|
163
|
+
const issueMatch = command.match(/\bbd\s+update\s+(\S+)/);
|
|
164
|
+
if (issueMatch) {
|
|
165
|
+
const issueId = issueMatch[1];
|
|
166
|
+
await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
|
|
167
|
+
memoryGateFired = false;
|
|
168
|
+
const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
|
|
169
|
+
return { content: [...event.content, { type: "text", text: claimNotice }] };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (/\bbd\s+close\b/.test(command) && !event.isError) {
|
|
174
|
+
const closeMatch = command.match(/\bbd\s+close\s+(\S+)/);
|
|
175
|
+
const closedIssueId = closeMatch?.[1] ?? null;
|
|
176
|
+
|
|
177
|
+
if (closedIssueId) {
|
|
178
|
+
await SubprocessRunner.run("bd", ["kv", "set", `closed-this-session:${sessionId}`, closedIssueId], { cwd });
|
|
179
|
+
memoryGateFired = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const memoryGateText = closedIssueId
|
|
183
|
+
? `\n\n**Beads Memory Gate**: close-time memory ack verified for \`${closedIssueId}\` (\`memory-acked:${closedIssueId}\`).`
|
|
184
|
+
: `\n\n**Beads**: Work completed. Consider if this session produced insights worth persisting via \`bd remember\`.`;
|
|
185
|
+
return { content: [...event.content, { type: "text", text: memoryGateText }] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return undefined;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Memory gate: clean up session markers and check ack at agent_end/session_shutdown.
|
|
192
|
+
// Memory gate prompt was already injected into bd close tool_result context (silent, agent-visible only).
|
|
193
|
+
// No UI notification — parity with Claude Stop hook {additionalContext} pattern.
|
|
194
|
+
const triggerMemoryGateIfNeeded = async (ctx: any) => {
|
|
195
|
+
const cwd = getCwd(ctx);
|
|
196
|
+
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
197
|
+
const sessionId = getSessionId(ctx);
|
|
198
|
+
|
|
199
|
+
const markerCheck = await SubprocessRunner.run("bd", ["kv", "get", `memory-gate-done:${sessionId}`], { cwd });
|
|
200
|
+
if (markerCheck.code === 0) {
|
|
201
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `memory-gate-done:${sessionId}`], { cwd });
|
|
202
|
+
await clearSessionMarkers(sessionId, cwd);
|
|
203
|
+
memoryGateFired = false;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (memoryGateFired) return;
|
|
208
|
+
|
|
209
|
+
const closedIssueId = await getClosedThisSession(sessionId, cwd);
|
|
210
|
+
if (!closedIssueId) return;
|
|
211
|
+
|
|
212
|
+
const closeTimeAcked = await hasIssueMemoryAck(closedIssueId, cwd);
|
|
213
|
+
if (closeTimeAcked) {
|
|
214
|
+
await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
|
|
215
|
+
memoryGateFired = false;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
memoryGateFired = true;
|
|
220
|
+
// No notify — memory gate was injected into bd close tool_result content (silent, agent-visible only).
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
224
|
+
await triggerMemoryGateIfNeeded(ctx);
|
|
225
|
+
return undefined;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
229
|
+
await triggerMemoryGateIfNeeded(ctx);
|
|
230
|
+
return undefined;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-beads",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "xtrm Pi extension: beads",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi",
|
|
11
|
+
"extension",
|
|
12
|
+
"xtrm"
|
|
13
|
+
],
|
|
14
|
+
"author": "xtrm",
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-pi Compact Header — table-style startup info with dynamic column widths
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { VERSION } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
7
|
+
|
|
8
|
+
export default function (pi: ExtensionAPI) {
|
|
9
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
10
|
+
if (!ctx.hasUI) return;
|
|
11
|
+
|
|
12
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
13
|
+
render(width: number): string[] {
|
|
14
|
+
const d = (s: string) => theme.fg("dim", s);
|
|
15
|
+
const a = (s: string) => theme.fg("accent", s);
|
|
16
|
+
|
|
17
|
+
const cmds = pi.getCommands();
|
|
18
|
+
const prompts = cmds.filter(c => c.source === "prompt").map(c => `/${c.name}`).join(" ");
|
|
19
|
+
const skills = cmds.filter(c => c.source === "skill").map(c => c.name).join(" ");
|
|
20
|
+
const model = ctx.model ? `${ctx.model.id}` : "no model";
|
|
21
|
+
const thinking = pi.getThinkingLevel();
|
|
22
|
+
const provider = ctx.model?.provider ?? "";
|
|
23
|
+
|
|
24
|
+
const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
|
|
25
|
+
const t = (s: string) => truncateToWidth(s, width);
|
|
26
|
+
const sep = d(" │ ");
|
|
27
|
+
|
|
28
|
+
// Right two columns are fixed width
|
|
29
|
+
const rCol = [
|
|
30
|
+
[d("esc"), a("interrupt"), d("S-tab"), a("thinking")],
|
|
31
|
+
[d("^C"), a("clear/exit"), d("^O"), a("expand")],
|
|
32
|
+
[d("^P"), a("model"), d("^G"), a("editor")],
|
|
33
|
+
[d("/"), a("commands"), d("^V"), a("paste")],
|
|
34
|
+
[d("!"), a("bash"), d(""), a("")],
|
|
35
|
+
];
|
|
36
|
+
const k1w = 6, v1w = 13, k2w = 6, v2w = 9;
|
|
37
|
+
const rightW = k1w + v1w + 3 + k2w + v2w + 3; // 3 for each sep
|
|
38
|
+
|
|
39
|
+
// Left column gets remaining space
|
|
40
|
+
const leftW = Math.max(20, width - rightW);
|
|
41
|
+
const lk = 9; // label width
|
|
42
|
+
|
|
43
|
+
const lCol = [
|
|
44
|
+
[d("version"), a(`v${VERSION} ${provider}`)],
|
|
45
|
+
[d("model"), a(model)],
|
|
46
|
+
[d("think"), a(thinking)],
|
|
47
|
+
[d(""), d("")],
|
|
48
|
+
[d(""), d("")],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const lines: string[] = [""];
|
|
52
|
+
for (let i = 0; i < 5; i++) {
|
|
53
|
+
const [lk0, lv0] = lCol[i];
|
|
54
|
+
const [rk0, rv0, rk1, rv1] = rCol[i];
|
|
55
|
+
const left = truncateToWidth(pad(lk0, lk) + lv0, leftW);
|
|
56
|
+
const right = pad(rk0, k1w) + pad(rv0, v1w) + sep + pad(rk1, k2w) + rv1;
|
|
57
|
+
lines.push(t(pad(left, leftW) + sep + right));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (prompts) lines.push(t(`${pad(d("prompts"), lk)}${a(prompts)}`));
|
|
61
|
+
if (skills) lines.push(t(`${pad(d("skills"), lk)}${a(skills)}`));
|
|
62
|
+
lines.push(d("─".repeat(width)));
|
|
63
|
+
|
|
64
|
+
return lines;
|
|
65
|
+
},
|
|
66
|
+
invalidate() {},
|
|
67
|
+
}));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-compact-header",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "xtrm Pi extension: compact-header",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pi",
|
|
11
|
+
"extension",
|
|
12
|
+
"xtrm"
|
|
13
|
+
],
|
|
14
|
+
"author": "xtrm",
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|