@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.
Files changed (32) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/process-cleanup.js +1 -1
  6. package/dist/process-cleanup.js.map +1 -1
  7. package/dist/sdk.d.ts +2 -2
  8. package/dist/sdk.d.ts.map +1 -1
  9. package/dist/sdk.js +32 -26
  10. package/dist/sdk.js.map +1 -1
  11. package/dist/workspace-transition-interactive.js +1 -1
  12. package/dist/workspace-transition-interactive.js.map +1 -1
  13. package/extensions/_shared/__tests__/shell-policy.test.ts +19 -0
  14. package/extensions/_shared/shell-policy.ts +121 -1
  15. package/extensions/command-expansion/index.ts +8 -2
  16. package/extensions/context-fork/frontmatter-index.ts +6 -1
  17. package/extensions/git-status/__tests__/git-status.test.ts +65 -2
  18. package/extensions/git-status/index.ts +268 -98
  19. package/extensions/minimal-skill-display/index.ts +7 -1
  20. package/extensions/read-tool-enhanced/index.ts +7 -2
  21. package/extensions/rewind/__tests__/session-files.test.ts +115 -0
  22. package/extensions/rewind/__tests__/snapshots.test.ts +23 -0
  23. package/extensions/rewind/index.ts +5 -0
  24. package/extensions/rewind/session-files.ts +138 -0
  25. package/extensions/rewind/snapshots.ts +104 -5
  26. package/extensions/skill-commands/index.ts +6 -1
  27. package/extensions/subagent-tool/schema.ts +1 -2
  28. package/extensions/teams-tool/tools/teammate-tools.ts +1 -2
  29. package/extensions/wezterm-pane-control/index.ts +1 -2
  30. package/node_modules/@mariozechner/pi-tui/package.json +1 -1
  31. package/package.json +5 -5
  32. 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
- * Removes all refs for the current session.
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
- cleanup(): void {
283
- const snapshots = this.listSnapshots();
284
- for (const snap of snapshots) {
285
- this.git(["update-ref", "-d", snap.ref]);
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({ agentDir, skillPaths: extraSkillPaths });
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-tui",
3
- "version": "0.67.3",
3
+ "version": "0.70.0",
4
4
  "private": true,
5
5
  "description": "Tallow's TUI fork — customizable loader, rounded borders, which-key overlay",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungle-scrubs/tallow",
3
- "version": "0.9.6",
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.67.3",
79
- "@mariozechner/pi-tui": "^0.67.3",
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.67.3",
91
- "@mariozechner/pi-ai": "^0.67.3",
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.