@dungle-scrubs/tallow 0.9.6 → 0.9.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/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/process-cleanup.js +1 -1
- package/dist/process-cleanup.js.map +1 -1
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +32 -26
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.js +1 -1
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/_shared/__tests__/shell-policy.test.ts +19 -0
- package/extensions/_shared/shell-policy.ts +121 -1
- package/extensions/command-expansion/index.ts +8 -2
- package/extensions/context-fork/frontmatter-index.ts +6 -1
- package/extensions/git-status/__tests__/git-status.test.ts +65 -2
- package/extensions/git-status/index.ts +268 -98
- package/extensions/minimal-skill-display/index.ts +7 -1
- package/extensions/read-tool-enhanced/index.ts +7 -2
- package/extensions/rewind/__tests__/session-files.test.ts +115 -0
- package/extensions/rewind/__tests__/snapshots.test.ts +23 -0
- package/extensions/rewind/index.ts +5 -0
- package/extensions/rewind/session-files.ts +138 -0
- package/extensions/rewind/snapshots.ts +104 -5
- package/extensions/skill-commands/index.ts +6 -1
- package/extensions/subagent-tool/schema.ts +1 -2
- package/extensions/teams-tool/tools/teammate-tools.ts +1 -2
- package/extensions/wezterm-pane-control/index.ts +1 -2
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/package.json +5 -5
- package/skills/tallow-expert/SKILL.md +2 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { SessionHeader } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { getDefaultTallowHomeDir, getTallowHomeDir } from "../_shared/tallow-paths.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Encode a cwd into the per-project session directory name used by tallow.
|
|
9
|
+
*
|
|
10
|
+
* @param cwd - Absolute working directory path
|
|
11
|
+
* @returns Encoded directory name (for example `--Users-kevin-dev-tallow--`)
|
|
12
|
+
*/
|
|
13
|
+
function encodeSessionDirName(cwd: string): string {
|
|
14
|
+
const withoutLeadingSlash = cwd.startsWith("/") || cwd.startsWith("\\") ? cwd.slice(1) : cwd;
|
|
15
|
+
const safeName = withoutLeadingSlash
|
|
16
|
+
.replaceAll("/", "-")
|
|
17
|
+
.replaceAll("\\", "-")
|
|
18
|
+
.replaceAll(":", "-");
|
|
19
|
+
return `--${safeName}--`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read additional tallow home directories from the maintainer's work-dir config.
|
|
24
|
+
*
|
|
25
|
+
* @returns Extra configured tallow home directories
|
|
26
|
+
*/
|
|
27
|
+
function readConfiguredHomeDirs(): string[] {
|
|
28
|
+
const workDirsPath = join(homedir(), ".config", "tallow-work-dirs");
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(workDirsPath, "utf-8");
|
|
32
|
+
return content
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
36
|
+
.map((line) => {
|
|
37
|
+
const colonIndex = line.indexOf(":");
|
|
38
|
+
return colonIndex === -1 ? "" : line.slice(colonIndex + 1).trim();
|
|
39
|
+
})
|
|
40
|
+
.filter((configDir) => configDir.length > 0);
|
|
41
|
+
} catch {
|
|
42
|
+
// Missing or unreadable work-dir config means there are no extra homes to scan.
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the set of tallow homes that should be searched for session files.
|
|
49
|
+
*
|
|
50
|
+
* @param homeDirs - Optional explicit home directories (used by tests to avoid scanning real homes)
|
|
51
|
+
* @returns Unique tallow home directories to inspect
|
|
52
|
+
*/
|
|
53
|
+
function resolveHomeDirs(homeDirs?: readonly string[]): Set<string> {
|
|
54
|
+
if (homeDirs) {
|
|
55
|
+
return new Set(homeDirs);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Set<string>([
|
|
59
|
+
getDefaultTallowHomeDir(),
|
|
60
|
+
getTallowHomeDir(),
|
|
61
|
+
...readConfiguredHomeDirs(),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Discover every session directory that can contain sessions for a specific cwd.
|
|
67
|
+
*
|
|
68
|
+
* Tallow can store sessions under the default home, the active runtime home,
|
|
69
|
+
* and any per-project homes listed in `~/.config/tallow-work-dirs`.
|
|
70
|
+
*
|
|
71
|
+
* @param cwd - Working directory whose session subdirectory should be resolved
|
|
72
|
+
* @param homeDirs - Optional explicit home directories (used by tests to avoid scanning real homes)
|
|
73
|
+
* @returns Existing session directory paths across all known tallow homes
|
|
74
|
+
*/
|
|
75
|
+
function discoverSessionDirsForCwd(cwd: string, homeDirs?: readonly string[]): string[] {
|
|
76
|
+
const dirName = encodeSessionDirName(cwd);
|
|
77
|
+
const dirs = new Set<string>();
|
|
78
|
+
|
|
79
|
+
for (const home of resolveHomeDirs(homeDirs)) {
|
|
80
|
+
const sessionsDir = join(home, "sessions", dirName);
|
|
81
|
+
if (existsSync(sessionsDir)) {
|
|
82
|
+
dirs.add(sessionsDir);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [...dirs];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read the session id from a JSONL session file.
|
|
91
|
+
*
|
|
92
|
+
* The conventional filename already contains the id, but parsing the header is
|
|
93
|
+
* more robust for renamed or migrated session files.
|
|
94
|
+
*
|
|
95
|
+
* @param filePath - Absolute path to the session JSONL file
|
|
96
|
+
* @returns Session id when readable and valid, otherwise null
|
|
97
|
+
*/
|
|
98
|
+
function readSessionId(filePath: string): string | null {
|
|
99
|
+
try {
|
|
100
|
+
const content = readFileSync(filePath, "utf-8");
|
|
101
|
+
const firstNewline = content.indexOf("\n");
|
|
102
|
+
const headerLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
|
|
103
|
+
const header = JSON.parse(headerLine) as SessionHeader;
|
|
104
|
+
if (header.type !== "session") return null;
|
|
105
|
+
return typeof header.id === "string" && header.id.length > 0 ? header.id : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List every live session id for the current cwd across all known tallow homes.
|
|
113
|
+
*
|
|
114
|
+
* @param cwd - Working directory whose sessions should be considered live
|
|
115
|
+
* @param homeDirs - Optional explicit home directories to scan instead of runtime discovery
|
|
116
|
+
* @returns Set of live session ids
|
|
117
|
+
*/
|
|
118
|
+
export function listLiveSessionIdsForCwd(cwd: string, homeDirs?: readonly string[]): Set<string> {
|
|
119
|
+
const ids = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const sessionsDir of discoverSessionDirsForCwd(cwd, homeDirs)) {
|
|
122
|
+
let files: string[];
|
|
123
|
+
try {
|
|
124
|
+
files = readdirSync(sessionsDir).filter((file) => file.endsWith(".jsonl"));
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const sessionId = readSessionId(join(sessionsDir, file));
|
|
131
|
+
if (sessionId) {
|
|
132
|
+
ids.add(sessionId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
@@ -277,13 +277,112 @@ export class SnapshotManager {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
|
-
*
|
|
280
|
+
* Remove all refs for the current session.
|
|
281
|
+
*
|
|
282
|
+
* @returns Number of refs deleted for this session
|
|
283
|
+
*/
|
|
284
|
+
cleanup(): number {
|
|
285
|
+
return this.cleanupSession(this.getSessionIdFromPrefix(this.refPrefix));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Remove rewind refs whose session ids no longer exist on disk.
|
|
290
|
+
*
|
|
291
|
+
* @param liveSessionIds - Session ids that still have backing session files
|
|
292
|
+
* @returns Count of deleted refs across all stale sessions
|
|
293
|
+
*/
|
|
294
|
+
cleanupStaleSessions(liveSessionIds: ReadonlySet<string>): number {
|
|
295
|
+
let deletedRefs = 0;
|
|
296
|
+
|
|
297
|
+
for (const sessionId of this.listSessionIds()) {
|
|
298
|
+
if (liveSessionIds.has(sessionId)) continue;
|
|
299
|
+
deletedRefs += this.cleanupSession(sessionId);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return deletedRefs;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* List every session id that currently has rewind refs in this repository.
|
|
307
|
+
*
|
|
308
|
+
* @returns Ordered unique session ids extracted from `refs/tallow/rewind/*`
|
|
309
|
+
*/
|
|
310
|
+
private listSessionIds(): string[] {
|
|
311
|
+
const raw = this.git(["for-each-ref", "--format=%(refname)", "refs/tallow/rewind/"]);
|
|
312
|
+
if (!raw) return [];
|
|
313
|
+
|
|
314
|
+
const ids = new Set<string>();
|
|
315
|
+
for (const line of raw.split("\n")) {
|
|
316
|
+
const ref = this.normalizeRefName(line);
|
|
317
|
+
if (!ref) continue;
|
|
318
|
+
const sessionId = this.parseSessionIdFromRef(ref);
|
|
319
|
+
if (sessionId) {
|
|
320
|
+
ids.add(sessionId);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Remove every rewind ref for a single session id.
|
|
329
|
+
*
|
|
330
|
+
* @param sessionId - Session id whose namespaced rewind refs should be deleted
|
|
331
|
+
* @returns Count of deleted refs
|
|
281
332
|
*/
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
333
|
+
private cleanupSession(sessionId: string): number {
|
|
334
|
+
const raw = this.git([
|
|
335
|
+
"for-each-ref",
|
|
336
|
+
"--format=%(refname)",
|
|
337
|
+
`refs/tallow/rewind/${sessionId}/`,
|
|
338
|
+
]);
|
|
339
|
+
if (!raw) return 0;
|
|
340
|
+
|
|
341
|
+
let deletedRefs = 0;
|
|
342
|
+
for (const line of raw.split("\n")) {
|
|
343
|
+
const ref = this.normalizeRefName(line);
|
|
344
|
+
if (!ref) continue;
|
|
345
|
+
if (this.git(["update-ref", "-d", ref]) !== null) {
|
|
346
|
+
deletedRefs++;
|
|
347
|
+
}
|
|
286
348
|
}
|
|
349
|
+
|
|
350
|
+
return deletedRefs;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse a session id from a full rewind ref name.
|
|
355
|
+
*
|
|
356
|
+
* @param ref - Full rewind ref name
|
|
357
|
+
* @returns Session id when the ref matches the rewind namespace, otherwise null
|
|
358
|
+
*/
|
|
359
|
+
private parseSessionIdFromRef(ref: string): string | null {
|
|
360
|
+
const match = /^refs\/tallow\/rewind\/([^/]+)\/turn-\d+$/.exec(ref);
|
|
361
|
+
return match?.[1] ?? null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Normalize a raw ref name line from git output.
|
|
366
|
+
*
|
|
367
|
+
* @param line - Raw line returned by `git for-each-ref`
|
|
368
|
+
* @returns Trimmed ref name without surrounding quotes
|
|
369
|
+
*/
|
|
370
|
+
private normalizeRefName(line: string): string {
|
|
371
|
+
const trimmed = line.trim();
|
|
372
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
373
|
+
return trimmed.slice(1, -1);
|
|
374
|
+
}
|
|
375
|
+
return trimmed;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Extract the current manager's session id from its ref prefix.
|
|
380
|
+
*
|
|
381
|
+
* @param refPrefix - Session-scoped rewind ref prefix
|
|
382
|
+
* @returns Session id portion of the prefix
|
|
383
|
+
*/
|
|
384
|
+
private getSessionIdFromPrefix(refPrefix: string): string {
|
|
385
|
+
return refPrefix.replace(/^refs\/tallow\/rewind\//, "");
|
|
287
386
|
}
|
|
288
387
|
|
|
289
388
|
/**
|
|
@@ -199,7 +199,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
199
|
// Load skills synchronously during extension init for autocomplete to work.
|
|
200
200
|
// includeDefaults: true picks up ~/.tallow/skills/ and ./skills/ (project).
|
|
201
201
|
// extraSkillPaths adds shared dirs + Claude bridge paths.
|
|
202
|
-
const { skills } = loadSkills({
|
|
202
|
+
const { skills } = loadSkills({
|
|
203
|
+
cwd: process.cwd(),
|
|
204
|
+
agentDir,
|
|
205
|
+
skillPaths: extraSkillPaths,
|
|
206
|
+
includeDefaults: true,
|
|
207
|
+
});
|
|
203
208
|
|
|
204
209
|
for (const skill of skills) {
|
|
205
210
|
// Validate name before registration — invalid names produce broken commands
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* TypeBox parameter schemas and event type definitions for the subagent tool.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { StringEnum } from "@mariozechner/pi-ai";
|
|
6
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { StringEnum, Type } from "@mariozechner/pi-ai";
|
|
7
6
|
|
|
8
7
|
// ── Parameter Schemas ────────────────────────────────────────────────────────
|
|
9
8
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Teammate tool definitions — coordination tools injected into each teammate session.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { StringEnum } from "@mariozechner/pi-ai";
|
|
5
|
+
import { StringEnum, Type } from "@mariozechner/pi-ai";
|
|
6
6
|
import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
import { Type } from "@sinclair/typebox";
|
|
8
7
|
import { getIcon } from "../../_icons/index.js";
|
|
9
8
|
import { appendDashboardFeedEvent, refreshTeamView } from "../dashboard/state.js";
|
|
10
9
|
import { autoDispatch, wakeTeammate } from "../dispatch/auto-dispatch.js";
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { StringEnum } from "@mariozechner/pi-ai";
|
|
12
|
+
import { StringEnum, Type } from "@mariozechner/pi-ai";
|
|
13
13
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { Type } from "@sinclair/typebox";
|
|
15
14
|
|
|
16
15
|
export type WeztermAction =
|
|
17
16
|
| "list"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungle-scrubs/tallow",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "An opinionated coding agent. Built on pi.",
|
|
5
5
|
"piConfig": {
|
|
6
6
|
"name": "tallow",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@clack/prompts": "^1.2.0",
|
|
77
77
|
"@dungle-scrubs/synapse": "0.1.8",
|
|
78
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
79
|
-
"@mariozechner/pi-tui": "^0.
|
|
78
|
+
"@mariozechner/pi-coding-agent": "^0.70.0",
|
|
79
|
+
"@mariozechner/pi-tui": "^0.70.0",
|
|
80
80
|
"@opentelemetry/api": "^1.9.1",
|
|
81
81
|
"@sinclair/typebox": "0.34.49",
|
|
82
82
|
"ai": "^6.0.162",
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@biomejs/biome": "2.4.12",
|
|
90
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
91
|
-
"@mariozechner/pi-ai": "^0.
|
|
90
|
+
"@mariozechner/pi-agent-core": "^0.70.0",
|
|
91
|
+
"@mariozechner/pi-ai": "^0.70.0",
|
|
92
92
|
"@types/node": "25.6.0",
|
|
93
93
|
"husky": "^9.1.7",
|
|
94
94
|
"lint-staged": "^16.4.0",
|
|
@@ -163,9 +163,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
|
|
|
163
163
|
### ExtensionCommandContext (`ctx` in command handlers, extends ExtensionContext)
|
|
164
164
|
|
|
165
165
|
- `waitForIdle()` — Wait for the agent to finish streaming
|
|
166
|
-
- `fork(entryId: string)` — Fork from a specific entry, creating a new session file.
|
|
167
166
|
- `navigateTree(targetId: string, options?: object)` — Navigate to a different point in the session tree.
|
|
168
|
-
- `switchSession(sessionPath: string)` — Switch to a different session file.
|
|
169
167
|
- `reload()` — Reload extensions, skills, prompts, and themes.
|
|
170
168
|
|
|
171
169
|
### ExtensionUIContext (`ctx.ui`)
|
|
@@ -177,6 +175,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
|
|
|
177
175
|
- `onTerminalInput(handler: TerminalInputHandler)` — Listen to raw terminal input (interactive mode only).
|
|
178
176
|
- `setStatus(key: string, text: string)` — Set status text in the footer/status bar.
|
|
179
177
|
- `setWorkingMessage(message?: string)` — Set the working/loading message shown during streaming.
|
|
178
|
+
- `setWorkingIndicator(options?: WorkingIndicatorOptions)` — Configure the interactive working indicator shown during streaming.
|
|
180
179
|
- `setHiddenThinkingLabel(label?: string)` — Set the label shown for hidden thinking blocks.
|
|
181
180
|
- `setWidget(key: string, content: string[], options?: ExtensionWidgetOptions)` — Set a widget to display above or below the editor.
|
|
182
181
|
- `setTitle(title: string)` — Set the terminal window/tab title.
|
|
@@ -184,6 +183,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
|
|
|
184
183
|
- `setEditorText(text: string)` — Set the text in the core input editor.
|
|
185
184
|
- `getEditorText()` — Get the current text from the core input editor.
|
|
186
185
|
- `editor(title: string, prefill?: string)` — Show a multi-line editor for text editing.
|
|
186
|
+
- `addAutocompleteProvider(factory: AutocompleteProviderFactory)` — Stack additional autocomplete behavior on top of the built-in provider.
|
|
187
187
|
- `readonly theme` — Get the current theme for styling.
|
|
188
188
|
- `getAllThemes()` — Get all available themes with their names and file paths.
|
|
189
189
|
- `getTheme(name: string)` — Load a theme by name without switching to it.
|