pi-atuin 0.1.3 → 0.1.7
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 +34 -8
- package/atuin-cli.ts +106 -0
- package/bash-exit-code.ts +20 -0
- package/bash-tracker.ts +87 -0
- package/commands.ts +48 -0
- package/config.ts +55 -0
- package/history-store.ts +6 -22
- package/index.ts +16 -6
- package/package.json +1 -1
- package/test/bash-tracker.test.ts +60 -0
- package/test/commands.test.ts +18 -0
- package/test/config.test.ts +9 -0
package/README.md
CHANGED
|
@@ -35,30 +35,56 @@ curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh
|
|
|
35
35
|
|
|
36
36
|
# Register / login
|
|
37
37
|
atuin register -u <username> -e <email>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
pi-atuin records history into atuin automatically — no `atuin hook install pi` needed. That hook registers a conflicting `bash` tool and breaks extensions like [pi-tool-display](https://github.com/MasuRii/pi-tool-display).
|
|
41
|
+
|
|
42
|
+
If you previously ran `atuin hook install pi`, remove the hook extension:
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
```bash
|
|
45
|
+
rm ~/.pi/agent/extensions/atuin.ts
|
|
46
|
+
pi /reload
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
With
|
|
49
|
+
With atuin installed, history syncs bidirectionally. Search in either pi (↑) or your shell (`atuin search`).
|
|
44
50
|
|
|
45
51
|
### JSONL-only (no atuin)
|
|
46
52
|
|
|
47
|
-
No setup needed.
|
|
53
|
+
No setup needed. Pi prompts are stored in `~/.pi/agent/pi-history.jsonl`.
|
|
54
|
+
|
|
55
|
+
## What gets recorded
|
|
56
|
+
|
|
57
|
+
| Source | JSONL (↑ search) | Atuin DB |
|
|
58
|
+
|--------|------------------|----------|
|
|
59
|
+
| Your pi prompts | Yes | Yes |
|
|
60
|
+
| Agent `bash` tool calls | No | Yes (default on) |
|
|
61
|
+
| Your `!` shell commands | No | Yes |
|
|
62
|
+
|
|
63
|
+
- **Prompts** always go to JSONL and atuin (when installed).
|
|
64
|
+
- **Agent bash** is recorded to atuin only (not mixed into ↑ prompt search). On by default; `/atuin record-agent-history` toggles it.
|
|
65
|
+
- **`!` commands** are your direct shell input in pi — tracked in atuin, not JSONL.
|
|
48
66
|
|
|
49
67
|
## Usage
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
### History search
|
|
70
|
+
|
|
71
|
+
1. Press **↑** when the input is empty
|
|
52
72
|
2. Type to fuzzy-filter
|
|
53
73
|
3. **↑↓** to navigate, **Enter** to select, **Esc** to cancel
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
### Commands
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
/atuin record-agent-history # toggle agent bash → atuin, shows on/off
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Settings persist in `~/.pi/agent/extensions/pi-atuin/config.json`.
|
|
56
82
|
|
|
57
83
|
## Privacy
|
|
58
84
|
|
|
59
85
|
- History file permissions: `600` (owner-only)
|
|
60
|
-
- Max 1,000 entries, auto-trimmed
|
|
61
|
-
- Atuin calls use `execFile` (no shell injection)
|
|
86
|
+
- Max 1,000 JSONL entries, auto-trimmed
|
|
87
|
+
- Atuin calls use `execFile` / `pi.exec` (no shell injection)
|
|
62
88
|
- No hardcoded credentials or keys
|
|
63
89
|
|
|
64
90
|
## License
|
package/atuin-cli.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Atuin CLI helpers for pi-atuin.
|
|
3
|
+
*
|
|
4
|
+
* Uses execFile (history-store) or pi.exec (bash-tracker) via a thin adapter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
export const ATUIN_AUTHOR = "pi";
|
|
14
|
+
export const ATUIN_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
16
|
+
export type AtuinExecResult = {
|
|
17
|
+
code: number;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type AtuinExec = (
|
|
23
|
+
command: string,
|
|
24
|
+
args: string[],
|
|
25
|
+
options?: { cwd?: string; timeout?: number },
|
|
26
|
+
) => Promise<AtuinExecResult>;
|
|
27
|
+
|
|
28
|
+
export function createExecFileAtuinExec(): AtuinExec {
|
|
29
|
+
return async (command, args, options) => {
|
|
30
|
+
try {
|
|
31
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
32
|
+
cwd: options?.cwd,
|
|
33
|
+
timeout: options?.timeout ?? ATUIN_TIMEOUT_MS,
|
|
34
|
+
});
|
|
35
|
+
return { code: 0, stdout: String(stdout), stderr: String(stderr) };
|
|
36
|
+
} catch (err: unknown) {
|
|
37
|
+
const e = err as { code?: number; stdout?: string; stderr?: string };
|
|
38
|
+
return {
|
|
39
|
+
code: typeof e.code === "number" ? e.code : 1,
|
|
40
|
+
stdout: String(e.stdout ?? ""),
|
|
41
|
+
stderr: String(e.stderr ?? ""),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createPiAtuinExec(pi: ExtensionAPI): AtuinExec {
|
|
48
|
+
return async (command, args, options) => {
|
|
49
|
+
const result = await pi.exec(command, args, {
|
|
50
|
+
cwd: options?.cwd,
|
|
51
|
+
timeout: options?.timeout ?? ATUIN_TIMEOUT_MS,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
code: result.code,
|
|
55
|
+
stdout: result.stdout,
|
|
56
|
+
stderr: result.stderr,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function startAtuinHistory(
|
|
62
|
+
exec: AtuinExec,
|
|
63
|
+
cwd: string,
|
|
64
|
+
command: string,
|
|
65
|
+
): Promise<string | undefined> {
|
|
66
|
+
try {
|
|
67
|
+
const result = await exec(
|
|
68
|
+
"atuin",
|
|
69
|
+
["history", "start", "--author", ATUIN_AUTHOR, "--", command],
|
|
70
|
+
{ cwd, timeout: ATUIN_TIMEOUT_MS },
|
|
71
|
+
);
|
|
72
|
+
if (result.code !== 0) return undefined;
|
|
73
|
+
const id = result.stdout.trim();
|
|
74
|
+
return id.length > 0 ? id : undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function endAtuinHistory(
|
|
81
|
+
exec: AtuinExec,
|
|
82
|
+
cwd: string,
|
|
83
|
+
historyId: string,
|
|
84
|
+
exitCode: number,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
await exec(
|
|
88
|
+
"atuin",
|
|
89
|
+
["history", "end", historyId, "--exit", String(exitCode)],
|
|
90
|
+
{ cwd, timeout: ATUIN_TIMEOUT_MS },
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
// Never block pi on atuin failures.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function recordAtuinCommand(
|
|
98
|
+
exec: AtuinExec,
|
|
99
|
+
cwd: string,
|
|
100
|
+
command: string,
|
|
101
|
+
exitCode = 0,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const historyId = await startAtuinHistory(exec, cwd, command);
|
|
104
|
+
if (!historyId) return;
|
|
105
|
+
await endAtuinHistory(exec, cwd, historyId, exitCode);
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Derive exit code from a bash tool_result event. */
|
|
2
|
+
export function parseBashExitCode(event: {
|
|
3
|
+
isError: boolean;
|
|
4
|
+
content: Array<{ type: string; text?: string }>;
|
|
5
|
+
}): number {
|
|
6
|
+
if (!event.isError) return 0;
|
|
7
|
+
|
|
8
|
+
const text = event.content
|
|
9
|
+
.filter((part) => part.type === "text")
|
|
10
|
+
.map((part) => part.text ?? "")
|
|
11
|
+
.join("\n");
|
|
12
|
+
|
|
13
|
+
const exitMatch = text.match(/Command exited with code (\d+)/);
|
|
14
|
+
if (exitMatch) return Number.parseInt(exitMatch[1]!, 10);
|
|
15
|
+
|
|
16
|
+
if (text.includes("Command aborted")) return 130;
|
|
17
|
+
if (text.includes("Command timed out")) return 124;
|
|
18
|
+
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
package/bash-tracker.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-based bash → atuin tracking for pi-atuin.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `atuin hook install pi` without registering a conflicting `bash` tool.
|
|
5
|
+
* Compatible with pi-tool-display and other extensions that own bash rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createLocalBashOperations, isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
createPiAtuinExec,
|
|
12
|
+
endAtuinHistory,
|
|
13
|
+
startAtuinHistory,
|
|
14
|
+
} from "./atuin-cli.js";
|
|
15
|
+
import { parseBashExitCode } from "./bash-exit-code.js";
|
|
16
|
+
import type { ConfigGetter } from "./commands.js";
|
|
17
|
+
|
|
18
|
+
interface PendingBashHistory {
|
|
19
|
+
historyId: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerBashTracker(pi: ExtensionAPI, getConfig: ConfigGetter): void {
|
|
24
|
+
const exec = createPiAtuinExec(pi);
|
|
25
|
+
const pending = new Map<string, PendingBashHistory>();
|
|
26
|
+
|
|
27
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
28
|
+
if (!getConfig().recordAgentHistory) return;
|
|
29
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
30
|
+
|
|
31
|
+
const command = event.input.command?.trim();
|
|
32
|
+
if (!command) return;
|
|
33
|
+
|
|
34
|
+
const historyId = await startAtuinHistory(exec, ctx.cwd, command);
|
|
35
|
+
if (historyId) {
|
|
36
|
+
pending.set(event.toolCallId, { historyId, cwd: ctx.cwd });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
pi.on("tool_result", async (event) => {
|
|
41
|
+
if (!getConfig().recordAgentHistory) return;
|
|
42
|
+
if (event.toolName !== "bash") return;
|
|
43
|
+
|
|
44
|
+
const track = pending.get(event.toolCallId);
|
|
45
|
+
if (!track) return;
|
|
46
|
+
|
|
47
|
+
pending.delete(event.toolCallId);
|
|
48
|
+
await endAtuinHistory(
|
|
49
|
+
exec,
|
|
50
|
+
track.cwd,
|
|
51
|
+
track.historyId,
|
|
52
|
+
parseBashExitCode(event),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const local = createLocalBashOperations();
|
|
57
|
+
|
|
58
|
+
pi.on("user_bash", async (event) => {
|
|
59
|
+
const historyId = await startAtuinHistory(exec, event.cwd, event.command);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
operations: {
|
|
63
|
+
async exec(command, cwd, options) {
|
|
64
|
+
let exitCode: number | null = null;
|
|
65
|
+
try {
|
|
66
|
+
const result = await local.exec(command, cwd, options);
|
|
67
|
+
exitCode = result.exitCode;
|
|
68
|
+
return result;
|
|
69
|
+
} finally {
|
|
70
|
+
if (historyId) {
|
|
71
|
+
await endAtuinHistory(
|
|
72
|
+
exec,
|
|
73
|
+
cwd,
|
|
74
|
+
historyId,
|
|
75
|
+
exitCode ?? (options.signal?.aborted ? 130 : 1),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
pi.on("session_shutdown", () => {
|
|
85
|
+
pending.clear();
|
|
86
|
+
});
|
|
87
|
+
}
|
package/commands.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /atuin slash commands for pi-atuin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { PiAtuinConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
export type ConfigGetter = () => PiAtuinConfig;
|
|
9
|
+
export type ConfigSetter = (next: PiAtuinConfig) => void;
|
|
10
|
+
|
|
11
|
+
export function toggleRecordAgentHistory(current: boolean): boolean {
|
|
12
|
+
return !current;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatRecordAgentHistoryStatus(enabled: boolean): string {
|
|
16
|
+
return `Agent bash history recording: ${enabled ? "on" : "off"}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerAtuinCommands(
|
|
20
|
+
pi: ExtensionAPI,
|
|
21
|
+
getConfig: ConfigGetter,
|
|
22
|
+
setConfig: ConfigSetter,
|
|
23
|
+
): void {
|
|
24
|
+
pi.registerCommand("atuin", {
|
|
25
|
+
description: "Toggle agent bash history recording",
|
|
26
|
+
handler: async (args, ctx) => {
|
|
27
|
+
const subcommand = args.trim().split(/\s+/)[0]?.toLowerCase();
|
|
28
|
+
|
|
29
|
+
if (subcommand !== "record-agent-history") {
|
|
30
|
+
ctx.ui.notify("/atuin record-agent-history", "info");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toggleRecordAgentHistoryCommand(ctx, getConfig, setConfig);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toggleRecordAgentHistoryCommand(
|
|
40
|
+
ctx: ExtensionCommandContext,
|
|
41
|
+
getConfig: ConfigGetter,
|
|
42
|
+
setConfig: ConfigSetter,
|
|
43
|
+
): void {
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
const next = toggleRecordAgentHistory(config.recordAgentHistory);
|
|
46
|
+
setConfig({ ...config, recordAgentHistory: next });
|
|
47
|
+
ctx.ui.notify(formatRecordAgentHistoryStatus(next), "info");
|
|
48
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-atuin settings persisted under ~/.pi/agent/extensions/pi-atuin/config.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = join(
|
|
10
|
+
process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
|
|
11
|
+
"extensions",
|
|
12
|
+
"pi-atuin",
|
|
13
|
+
);
|
|
14
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
15
|
+
|
|
16
|
+
export interface PiAtuinConfig {
|
|
17
|
+
/** Record agent `bash` tool calls into atuin (default: true). */
|
|
18
|
+
recordAgentHistory: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_CONFIG: PiAtuinConfig = {
|
|
22
|
+
recordAgentHistory: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function normalizeConfig(source: unknown): PiAtuinConfig {
|
|
26
|
+
const raw = source && typeof source === "object" ? (source as Partial<PiAtuinConfig>) : {};
|
|
27
|
+
return {
|
|
28
|
+
recordAgentHistory:
|
|
29
|
+
typeof raw.recordAgentHistory === "boolean"
|
|
30
|
+
? raw.recordAgentHistory
|
|
31
|
+
: DEFAULT_CONFIG.recordAgentHistory,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadConfig(): PiAtuinConfig {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
38
|
+
const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf8")) as unknown;
|
|
39
|
+
return normalizeConfig(raw);
|
|
40
|
+
} catch {
|
|
41
|
+
return { ...DEFAULT_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveConfig(config: PiAtuinConfig): void {
|
|
46
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
47
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
mode: 0o600,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getConfigPath(): string {
|
|
54
|
+
return CONFIG_FILE;
|
|
55
|
+
}
|
package/history-store.ts
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
import { readFile, writeFile, appendFile, mkdir } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { fuzzySearch } from "./fuzzy-match.js";
|
|
9
|
+
import { createExecFileAtuinExec, recordAtuinCommand } from "./atuin-cli.js";
|
|
8
10
|
import { execFile } from "node:child_process";
|
|
9
11
|
import { promisify } from "node:util";
|
|
10
|
-
import { fuzzySearch } from "./fuzzy-match.js";
|
|
11
12
|
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
const atuinExec = createExecFileAtuinExec();
|
|
13
15
|
|
|
14
16
|
const JSONL_PATH = join(homedir(), ".pi", "agent", "pi-history.jsonl");
|
|
15
|
-
const
|
|
17
|
+
const ATUIN_READ_TIMEOUT_MS = 5_000;
|
|
16
18
|
const MAX_ENTRIES = 1000;
|
|
17
19
|
|
|
18
20
|
export interface HistoryEntry {
|
|
@@ -111,7 +113,7 @@ async function readAtuinHistory(): Promise<HistoryEntry[]> {
|
|
|
111
113
|
const { stdout } = await execFileAsync(
|
|
112
114
|
"atuin",
|
|
113
115
|
["search", "--format", "{time}\t{command}", "--limit", "200"],
|
|
114
|
-
{ timeout:
|
|
116
|
+
{ timeout: ATUIN_READ_TIMEOUT_MS },
|
|
115
117
|
);
|
|
116
118
|
|
|
117
119
|
const lines = stdout.split("\n").filter((l) => l.trim());
|
|
@@ -212,24 +214,6 @@ export function invalidateCache(): void {
|
|
|
212
214
|
// Write back to atuin (so shell atuin search also sees pi prompts)
|
|
213
215
|
// ---------------------------------------------------------------------------
|
|
214
216
|
|
|
215
|
-
const ATUIN_AUTHOR = "pi";
|
|
216
|
-
|
|
217
217
|
export async function writeToAtuin(text: string, cwd: string): Promise<void> {
|
|
218
|
-
|
|
219
|
-
const { stdout } = await execFileAsync(
|
|
220
|
-
"atuin",
|
|
221
|
-
["history", "start", "--author", ATUIN_AUTHOR, "--", text],
|
|
222
|
-
{ cwd, timeout: ATUIN_TIMEOUT_MS },
|
|
223
|
-
);
|
|
224
|
-
const id = stdout.trim();
|
|
225
|
-
if (id) {
|
|
226
|
-
await execFileAsync(
|
|
227
|
-
"atuin",
|
|
228
|
-
["history", "end", id, "--exit", "0"],
|
|
229
|
-
{ cwd, timeout: ATUIN_TIMEOUT_MS },
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
// atuin not installed or failed — silently ignore
|
|
234
|
-
}
|
|
218
|
+
await recordAtuinCommand(atuinExec, cwd, text, 0);
|
|
235
219
|
}
|
package/index.ts
CHANGED
|
@@ -11,8 +11,21 @@ import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { matchesKey, Key } from "@earendil-works/pi-tui";
|
|
12
12
|
import { addEntry, invalidateCache, listRecent, writeToAtuin, isRecentWrite, markRecentWrite } from "./history-store.js";
|
|
13
13
|
import { HistorySearchComponent } from "./search-ui.js";
|
|
14
|
+
import { registerBashTracker } from "./bash-tracker.js";
|
|
15
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
16
|
+
import { registerAtuinCommands } from "./commands.js";
|
|
14
17
|
|
|
15
18
|
export default function piAtuin(pi: ExtensionAPI) {
|
|
19
|
+
let config = loadConfig();
|
|
20
|
+
|
|
21
|
+
const getConfig = () => config;
|
|
22
|
+
const setConfig = (next: typeof config) => {
|
|
23
|
+
config = next;
|
|
24
|
+
saveConfig(config);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
registerBashTracker(pi, getConfig);
|
|
28
|
+
registerAtuinCommands(pi, getConfig, setConfig);
|
|
16
29
|
// ---- 1. Record user inputs to history ----
|
|
17
30
|
pi.on("input", async (event, ctx) => {
|
|
18
31
|
if (event.source === "interactive" && event.text.trim()) {
|
|
@@ -35,12 +48,9 @@ export default function piAtuin(pi: ExtensionAPI) {
|
|
|
35
48
|
const editor = new CustomEditor(tui, theme, keybindings);
|
|
36
49
|
|
|
37
50
|
editor.onExtensionShortcut = (data: string) => {
|
|
38
|
-
if (matchesKey(data, Key.up)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
openSearch(ctx, editor);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
51
|
+
if (matchesKey(data, Key.up) && editor.getText().length === 0) {
|
|
52
|
+
openSearch(ctx, editor);
|
|
53
|
+
return true;
|
|
44
54
|
}
|
|
45
55
|
return false;
|
|
46
56
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseBashExitCode } from "../bash-exit-code.ts";
|
|
4
|
+
|
|
5
|
+
describe("parseBashExitCode", () => {
|
|
6
|
+
it("returns 0 for successful commands", () => {
|
|
7
|
+
assert.equal(
|
|
8
|
+
parseBashExitCode({
|
|
9
|
+
isError: false,
|
|
10
|
+
content: [{ type: "text", text: "hello\n" }],
|
|
11
|
+
}),
|
|
12
|
+
0,
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses explicit exit codes from bash output", () => {
|
|
17
|
+
assert.equal(
|
|
18
|
+
parseBashExitCode({
|
|
19
|
+
isError: true,
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: "something failed\n\nCommand exited with code 42",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
42,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns 130 for aborted commands", () => {
|
|
32
|
+
assert.equal(
|
|
33
|
+
parseBashExitCode({
|
|
34
|
+
isError: true,
|
|
35
|
+
content: [{ type: "text", text: "Command aborted" }],
|
|
36
|
+
}),
|
|
37
|
+
130,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns 124 for timed out commands", () => {
|
|
42
|
+
assert.equal(
|
|
43
|
+
parseBashExitCode({
|
|
44
|
+
isError: true,
|
|
45
|
+
content: [{ type: "text", text: "Command timed out after 30 seconds" }],
|
|
46
|
+
}),
|
|
47
|
+
124,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("defaults to 1 for generic errors", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
parseBashExitCode({
|
|
54
|
+
isError: true,
|
|
55
|
+
content: [{ type: "text", text: "unknown failure" }],
|
|
56
|
+
}),
|
|
57
|
+
1,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
formatRecordAgentHistoryStatus,
|
|
5
|
+
toggleRecordAgentHistory,
|
|
6
|
+
} from "../commands.ts";
|
|
7
|
+
|
|
8
|
+
describe("record-agent-history toggle", () => {
|
|
9
|
+
it("flips the current value", () => {
|
|
10
|
+
assert.equal(toggleRecordAgentHistory(true), false);
|
|
11
|
+
assert.equal(toggleRecordAgentHistory(false), true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("formats status for notify", () => {
|
|
15
|
+
assert.equal(formatRecordAgentHistoryStatus(true), "Agent bash history recording: on");
|
|
16
|
+
assert.equal(formatRecordAgentHistoryStatus(false), "Agent bash history recording: off");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DEFAULT_CONFIG } from "../config.ts";
|
|
4
|
+
|
|
5
|
+
describe("DEFAULT_CONFIG", () => {
|
|
6
|
+
it("records agent bash history by default", () => {
|
|
7
|
+
assert.equal(DEFAULT_CONFIG.recordAgentHistory, true);
|
|
8
|
+
});
|
|
9
|
+
});
|