@groeponline/pi-wishcraft 0.17.3

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 (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,157 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { ansi, fgOnly, getFgAnsiCode } from "../theme/colors.ts";
3
+ import { centerText, fitToWidth, getBoxLayout } from "./layout.ts";
4
+ import type { WelcomeData, WelcomeWidget, WidgetRenderContext } from "./types.ts";
5
+
6
+ import { QueueWidget } from "./widgets/queue-widget.ts";
7
+ import { SessionsWidget } from "./widgets/sessions-widget.ts";
8
+ import { ShortcutsWidget } from "./widgets/shortcuts-widget.ts";
9
+ import { SystemWidget } from "./widgets/system-widget.ts";
10
+
11
+ const PI_LOGO = [
12
+ " . * ",
13
+ " * ╭───╮ . ",
14
+ " . │ │ * ",
15
+ " │ │ ",
16
+ " * ╰─┬─╯ . ",
17
+ " . ┴ ",
18
+ ];
19
+
20
+ const GRADIENT_COLORS = [
21
+ "\x1b[38;5;199m",
22
+ "\x1b[38;5;171m",
23
+ "\x1b[38;5;135m",
24
+ "\x1b[38;5;99m",
25
+ "\x1b[38;5;75m",
26
+ "\x1b[38;5;51m",
27
+ ];
28
+
29
+ function bold(text: string): string {
30
+ return `\x1b[1m${text}\x1b[22m`;
31
+ }
32
+
33
+ export function dim(text: string): string {
34
+ return getFgAnsiCode("sep") + text + ansi.reset;
35
+ }
36
+
37
+ function gradientLine(line: string): string {
38
+ const reset = ansi.reset;
39
+ let result = "";
40
+ let colorIdx = 0;
41
+ const step = Math.max(1, Math.floor(line.length / GRADIENT_COLORS.length));
42
+
43
+ for (let i = 0; i < line.length; i++) {
44
+ if (i > 0 && i % step === 0 && colorIdx < GRADIENT_COLORS.length - 1)
45
+ colorIdx++;
46
+ const char = line[i];
47
+ if (char !== " ") {
48
+ result += GRADIENT_COLORS[colorIdx] + char + reset;
49
+ } else {
50
+ result += char;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function buildLeftColumn(ctx: WidgetRenderContext): string[] {
57
+ const logoColored = PI_LOGO.map((line) => gradientLine(line));
58
+
59
+ return [
60
+ "",
61
+ ...logoColored.map((l) => centerText(l, ctx.width)),
62
+ "",
63
+ centerText(fgOnly("model", ctx.data.modelName), ctx.width),
64
+ centerText(dim(ctx.data.providerName), ctx.width),
65
+ ];
66
+ }
67
+
68
+ function buildRightColumn(
69
+ ctx: WidgetRenderContext,
70
+ widgets: WelcomeWidget[]
71
+ ): string[] {
72
+ const hChar = "─";
73
+ const separator = ` ${dim(hChar.repeat(Math.max(1, ctx.width - 2)))}`;
74
+ const lines: string[] = [];
75
+
76
+ lines.push(` ${bold(fgOnly("accent", "Signals & Wishes"))}`);
77
+ lines.push(` ${dim("Write it down, let it rise, keep your focus clear.")}`);
78
+ lines.push(separator);
79
+
80
+ for (let i = 0; i < widgets.length; i++) {
81
+ const wLines = widgets[i].render(ctx);
82
+ lines.push(...wLines);
83
+ if (i < widgets.length - 1) {
84
+ lines.push(separator);
85
+ }
86
+ }
87
+
88
+ lines.push("");
89
+ return lines;
90
+ }
91
+
92
+ export function renderWelcomeBox(
93
+ data: WelcomeData,
94
+ termWidth: number,
95
+ bottomLine: string,
96
+ ): string[] {
97
+ const layout = getBoxLayout(termWidth);
98
+ if (!layout) {
99
+ return [];
100
+ }
101
+
102
+ const { boxWidth, leftCol, rightCol } = layout;
103
+
104
+ const hChar = "─";
105
+ const v = dim("│");
106
+ const tl = dim("╭");
107
+ const tr = dim("╮");
108
+ const bl = dim("╰");
109
+ const br = dim("╯");
110
+
111
+ const rightWidgets = [
112
+ SystemWidget,
113
+ QueueWidget,
114
+ ShortcutsWidget,
115
+ SessionsWidget,
116
+ ];
117
+
118
+ const leftCtx: WidgetRenderContext = {
119
+ data,
120
+ width: leftCol,
121
+ dim,
122
+ bold,
123
+ color: fgOnly,
124
+ };
125
+
126
+ const rightCtx: WidgetRenderContext = {
127
+ data,
128
+ width: rightCol,
129
+ dim,
130
+ bold,
131
+ color: fgOnly,
132
+ };
133
+
134
+ const leftLines = buildLeftColumn(leftCtx);
135
+ const rightLines = buildRightColumn(rightCtx, rightWidgets);
136
+
137
+ const lines: string[] = [];
138
+
139
+ const title = " pi-wishcraft ";
140
+ const titlePrefix = dim(hChar.repeat(3));
141
+ const titleStyled = titlePrefix + fgOnly("model", title);
142
+ const titleVisLen = 3 + visibleWidth(title);
143
+ const afterTitle = boxWidth - 2 - titleVisLen;
144
+ const afterTitleText = afterTitle > 0 ? dim(hChar.repeat(afterTitle)) : "";
145
+ lines.push(tl + titleStyled + afterTitleText + tr);
146
+
147
+ const maxRows = Math.max(leftLines.length, rightLines.length);
148
+ for (let i = 0; i < maxRows; i++) {
149
+ const left = fitToWidth(leftLines[i] ?? "", leftCol);
150
+ const right = fitToWidth(rightLines[i] ?? "", rightCol);
151
+ lines.push(v + left + v + right + v);
152
+ }
153
+
154
+ lines.push(bl + bottomLine + br);
155
+
156
+ return lines;
157
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ import { getAgentSessionDirs } from "../paths/agent-dirs.ts";
4
+ import { logDiscoveryError } from "./discover.ts";
5
+ import { formatTimeAgo } from "./format.ts";
6
+ import type { RecentSession } from "./types.ts";
7
+
8
+ const MAX_HEADER_SIZE = 8192;
9
+
10
+ interface SessionRecord {
11
+ name: string;
12
+ mtime: number;
13
+ }
14
+
15
+ function parseProjectNameFromHeader(filePath: string): string | null {
16
+ try {
17
+ const buffer = readFileSync(filePath);
18
+ const bytesToRead = Math.min(buffer.length, MAX_HEADER_SIZE);
19
+ const firstLine = buffer
20
+ .toString("utf8", 0, bytesToRead)
21
+ .split(/\r?\n/, 1)[0]
22
+ ?.trim();
23
+
24
+ if (!firstLine) return null;
25
+
26
+ const header: unknown = JSON.parse(firstLine);
27
+
28
+ if (typeof header !== "object" || header === null) return null;
29
+ if (!("cwd" in header)) return null;
30
+
31
+ const cwd = (header as { cwd: unknown }).cwd;
32
+ if (typeof cwd !== "string" || cwd.trim().length === 0) return null;
33
+
34
+ return basename(cwd) || cwd;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function extractProjectNameFromDir(dir: string): string {
41
+ const parentName = basename(dir);
42
+ if (!parentName.startsWith("--")) {
43
+ return parentName;
44
+ }
45
+
46
+ const parts = parentName.split("-").filter(Boolean);
47
+ return parts[parts.length - 1] || parentName;
48
+ }
49
+
50
+ function scanSessionDirectory(dir: string, sessions: SessionRecord[]): void {
51
+ if (!existsSync(dir)) return;
52
+
53
+ try {
54
+ const entries = readdirSync(dir);
55
+ for (const entry of entries) {
56
+ const entryPath = join(dir, entry);
57
+ try {
58
+ const stats = statSync(entryPath);
59
+ if (stats.isDirectory()) {
60
+ scanSessionDirectory(entryPath, sessions);
61
+ } else if (entry.endsWith(".jsonl")) {
62
+ const projectName =
63
+ parseProjectNameFromHeader(entryPath) ??
64
+ extractProjectNameFromDir(dir);
65
+ sessions.push({ name: projectName, mtime: stats.mtimeMs });
66
+ }
67
+ } catch (error) {
68
+ logDiscoveryError(`Failed to inspect session entry ${entryPath}`, error);
69
+ }
70
+ }
71
+ } catch (error) {
72
+ logDiscoveryError(`Failed to scan sessions dir ${dir}`, error);
73
+ }
74
+ }
75
+
76
+ function filterAndFormatSessions(sessions: SessionRecord[], maxCount: number): RecentSession[] {
77
+ if (sessions.length === 0) return [];
78
+
79
+ sessions.sort((a, b) => b.mtime - a.mtime);
80
+
81
+ const seen = new Set<string>();
82
+ const uniqueSessions: SessionRecord[] = [];
83
+
84
+ for (const s of sessions) {
85
+ if (!seen.has(s.name)) {
86
+ seen.add(s.name);
87
+ uniqueSessions.push(s);
88
+ }
89
+ }
90
+
91
+ const now = Date.now();
92
+ return uniqueSessions.slice(0, maxCount).map((s) => ({
93
+ name: s.name.length > 20 ? s.name.slice(0, 17) + "…" : s.name,
94
+ timeAgo: formatTimeAgo(now - s.mtime),
95
+ }));
96
+ }
97
+
98
+ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
99
+ const sessionsDirs = getAgentSessionDirs();
100
+ const sessions: SessionRecord[] = [];
101
+
102
+ for (const dir of sessionsDirs) {
103
+ scanSessionDirectory(dir, sessions);
104
+ }
105
+
106
+ return filterAndFormatSessions(sessions, maxCount);
107
+ }
@@ -0,0 +1,41 @@
1
+ export interface LoadedCounts {
2
+ contextFiles: number;
3
+ extensions: number;
4
+ skills: number;
5
+ promptTemplates: number;
6
+ }
7
+
8
+ export interface RecentSession {
9
+ name: string;
10
+ timeAgo: string;
11
+ }
12
+
13
+ export interface WelcomeData {
14
+ modelName: string;
15
+ providerName: string;
16
+ recentSessions: RecentSession[];
17
+ loadedCounts: LoadedCounts;
18
+ initialContextTokens: number | null;
19
+ queueCount?: number;
20
+ hasStash?: boolean;
21
+ }
22
+
23
+ export interface WelcomeWidget {
24
+ id: string;
25
+ render(ctx: WidgetRenderContext): string[];
26
+ }
27
+
28
+ export interface WidgetRenderContext {
29
+ data: WelcomeData;
30
+ width: number;
31
+ dim: (text: string) => string;
32
+ bold: (text: string) => string;
33
+ color: (semantic: string, text: string) => string;
34
+ }
35
+
36
+ export interface WelcomeLayoutConfig {
37
+ width: number;
38
+ padding: number;
39
+ columnGap: number;
40
+ breakpoint: number;
41
+ }
@@ -0,0 +1,25 @@
1
+ import type { WelcomeWidget, WidgetRenderContext } from "../types.ts";
2
+ import { renderSparkline } from "../../tools/graph.ts";
3
+
4
+ /**
5
+ * Graph widget that renders a mini token sparkline and telemetry activity.
6
+ */
7
+ export const graphWidget: WelcomeWidget = {
8
+ id: "graph",
9
+ render(ctx: WidgetRenderContext): string[] {
10
+ const lines: string[] = [];
11
+ const hChar = "─";
12
+ const separator = ` ${ctx.dim(hChar.repeat(Math.max(1, ctx.width - 2)))}`;
13
+
14
+ lines.push(` ${ctx.bold(ctx.color("accent", "Telemetry Sparkline"))}`);
15
+
16
+ // Sample usage trend points
17
+ const sampleData = [1200, 2400, 3100, 2800, 4500, 6200, 8900, 11400, 9800, 12400];
18
+ const sparkline = renderSparkline(sampleData, 12);
19
+
20
+ lines.push(` ${ctx.dim("activity:")} ${ctx.color("gitClean", sparkline)} ${ctx.dim("12.4k tok")}`);
21
+ lines.push(separator);
22
+
23
+ return lines;
24
+ },
25
+ };
@@ -0,0 +1,20 @@
1
+ import type { WelcomeWidget } from "../types.ts";
2
+ import { SystemWidget } from "./system-widget.ts";
3
+ import { QueueWidget } from "./queue-widget.ts";
4
+ import { ShortcutsWidget } from "./shortcuts-widget.ts";
5
+ import { SessionsWidget } from "./sessions-widget.ts";
6
+ import { graphWidget } from "./graph-widget.ts";
7
+
8
+ export { SystemWidget } from "./system-widget.ts";
9
+ export { QueueWidget } from "./queue-widget.ts";
10
+ export { ShortcutsWidget } from "./shortcuts-widget.ts";
11
+ export { SessionsWidget } from "./sessions-widget.ts";
12
+ export { graphWidget } from "./graph-widget.ts";
13
+
14
+ export const ALL_WELCOME_WIDGETS: WelcomeWidget[] = [
15
+ SystemWidget,
16
+ graphWidget,
17
+ QueueWidget,
18
+ ShortcutsWidget,
19
+ SessionsWidget,
20
+ ];
@@ -0,0 +1,26 @@
1
+ import type { WelcomeWidget, WidgetRenderContext } from "../types.ts";
2
+
3
+ export const QueueWidget: WelcomeWidget = {
4
+ id: "queue",
5
+ render(ctx: WidgetRenderContext): string[] {
6
+ const { data, dim, color } = ctx;
7
+ const lines: string[] = [];
8
+
9
+ const prefix = dim("- ");
10
+ if (data.queueCount && data.queueCount > 0) {
11
+ lines.push(` ${prefix}${color("gitClean", `${data.queueCount}`)} queued items ready`);
12
+ } else {
13
+ lines.push(` ${prefix}type ${color("model", "# <idea>")} to capture a thought`);
14
+ }
15
+
16
+ if (data.hasStash) {
17
+ lines.push(` ${prefix}${color("gitClean", "1")} draft stashed (Alt+S to pop)`);
18
+ } else {
19
+ lines.push(` ${prefix}press ${color("model", "alt+s")} to park a draft`);
20
+ }
21
+
22
+ lines.push(` ${prefix}${dim("dreaming & mission queue ready")}`);
23
+
24
+ return lines;
25
+ }
26
+ };
@@ -0,0 +1,23 @@
1
+ import type { WelcomeWidget, WidgetRenderContext } from "../types.ts";
2
+
3
+ export const SessionsWidget: WelcomeWidget = {
4
+ id: "sessions",
5
+ render(ctx: WidgetRenderContext): string[] {
6
+ const { data, dim, bold, color } = ctx;
7
+ const lines: string[] = [];
8
+
9
+ lines.push(` ${bold(color("accent", "Recent Crafts"))}`);
10
+
11
+ if (data.recentSessions.length === 0) {
12
+ lines.push(` ${dim("No recent sessions")}`);
13
+ } else {
14
+ for (const session of data.recentSessions.slice(0, 3)) {
15
+ lines.push(
16
+ ` ${dim("• ")}${color("path", session.name)}${dim(` (${session.timeAgo})`)}`
17
+ );
18
+ }
19
+ }
20
+
21
+ return lines;
22
+ }
23
+ };
@@ -0,0 +1,17 @@
1
+ import type { WelcomeWidget, WidgetRenderContext } from "../types.ts";
2
+
3
+ export const ShortcutsWidget: WelcomeWidget = {
4
+ id: "shortcuts",
5
+ render(ctx: WidgetRenderContext): string[] {
6
+ const { dim, bold, color } = ctx;
7
+ const lines: string[] = [];
8
+
9
+ lines.push(` ${bold(color("accent", "Quick Launch / Tactical"))}`);
10
+ lines.push(` ${dim("# <idea> ")} capture idea to queue`);
11
+ lines.push(` ${dim("alt+p ")} tactical powerline overlay`);
12
+ lines.push(` ${dim("!cmd ")} sticky bash session`);
13
+ lines.push(` ${dim("alt+s ")} stash/pop prompt draft`);
14
+
15
+ return lines;
16
+ }
17
+ };
@@ -0,0 +1,29 @@
1
+ import { formatTokens } from "../format.ts";
2
+ import type { WelcomeWidget, WidgetRenderContext } from "../types.ts";
3
+
4
+ export const SystemWidget: WelcomeWidget = {
5
+ id: "system",
6
+ render(ctx: WidgetRenderContext): string[] {
7
+ const { data, dim, bold, color } = ctx;
8
+ const lines: string[] = [];
9
+
10
+ const prefix = dim("- ");
11
+ lines.push(` ${bold(color("accent", "Active Horizon"))}`);
12
+
13
+ lines.push(` ${prefix}Model: ${color("model", data.modelName)} (${dim(data.providerName)})`);
14
+
15
+ if (data.initialContextTokens !== null && data.initialContextTokens > 0) {
16
+ lines.push(
17
+ ` ${prefix}${color("gitClean", `≈ ${formatTokens(data.initialContextTokens)}`)} initial prompt tokens`
18
+ );
19
+ }
20
+
21
+ const { extensions, skills } = data.loadedCounts;
22
+ const toolsCount = extensions + skills;
23
+ if (toolsCount > 0) {
24
+ lines.push(` ${prefix}${color("gitClean", `${toolsCount}`)} skills/extensions loaded`);
25
+ }
26
+
27
+ return lines;
28
+ }
29
+ };
@@ -0,0 +1,144 @@
1
+ // generate.ts
2
+ // CLI-arg parsing and batch generation for `/vibe generate`.
3
+
4
+ import {
5
+ BATCH_PROMPT,
6
+ getVibeFilePath,
7
+ saveVibesToFile,
8
+ vibeState,
9
+ } from "./storage.ts";
10
+ import { buildAiContext, completeVibe } from "./provider.ts";
11
+
12
+ export type GenerateVibesResult =
13
+ | { success: true; count: number; filePath: string }
14
+ | { success: false; count: 0; filePath: string; error: string };
15
+
16
+ export function parseVibeGenerateArgs(
17
+ args: readonly string[],
18
+ ): { theme: string; count: number } | null {
19
+ if (args.length === 0) return null;
20
+
21
+ const last = args.at(-1);
22
+ const parsedCount =
23
+ last && /^\d+$/.test(last) ? Number.parseInt(last, 10) : Number.NaN;
24
+ const hasCount = Number.isFinite(parsedCount) && args.length > 1;
25
+ const theme = hasCount ? args.slice(0, -1).join(" ") : args.join(" ");
26
+ if (!theme) return null;
27
+
28
+ return {
29
+ theme,
30
+ count: hasCount ? Math.min(Math.max(Math.floor(parsedCount), 1), 500) : 100,
31
+ };
32
+ }
33
+
34
+ export async function generateVibesBatch(
35
+ theme: string,
36
+ count: number = 100,
37
+ ): Promise<GenerateVibesResult> {
38
+ const filePath = getVibeFilePath(theme);
39
+ const safeCount = Number.isFinite(count)
40
+ ? Math.min(Math.max(Math.floor(count), 1), 500)
41
+ : 100;
42
+
43
+ if (!vibeState.extensionCtx) {
44
+ return {
45
+ success: false,
46
+ count: 0,
47
+ filePath,
48
+ error: "Extension not initialized",
49
+ };
50
+ }
51
+
52
+ // Parse model spec
53
+ const slashIndex = vibeState.config.modelSpec.indexOf("/");
54
+ if (slashIndex === -1) {
55
+ return { success: false, count: 0, filePath, error: "Invalid model spec" };
56
+ }
57
+ const provider = vibeState.config.modelSpec.slice(0, slashIndex);
58
+ const modelId = vibeState.config.modelSpec.slice(slashIndex + 1);
59
+
60
+ // Resolve model
61
+ const model = vibeState.extensionCtx.modelRegistry.find(provider, modelId);
62
+ if (!model) {
63
+ return {
64
+ success: false,
65
+ count: 0,
66
+ filePath,
67
+ error: `Model not found: ${vibeState.config.modelSpec}`,
68
+ };
69
+ }
70
+
71
+ // Get auth
72
+ const auth =
73
+ await vibeState.extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
74
+ if (!auth.ok) {
75
+ return { success: false, count: 0, filePath, error: auth.error };
76
+ }
77
+
78
+ // Build batch prompt
79
+ const prompt = BATCH_PROMPT.replace(/\{theme\}/g, theme).replace(
80
+ /\{count\}/g,
81
+ String(safeCount),
82
+ );
83
+
84
+ const aiContext = buildAiContext(prompt);
85
+
86
+ try {
87
+ // Use longer timeout for batch generation (30 seconds)
88
+ const signal = AbortSignal.timeout(30000);
89
+ const response = await completeVibe(provider, model, aiContext, {
90
+ apiKey: auth.apiKey,
91
+ headers: auth.headers,
92
+ env: auth.env,
93
+ signal,
94
+ });
95
+
96
+ const textContent = response.content.find((c) => c.type === "text");
97
+ if (!textContent?.text) {
98
+ const error =
99
+ response.stopReason === "error" && response.errorMessage
100
+ ? response.errorMessage
101
+ : "Empty response from model";
102
+ return { success: false, count: 0, filePath, error };
103
+ }
104
+
105
+ // Parse response: one vibe per line
106
+ const vibes = textContent.text
107
+ .split("\n")
108
+ .map((line) => line.trim())
109
+ .filter((line) => line.length > 0)
110
+ .map((line) => {
111
+ // Clean up each line
112
+ let vibe = line.replace(/^["'\d.\-)\s]+/, "").trim(); // Remove leading quotes, numbers, bullets
113
+ vibe = vibe.replace(/["']$/g, ""); // Remove trailing quotes
114
+ if (!vibe.endsWith("...")) {
115
+ vibe = vibe.replace(/\.+$/, "") + "...";
116
+ }
117
+ return vibe;
118
+ })
119
+ .filter((vibe) => vibe.length > 3 && vibe !== "..."); // Filter invalid
120
+
121
+ if (vibes.length === 0) {
122
+ return {
123
+ success: false,
124
+ count: 0,
125
+ filePath,
126
+ error: "No valid vibes generated",
127
+ };
128
+ }
129
+
130
+ // Save to file
131
+ saveVibesToFile(theme, vibes);
132
+
133
+ // Clear cache so next use loads fresh
134
+ if (vibeState.vibeCacheTheme === theme) {
135
+ vibeState.vibeCache = [];
136
+ vibeState.vibeCacheTheme = null;
137
+ }
138
+
139
+ return { success: true, count: vibes.length, filePath };
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : "Unknown error";
142
+ return { success: false, count: 0, filePath, error: message };
143
+ }
144
+ }
@@ -0,0 +1,24 @@
1
+ // index.ts
2
+ // Barrel re-exporting the public API for working-vibes
3
+ // (AI-generated contextual working messages that match a user's preferred theme/vibe).
4
+
5
+ export {
6
+ initVibeManager,
7
+ getVibeTheme,
8
+ setVibeTheme,
9
+ getVibeModel,
10
+ setVibeModel,
11
+ onVibeBeforeAgentStart,
12
+ onVibeAgentStart,
13
+ onVibeToolCall,
14
+ onVibeAgentEnd,
15
+ getVibeMode,
16
+ setVibeMode,
17
+ hasVibeFile,
18
+ getVibeFileCount,
19
+ } from "./manager.ts";
20
+
21
+ export type { VibeMode } from "./storage.ts";
22
+
23
+ export type { GenerateVibesResult } from "./generate.ts";
24
+ export { parseVibeGenerateArgs, generateVibesBatch } from "./generate.ts";