pi-chalin 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.
@@ -0,0 +1,108 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { type ExtensionContext, SessionManager } from "@earendil-works/pi-coding-agent";
4
+ import { resolveChalinPaths } from "./paths.ts";
5
+ import type { RunStepState } from "./schemas.ts";
6
+
7
+ const CHILD_SESSION_SCOPE = "pi-chalin";
8
+ const LEGACY_CHILD_SESSION_ARCHIVE = ".pi-chalin-hidden-child-sessions";
9
+
10
+ export interface ChildSessionDirOptions {
11
+ cwd: string;
12
+ runId: string;
13
+ stepId: string;
14
+ agent: string;
15
+ parentSessionFile?: string;
16
+ }
17
+
18
+ export interface ChildSessionManagerOptions {
19
+ cwd: string;
20
+ runId: string;
21
+ step: RunStepState;
22
+ extensionContext?: Pick<ExtensionContext, "sessionManager">;
23
+ }
24
+
25
+ export interface LegacyChildSessionCleanupResult {
26
+ moved: string[];
27
+ failed: string[];
28
+ }
29
+
30
+ export function chalinChildSessionRoot(options: Pick<ChildSessionDirOptions, "cwd" | "runId" | "parentSessionFile">): string {
31
+ if (options.parentSessionFile) {
32
+ const parentBaseName = path.basename(options.parentSessionFile, ".jsonl");
33
+ return path.join(path.dirname(options.parentSessionFile), parentBaseName, CHILD_SESSION_SCOPE, safePathSegment(options.runId));
34
+ }
35
+
36
+ return path.join(resolveChalinPaths({ cwd: options.cwd }).projectRoot, ".pi-chalin", "child-sessions", safePathSegment(options.runId));
37
+ }
38
+
39
+ export function chalinChildSessionDir(options: ChildSessionDirOptions): string {
40
+ return path.join(
41
+ chalinChildSessionRoot(options),
42
+ `${safePathSegment(options.stepId)}-${safePathSegment(options.agent)}`,
43
+ );
44
+ }
45
+
46
+ export function createChalinChildSessionManager(options: ChildSessionManagerOptions): SessionManager {
47
+ const parentSessionFile = options.extensionContext?.sessionManager.getSessionFile();
48
+ const sessionDir = chalinChildSessionDir({
49
+ cwd: options.cwd,
50
+ runId: options.runId,
51
+ stepId: options.step.id,
52
+ agent: options.step.agent,
53
+ parentSessionFile,
54
+ });
55
+ fs.mkdirSync(sessionDir, { recursive: true });
56
+
57
+ const manager = SessionManager.create(options.cwd, sessionDir);
58
+ if (parentSessionFile) manager.newSession({ parentSession: parentSessionFile });
59
+ return manager;
60
+ }
61
+
62
+ export async function hideLegacyTopLevelChildSessions(ctx: Pick<ExtensionContext, "sessionManager">): Promise<LegacyChildSessionCleanupResult> {
63
+ const sessionDir = ctx.sessionManager.getSessionDir();
64
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
65
+ const sessions = await SessionManager.list(ctx.sessionManager.getCwd(), sessionDir);
66
+ const result: LegacyChildSessionCleanupResult = { moved: [], failed: [] };
67
+
68
+ for (const session of sessions) {
69
+ if (session.path === currentSessionFile) continue;
70
+ if (!isPiChalinChildSessionPreview(session.firstMessage)) continue;
71
+
72
+ const destination = hiddenLegacyChildSessionPath(sessionDir, session.path);
73
+ try {
74
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
75
+ fs.renameSync(session.path, destination);
76
+ result.moved.push(destination);
77
+ } catch {
78
+ result.failed.push(session.path);
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ export function isPiChalinChildSessionPreview(firstMessage: string): boolean {
86
+ return /^You are pi-chalin [a-zA-Z0-9._-]+:/.test(firstMessage.trimStart());
87
+ }
88
+
89
+ function safePathSegment(value: string): string {
90
+ const normalized = value
91
+ .trim()
92
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
93
+ .replace(/^-+|-+$/g, "")
94
+ .slice(0, 80);
95
+ return normalized || "session";
96
+ }
97
+
98
+ function hiddenLegacyChildSessionPath(sessionDir: string, sourceFile: string): string {
99
+ const sourceBaseName = path.basename(sourceFile, ".jsonl");
100
+ const archiveDir = path.join(sessionDir, LEGACY_CHILD_SESSION_ARCHIVE, safePathSegment(sourceBaseName));
101
+ let candidate = path.join(archiveDir, path.basename(sourceFile));
102
+ let suffix = 1;
103
+ while (fs.existsSync(candidate)) {
104
+ candidate = path.join(archiveDir, `${sourceBaseName}-${suffix}.jsonl`);
105
+ suffix += 1;
106
+ }
107
+ return candidate;
108
+ }