@nredd/pi-session-kit 0.0.0-stage → 0.4.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/LICENSE +18 -0
- package/README.md +191 -2
- package/dist/pi-stats-export.mjs +657 -0
- package/docs/stats-snapshots.md +122 -0
- package/package.json +55 -4
- package/src/child.ts +79 -0
- package/src/clock.ts +25 -0
- package/src/config.ts +64 -0
- package/src/diff/component.ts +214 -0
- package/src/diff/git.ts +289 -0
- package/src/diff/index.ts +65 -0
- package/src/diff/parse.ts +180 -0
- package/src/diff/view.ts +98 -0
- package/src/goal/index.ts +343 -0
- package/src/goal/state.ts +270 -0
- package/src/goal/subagents.ts +77 -0
- package/src/index.ts +52 -0
- package/src/loop/index.ts +151 -0
- package/src/loop/state.ts +107 -0
- package/src/monitor/README.md +12 -0
- package/src/monitor/coalescer.ts +76 -0
- package/src/monitor/index.ts +220 -0
- package/src/monitor/manager.ts +336 -0
- package/src/monitor/matcher.ts +113 -0
- package/src/monitor/messages.ts +93 -0
- package/src/monitor/output.ts +41 -0
- package/src/monitor/pidfiles.ts +415 -0
- package/src/monitor/process.ts +125 -0
- package/src/monitor/ring.ts +52 -0
- package/src/monitor/splitter.ts +57 -0
- package/src/monitor/types.ts +35 -0
- package/src/monitor/validate.ts +59 -0
- package/src/monitor/widget.ts +75 -0
- package/src/output.ts +25 -0
- package/src/rules/index.ts +94 -0
- package/src/rules/logic.ts +96 -0
- package/src/shared.ts +177 -0
- package/src/stats/aggregate.ts +287 -0
- package/src/stats/chart.ts +247 -0
- package/src/stats/cli.ts +50 -0
- package/src/stats/component.ts +496 -0
- package/src/stats/days.ts +46 -0
- package/src/stats/format.ts +49 -0
- package/src/stats/heatmap.ts +98 -0
- package/src/stats/index.ts +88 -0
- package/src/stats/library.ts +4 -0
- package/src/stats/load.ts +512 -0
- package/src/stats/palette.ts +22 -0
- package/src/stats/snapshot.ts +497 -0
- package/src/titles/aliases.ts +20 -0
- package/src/titles/index.ts +87 -0
- package/src/titles/logic.ts +160 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** Pure logic for auto-titles: model order, prompt building and output sanitising. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TITLE_MODELS = ["anthropic/claude-haiku-5-5", "openai-codex/gpt-6-luna"] as const;
|
|
4
|
+
export const MAX_USER_CHARS = 2000;
|
|
5
|
+
export const MAX_ASSISTANT_CHARS = 1000;
|
|
6
|
+
export const MAX_TITLE_CHARS = 60;
|
|
7
|
+
export const MAX_TITLE_WORDS = 6;
|
|
8
|
+
|
|
9
|
+
export const TITLE_SYSTEM_PROMPT =
|
|
10
|
+
"You name coding-agent sessions. Reply with ONLY a title of 3 to 6 words that says what the session is about. No quotes, no punctuation, no preamble.";
|
|
11
|
+
|
|
12
|
+
export interface ModelRef {
|
|
13
|
+
provider: string;
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Parse `provider/id`; the id may itself contain slashes. */
|
|
18
|
+
export function parseModelRef(ref: string): ModelRef | undefined {
|
|
19
|
+
const slash = ref.indexOf("/");
|
|
20
|
+
if (slash <= 0 || slash === ref.length - 1) return undefined;
|
|
21
|
+
return { provider: ref.slice(0, slash), id: ref.slice(slash + 1) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Logical model name -> `provider/id` candidates, lower-cased keys. */
|
|
25
|
+
export type ModelAliases = Readonly<Record<string, readonly string[]>>;
|
|
26
|
+
|
|
27
|
+
export const ALIAS_MANIFEST_VERSION = 1;
|
|
28
|
+
/** Logical alias names tried after the pinned default, in order. */
|
|
29
|
+
export const TITLE_ALIAS_NAMES = ["claude-haiku-5-5", "claude-haiku-4-5"] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract `subagents.modelAliases` from the machine-local manifest text. Mirrors pi-subagents:
|
|
33
|
+
* rejects group/other-accessible files, a non-object or wrong-version manifest, and ignores
|
|
34
|
+
* invalid alias entries. Silent on any problem; no other manifest field is read.
|
|
35
|
+
*/
|
|
36
|
+
export function parseModelAliases(text: string, mode: number): ModelAliases {
|
|
37
|
+
if ((mode & 0o077) !== 0) return {};
|
|
38
|
+
let raw: unknown;
|
|
39
|
+
try {
|
|
40
|
+
raw = JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
if (!isRecord(raw) || raw.version !== ALIAS_MANIFEST_VERSION) return {};
|
|
45
|
+
const subagents = raw.subagents;
|
|
46
|
+
if (subagents === undefined || !isRecord(subagents)) return {};
|
|
47
|
+
const rawAliases = subagents.modelAliases;
|
|
48
|
+
if (!isRecord(rawAliases)) return {};
|
|
49
|
+
const aliases: Record<string, string[]> = {};
|
|
50
|
+
for (const [alias, rawCandidates] of Object.entries(rawAliases)) {
|
|
51
|
+
const name = alias.trim().toLowerCase();
|
|
52
|
+
const candidates = Array.isArray(rawCandidates)
|
|
53
|
+
? rawCandidates
|
|
54
|
+
.filter((c): c is string => typeof c === "string")
|
|
55
|
+
.map((c) => c.trim())
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
: [];
|
|
58
|
+
if (!name || name.includes("/") || name in aliases || candidates.length === 0) continue;
|
|
59
|
+
if (candidates.some((c) => parseModelRef(c) === undefined)) continue;
|
|
60
|
+
aliases[name] = [...new Set(candidates)];
|
|
61
|
+
}
|
|
62
|
+
return aliases;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Candidate models, best first: the configured one, the pinned default, the alias candidates for
|
|
71
|
+
* each logical name, then the fallback default. Deduplicated.
|
|
72
|
+
*/
|
|
73
|
+
export function modelCandidates(titleModel?: string, aliases: ModelAliases = {}): ModelRef[] {
|
|
74
|
+
const [primary, ...rest] = DEFAULT_TITLE_MODELS;
|
|
75
|
+
const refs = [titleModel, primary, ...TITLE_ALIAS_NAMES.flatMap((name) => aliases[name] ?? []), ...rest];
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
const out: ModelRef[] = [];
|
|
78
|
+
for (const ref of refs) {
|
|
79
|
+
const parsed = ref === undefined ? undefined : parseModelRef(ref);
|
|
80
|
+
if (!parsed) continue;
|
|
81
|
+
const key = `${parsed.provider}/${parsed.id}`;
|
|
82
|
+
if (seen.has(key)) continue;
|
|
83
|
+
seen.add(key);
|
|
84
|
+
out.push(parsed);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Flatten message content (string or content blocks) to its text blocks. */
|
|
90
|
+
export function contentText(content: unknown): string {
|
|
91
|
+
if (typeof content === "string") return content;
|
|
92
|
+
if (!Array.isArray(content)) return "";
|
|
93
|
+
return content
|
|
94
|
+
.map((block) =>
|
|
95
|
+
typeof block === "object" && block !== null && (block as { type?: unknown }).type === "text"
|
|
96
|
+
? String((block as { text?: unknown }).text ?? "")
|
|
97
|
+
: "",
|
|
98
|
+
)
|
|
99
|
+
.join("");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FirstExchange {
|
|
103
|
+
user: string;
|
|
104
|
+
assistant: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** First user message and first assistant reply on a branch of session entries. */
|
|
108
|
+
export function firstExchange(entries: readonly unknown[]): FirstExchange | undefined {
|
|
109
|
+
let user: string | undefined;
|
|
110
|
+
let assistant: string | undefined;
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const message = (entry as { type?: string; message?: { role?: string; content?: unknown } }).message;
|
|
113
|
+
if ((entry as { type?: string }).type !== "message" || !message) continue;
|
|
114
|
+
if (user === undefined && message.role === "user") user = contentText(message.content);
|
|
115
|
+
else if (user !== undefined && assistant === undefined && message.role === "assistant") {
|
|
116
|
+
assistant = contentText(message.content);
|
|
117
|
+
}
|
|
118
|
+
if (user !== undefined && assistant !== undefined) break;
|
|
119
|
+
}
|
|
120
|
+
if (!user?.trim()) return undefined;
|
|
121
|
+
return { user, assistant: assistant ?? "" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildTitlePrompt(exchange: FirstExchange): string {
|
|
125
|
+
const parts = [`User message:\n${exchange.user.slice(0, MAX_USER_CHARS)}`];
|
|
126
|
+
if (exchange.assistant.trim()) {
|
|
127
|
+
parts.push(`Assistant reply (start):\n${exchange.assistant.slice(0, MAX_ASSISTANT_CHARS)}`);
|
|
128
|
+
}
|
|
129
|
+
parts.push("Title (3-6 words, no quotes, no punctuation):");
|
|
130
|
+
return parts.join("\n\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Strip quotes, newlines and trailing punctuation; cap at 6 words and 60 chars on a word boundary. */
|
|
134
|
+
export function sanitizeTitle(raw: string): string | undefined {
|
|
135
|
+
const firstLine = raw.split(/\r?\n/).find((line) => line.trim() !== "") ?? "";
|
|
136
|
+
let title = firstLine
|
|
137
|
+
.replace(/^\s*(title|name)\s*:\s*/i, "")
|
|
138
|
+
.replace(/["'`\u201c\u201d\u2018\u2019]/g, "")
|
|
139
|
+
.replace(/[*_#]+/g, "")
|
|
140
|
+
.replace(/\s+/g, " ")
|
|
141
|
+
.trim()
|
|
142
|
+
.replace(/[.,;:!?\-\u2013\u2014]+$/, "")
|
|
143
|
+
.trim();
|
|
144
|
+
title = title.split(" ").slice(0, MAX_TITLE_WORDS).join(" ");
|
|
145
|
+
if (title.length > MAX_TITLE_CHARS) {
|
|
146
|
+
const cut = title.slice(0, MAX_TITLE_CHARS);
|
|
147
|
+
const space = cut.lastIndexOf(" ");
|
|
148
|
+
title = (space > 0 ? cut.slice(0, space) : cut).trim();
|
|
149
|
+
}
|
|
150
|
+
return title === "" ? undefined : title;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** True when the newest `plan-mode-state` entry on the branch says planning is active. */
|
|
154
|
+
export function planModeActive(entries: readonly unknown[]): boolean {
|
|
155
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
156
|
+
const entry = entries[i] as { type?: string; customType?: string; data?: { enabled?: unknown } };
|
|
157
|
+
if (entry?.type === "custom" && entry.customType === "plan-mode-state") return entry.data?.enabled === true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|