pi-creel 0.1.0
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 +40 -0
- package/package.json +37 -0
- package/src/capture.test.ts +35 -0
- package/src/capture.ts +62 -0
- package/src/index.test.ts +94 -0
- package/src/index.ts +151 -0
- package/src/tmux.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# pi-creel
|
|
2
|
+
|
|
3
|
+
`request_secret` for the pi coding agent — capture an API key into a local
|
|
4
|
+
`.env` **without it ever entering the chat/context**.
|
|
5
|
+
|
|
6
|
+
When the model needs a secret, it calls `request_secret` instead of asking the
|
|
7
|
+
user to paste one into the transcript. The tool opens a masked
|
|
8
|
+
`tmux display-popup` running the [`creel`](https://tackle.tools) binary; the
|
|
9
|
+
user pastes once, and the value goes straight from the popup into the target
|
|
10
|
+
`.env`. The pi process — and therefore the model — receives only a status token
|
|
11
|
+
(`added` / `updated` / `cancelled` / `error`), never the value.
|
|
12
|
+
|
|
13
|
+
## Tool
|
|
14
|
+
|
|
15
|
+
`request_secret(name, dest?)`
|
|
16
|
+
|
|
17
|
+
- `name` — the environment-variable name, e.g. `OPENAI_API_KEY`.
|
|
18
|
+
- `dest` — path to the `.env` (relative to cwd), default `.env`.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- A tmux session (`$TMUX`). Without one the tool fails fast and asks the user to
|
|
23
|
+
paste manually.
|
|
24
|
+
- The `creel` binary on `PATH` (installed via the tackle family download /
|
|
25
|
+
kempt). Without it the tool returns an actionable error.
|
|
26
|
+
|
|
27
|
+
## How the value stays out of context
|
|
28
|
+
|
|
29
|
+
`creel` does the masked read, the `.env` upsert (atomic, `chmod 600`), and the
|
|
30
|
+
gitignore check inside the popup. It writes only a one-word status token to a
|
|
31
|
+
temp file that this extension polls. The seam between the two —
|
|
32
|
+
`creel <NAME> --dest <PATH> --status-file <FILE>` — carries a name, a
|
|
33
|
+
destination, and a token, and nothing else. The same seam lets other harnesses
|
|
34
|
+
(Claude Code / Cursor, via a future `creel mcp`) reuse the identical capture
|
|
35
|
+
core.
|
|
36
|
+
|
|
37
|
+
## Design
|
|
38
|
+
|
|
39
|
+
Pure pieces (schema, popup command, token→reply mapping) live in `capture.ts`;
|
|
40
|
+
`index.ts` is thin, injectable pi/tmux/fs wiring — the same split as `pi-wakeup`.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-creel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "request_secret for the pi coding agent: capture an API key into a local .env via a tmux popup (the creel tool) so it never enters the chat/context.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/schuettc/pi-extensions.git",
|
|
10
|
+
"directory": "packages/pi-creel"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"pi-coding-agent",
|
|
16
|
+
"secret",
|
|
17
|
+
"api-key",
|
|
18
|
+
"dotenv"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/index.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
31
|
+
"test": "node --test src/*.test.ts"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
35
|
+
"typescript": "^5.6.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { validName, creelCommand, tokenToText } from "./capture.ts";
|
|
5
|
+
|
|
6
|
+
test("validName accepts env identifiers and rejects the rest", () => {
|
|
7
|
+
for (const s of ["A", "OPENAI_API_KEY", "_x", "a1_b2"]) {
|
|
8
|
+
assert.equal(validName(s), true, s);
|
|
9
|
+
}
|
|
10
|
+
for (const s of ["", "1ABC", "A-B", "A B", "A.B", "$X"]) {
|
|
11
|
+
assert.equal(validName(s), false, s);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("creelCommand single-quotes its arguments", () => {
|
|
16
|
+
assert.equal(
|
|
17
|
+
creelCommand("OPENAI_API_KEY", ".env", "/tmp/creel-status-abc"),
|
|
18
|
+
"creel 'OPENAI_API_KEY' --dest '.env' --status-file '/tmp/creel-status-abc'",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("creelCommand neutralizes a quote in the destination", () => {
|
|
23
|
+
const cmd = creelCommand("K", "a'b/.env", "/tmp/s");
|
|
24
|
+
assert.equal(cmd, "creel 'K' --dest 'a'\\''b/.env' --status-file '/tmp/s'");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("tokenToText maps every outcome and never echoes a value", () => {
|
|
28
|
+
assert.match(tokenToText("added", "OPENAI_API_KEY", ".env"), /Added OPENAI_API_KEY to \.env/);
|
|
29
|
+
assert.match(tokenToText("updated", "OPENAI_API_KEY", ".env"), /Updated OPENAI_API_KEY in \.env/);
|
|
30
|
+
assert.match(tokenToText("cancelled", "K", ".env"), /cancelled/i);
|
|
31
|
+
assert.match(tokenToText("error:dest-outside-cwd", "K", ".env"), /Could not store K: dest-outside-cwd/);
|
|
32
|
+
assert.match(tokenToText(undefined, "K", ".env"), /Timed out/);
|
|
33
|
+
assert.match(tokenToText("", "K", ".env"), /Timed out/);
|
|
34
|
+
assert.match(tokenToText("weird", "K", ".env"), /unexpected status \(weird\)/);
|
|
35
|
+
});
|
package/src/capture.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Pure pieces of the request_secret tool: parameter schema, env-var name
|
|
2
|
+
// validation, the popup command string, and the status-token -> reply mapping.
|
|
3
|
+
// None of these ever see the captured value — creel writes it straight to the
|
|
4
|
+
// .env, and this layer only ever handles a name, a destination, and a token.
|
|
5
|
+
|
|
6
|
+
export const REQUEST_SECRET_PARAMS = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
name: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description:
|
|
12
|
+
"The environment-variable name to store the secret under, e.g. OPENAI_API_KEY.",
|
|
13
|
+
},
|
|
14
|
+
dest: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description:
|
|
17
|
+
"Path to the .env file to write, relative to the working directory. Defaults to .env.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ["name"],
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
// ValidName mirrors the Go creel.ValidName: a legal env-var name.
|
|
24
|
+
export function validName(s: string): boolean {
|
|
25
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function shq(s: string): string {
|
|
29
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// creelCommand is the sh command tmux display-popup runs. name/dest/status are
|
|
33
|
+
// single-quoted; the popup's creel writes the value to dest and the token to
|
|
34
|
+
// status.
|
|
35
|
+
export function creelCommand(name: string, dest: string, statusFile: string): string {
|
|
36
|
+
return `creel ${shq(name)} --dest ${shq(dest)} --status-file ${shq(statusFile)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// tokenToText maps creel's status token to the reply the model sees. It never
|
|
40
|
+
// includes the secret — only the outcome.
|
|
41
|
+
export function tokenToText(
|
|
42
|
+
token: string | undefined,
|
|
43
|
+
name: string,
|
|
44
|
+
dest: string,
|
|
45
|
+
): string {
|
|
46
|
+
if (token === undefined || token === "") {
|
|
47
|
+
return `Timed out waiting for the creel popup; nothing was recorded for ${name}.`;
|
|
48
|
+
}
|
|
49
|
+
switch (token) {
|
|
50
|
+
case "added":
|
|
51
|
+
return `Added ${name} to ${dest} (chmod 600). The value was not shown here.`;
|
|
52
|
+
case "updated":
|
|
53
|
+
return `Updated ${name} in ${dest}. The value was not shown here.`;
|
|
54
|
+
case "cancelled":
|
|
55
|
+
return `Capture cancelled; nothing was written for ${name}.`;
|
|
56
|
+
default:
|
|
57
|
+
if (token.startsWith("error:")) {
|
|
58
|
+
return `Could not store ${name}: ${token.slice("error:".length)}.`;
|
|
59
|
+
}
|
|
60
|
+
return `Capture finished with an unexpected status (${token}).`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { createCreel, type Deps } from "./index.ts";
|
|
5
|
+
import type { TmuxContext } from "./tmux.ts";
|
|
6
|
+
|
|
7
|
+
type Registered = {
|
|
8
|
+
name: string;
|
|
9
|
+
execute: (id: string, params: any) => Promise<{ content: { text: string }[] }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function harness(deps: Deps): Registered {
|
|
13
|
+
let reg: Registered | undefined;
|
|
14
|
+
const pi = {
|
|
15
|
+
registerTool(def: Registered) {
|
|
16
|
+
reg = def;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
createCreel(pi, deps);
|
|
20
|
+
if (!reg) throw new Error("tool not registered");
|
|
21
|
+
return reg;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tmux: TmuxContext = { socket: "proj-x", pane: "%1" };
|
|
25
|
+
const okDeps = (overrides: Partial<Deps> = {}): Deps => ({
|
|
26
|
+
resolveTmux: () => tmux,
|
|
27
|
+
creelOnPath: () => true,
|
|
28
|
+
spawnPopup: () => {},
|
|
29
|
+
waitForToken: async () => "added",
|
|
30
|
+
tmpStatusPath: () => "/tmp/creel-status-test",
|
|
31
|
+
...overrides,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
async function run(reg: Registered, params: any): Promise<string> {
|
|
35
|
+
const out = await reg.execute("id", params);
|
|
36
|
+
return out.content[0].text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("registers request_secret", () => {
|
|
40
|
+
const reg = harness(okDeps());
|
|
41
|
+
assert.equal(reg.name, "request_secret");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rejects an invalid name before touching tmux", async () => {
|
|
45
|
+
let spawned = false;
|
|
46
|
+
const reg = harness(okDeps({ spawnPopup: () => { spawned = true; } }));
|
|
47
|
+
const text = await run(reg, { name: "BAD-NAME" });
|
|
48
|
+
assert.match(text, /not a valid environment-variable name/);
|
|
49
|
+
assert.equal(spawned, false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("fails fast when not in tmux", async () => {
|
|
53
|
+
const reg = harness(okDeps({ resolveTmux: () => undefined }));
|
|
54
|
+
const text = await run(reg, { name: "OPENAI_API_KEY" });
|
|
55
|
+
assert.match(text, /needs a tmux session/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("fails when creel is not on PATH", async () => {
|
|
59
|
+
const reg = harness(okDeps({ creelOnPath: () => false }));
|
|
60
|
+
const text = await run(reg, { name: "OPENAI_API_KEY" });
|
|
61
|
+
assert.match(text, /'creel' binary was not found/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("passes name and default dest to the popup and maps the token", async () => {
|
|
65
|
+
let gotCommand = "";
|
|
66
|
+
const reg = harness(
|
|
67
|
+
okDeps({
|
|
68
|
+
spawnPopup: (_t, command) => { gotCommand = command; },
|
|
69
|
+
waitForToken: async () => "updated",
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const text = await run(reg, { name: "OPENAI_API_KEY" });
|
|
73
|
+
assert.match(gotCommand, /creel 'OPENAI_API_KEY' --dest '\.env' --status-file/);
|
|
74
|
+
assert.match(text, /Updated OPENAI_API_KEY in \.env/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("honors a custom dest", async () => {
|
|
78
|
+
let gotCommand = "";
|
|
79
|
+
const reg = harness(okDeps({ spawnPopup: (_t, c) => { gotCommand = c; } }));
|
|
80
|
+
await run(reg, { name: "K", dest: "config/.env" });
|
|
81
|
+
assert.match(gotCommand, /--dest 'config\/\.env'/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("reports a timeout when no token arrives", async () => {
|
|
85
|
+
const reg = harness(okDeps({ waitForToken: async () => undefined }));
|
|
86
|
+
const text = await run(reg, { name: "K" });
|
|
87
|
+
assert.match(text, /Timed out/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("surfaces a creel error token", async () => {
|
|
91
|
+
const reg = harness(okDeps({ waitForToken: async () => "error:dest-outside-cwd" }));
|
|
92
|
+
const text = await run(reg, { name: "K", dest: "../x.env" });
|
|
93
|
+
assert.match(text, /Could not store K: dest-outside-cwd/);
|
|
94
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// pi-creel — the request_secret tool.
|
|
2
|
+
//
|
|
3
|
+
// When the model needs an API key it calls request_secret instead of asking
|
|
4
|
+
// the user to paste it into chat. This wiring resolves tmux, opens a
|
|
5
|
+
// display-popup running the creel binary, and waits for creel to write a
|
|
6
|
+
// status token to a temp file. The captured value goes straight from the popup
|
|
7
|
+
// into the target .env; this process — and therefore the model's context —
|
|
8
|
+
// only ever sees the token (added / updated / cancelled / error), never the
|
|
9
|
+
// value.
|
|
10
|
+
//
|
|
11
|
+
// The pure pieces (schema, command string, token mapping) live in capture.ts;
|
|
12
|
+
// this file is the thin, injectable pi/tmux/fs wiring, in the same split
|
|
13
|
+
// pi-wakeup and guardrails use.
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
21
|
+
|
|
22
|
+
import { resolveTmux, type TmuxContext } from "./tmux.ts";
|
|
23
|
+
import {
|
|
24
|
+
REQUEST_SECRET_PARAMS,
|
|
25
|
+
creelCommand,
|
|
26
|
+
tokenToText,
|
|
27
|
+
validName,
|
|
28
|
+
} from "./capture.ts";
|
|
29
|
+
|
|
30
|
+
const POPUP_WIDTH = "64";
|
|
31
|
+
const POPUP_HEIGHT = "14";
|
|
32
|
+
const TOKEN_TIMEOUT_MS = 120_000;
|
|
33
|
+
const POLL_INTERVAL_MS = 150;
|
|
34
|
+
|
|
35
|
+
export type Deps = {
|
|
36
|
+
resolveTmux?: (env: NodeJS.ProcessEnv) => TmuxContext | undefined;
|
|
37
|
+
creelOnPath?: () => boolean;
|
|
38
|
+
spawnPopup?: (tmux: TmuxContext, command: string) => void;
|
|
39
|
+
waitForToken?: (statusPath: string, timeoutMs: number) => Promise<string | undefined>;
|
|
40
|
+
tmpStatusPath?: () => string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function defaultCreelOnPath(): boolean {
|
|
44
|
+
try {
|
|
45
|
+
execFileSync("creel", ["version"], { stdio: "ignore" });
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function defaultSpawnPopup(tmux: TmuxContext, command: string): void {
|
|
53
|
+
// display-popup returns immediately (the popup runs detached on the server),
|
|
54
|
+
// so completion is observed out-of-band via the status file.
|
|
55
|
+
execFileSync(
|
|
56
|
+
"tmux",
|
|
57
|
+
[
|
|
58
|
+
"-L", tmux.socket,
|
|
59
|
+
"display-popup",
|
|
60
|
+
"-t", tmux.pane,
|
|
61
|
+
"-E",
|
|
62
|
+
"-w", POPUP_WIDTH,
|
|
63
|
+
"-h", POPUP_HEIGHT,
|
|
64
|
+
"-T", " creel · request_secret ",
|
|
65
|
+
command,
|
|
66
|
+
],
|
|
67
|
+
{ stdio: "ignore" },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function defaultWaitForToken(
|
|
72
|
+
statusPath: string,
|
|
73
|
+
timeoutMs: number,
|
|
74
|
+
): Promise<string | undefined> {
|
|
75
|
+
const deadline = Date.now() + timeoutMs;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
if (existsSync(statusPath)) {
|
|
78
|
+
const raw = readFileSync(statusPath, "utf-8");
|
|
79
|
+
// creel writes "<token>\n" in a single write; wait for the newline so we
|
|
80
|
+
// never read a half-written token.
|
|
81
|
+
if (raw.endsWith("\n")) return raw.trim();
|
|
82
|
+
}
|
|
83
|
+
await sleep(POLL_INTERVAL_MS);
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function defaultTmpStatusPath(): string {
|
|
89
|
+
return join(tmpdir(), `creel-status-${randomUUID()}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function reply(text: string) {
|
|
93
|
+
return { content: [{ type: "text", text }] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createCreel(pi: any, deps: Deps = {}): void {
|
|
97
|
+
const resolve = deps.resolveTmux ?? resolveTmux;
|
|
98
|
+
const onPath = deps.creelOnPath ?? defaultCreelOnPath;
|
|
99
|
+
const spawn = deps.spawnPopup ?? defaultSpawnPopup;
|
|
100
|
+
const wait = deps.waitForToken ?? defaultWaitForToken;
|
|
101
|
+
const tmpPath = deps.tmpStatusPath ?? defaultTmpStatusPath;
|
|
102
|
+
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "request_secret",
|
|
105
|
+
label: "Request Secret",
|
|
106
|
+
description:
|
|
107
|
+
"Capture an API key or secret from the user without it entering the chat. Opens a masked tmux popup (the creel tool); the user pastes once and the value is written straight into a local .env. Call this whenever you would otherwise ask the user to paste a key — you receive only a confirmation (added/updated), never the value. Requires a tmux session and the 'creel' binary on PATH.",
|
|
108
|
+
promptSnippet:
|
|
109
|
+
"request_secret(name, dest?) — need an API key/secret? Capture it into a .env via a popup instead of asking the user to paste it into chat.",
|
|
110
|
+
parameters: REQUEST_SECRET_PARAMS,
|
|
111
|
+
async execute(_id: string, params: { name?: string; dest?: string }) {
|
|
112
|
+
const name = typeof params?.name === "string" ? params.name : "";
|
|
113
|
+
const dest = typeof params?.dest === "string" && params.dest ? params.dest : ".env";
|
|
114
|
+
|
|
115
|
+
if (!validName(name)) {
|
|
116
|
+
return reply(`"${name}" is not a valid environment-variable name; nothing captured.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tmux = resolve(process.env);
|
|
120
|
+
if (!tmux) {
|
|
121
|
+
return reply(
|
|
122
|
+
"request_secret needs a tmux session and none is attached. Ask the user to paste the key manually, or run pi under tmux.",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!onPath()) {
|
|
126
|
+
return reply(
|
|
127
|
+
"The 'creel' binary was not found on PATH. Install the tackle 'creel' tool, then retry.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const statusPath = tmpPath();
|
|
132
|
+
try {
|
|
133
|
+
spawn(tmux, creelCommand(name, dest, statusPath));
|
|
134
|
+
const token = await wait(statusPath, TOKEN_TIMEOUT_MS);
|
|
135
|
+
return reply(tokenToText(token, name, dest));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return reply(`Failed to run the creel popup: ${String(error)}`);
|
|
138
|
+
} finally {
|
|
139
|
+
try {
|
|
140
|
+
rmSync(statusPath, { force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
// best-effort cleanup
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default function (pi: any) {
|
|
150
|
+
createCreel(pi);
|
|
151
|
+
}
|
package/src/tmux.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// tmux context resolution, mirrored from pi-harness. Duplicated rather than
|
|
2
|
+
// cross-imported so pi-creel stays a standalone package (the same choice the
|
|
3
|
+
// other packages in this monorepo make).
|
|
4
|
+
|
|
5
|
+
export type TmuxContext = {
|
|
6
|
+
socket: string;
|
|
7
|
+
pane: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// $TMUX is "<socket-path>,<pid>,<session>". The socket NAME is the basename of
|
|
11
|
+
// the first field; tmux is addressed as `tmux -L <name>`.
|
|
12
|
+
export function resolveTmux(
|
|
13
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
14
|
+
): TmuxContext | undefined {
|
|
15
|
+
const tmuxEnv = env.TMUX;
|
|
16
|
+
const pane = env.TMUX_PANE;
|
|
17
|
+
if (!tmuxEnv || !pane) return undefined;
|
|
18
|
+
const socketPath = tmuxEnv.split(",")[0];
|
|
19
|
+
if (!socketPath) return undefined;
|
|
20
|
+
const socket = socketPath.slice(socketPath.lastIndexOf("/") + 1);
|
|
21
|
+
if (!socket) return undefined;
|
|
22
|
+
return { socket, pane };
|
|
23
|
+
}
|