@every-env/spiral-cli 0.2.0 → 1.0.1

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.
@@ -1,205 +0,0 @@
1
- // Suggestion management commands
2
- // See plan: Performance review identified unbounded storage risk
3
-
4
- import { confirm } from "@inquirer/prompts";
5
- import ora from "ora";
6
- import { applyDraftPatch } from "../api";
7
- import { theme } from "../theme";
8
- import type { Suggestion } from "../types";
9
- import { displayInlineDiff } from "./diff";
10
- import { parseSuggestions } from "./parser";
11
-
12
- // LRU cache for suggestions with TTL
13
- const MAX_SUGGESTIONS = 50;
14
- const SUGGESTION_TTL_MS = 30 * 60 * 1000; // 30 minutes
15
-
16
- interface CachedSuggestion {
17
- suggestion: Suggestion;
18
- timestamp: number;
19
- }
20
-
21
- // Store pending suggestions with LRU eviction
22
- const suggestionCache: Map<string, CachedSuggestion> = new Map();
23
-
24
- /**
25
- * Clean expired suggestions and enforce max size
26
- */
27
- function cleanupCache(): void {
28
- const now = Date.now();
29
- const entries = Array.from(suggestionCache.entries());
30
-
31
- // Remove expired
32
- for (const [id, cached] of entries) {
33
- if (now - cached.timestamp > SUGGESTION_TTL_MS) {
34
- suggestionCache.delete(id);
35
- }
36
- }
37
-
38
- // LRU eviction if over limit
39
- if (suggestionCache.size > MAX_SUGGESTIONS) {
40
- const sorted = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
41
- const toRemove = sorted.slice(0, suggestionCache.size - MAX_SUGGESTIONS);
42
- for (const [id] of toRemove) {
43
- suggestionCache.delete(id);
44
- }
45
- }
46
- }
47
-
48
- /**
49
- * Register suggestions from AI response content
50
- */
51
- export function registerSuggestions(content: string): number {
52
- const suggestions = parseSuggestions(content);
53
- for (const sug of suggestions) {
54
- suggestionCache.set(sug.id, {
55
- suggestion: sug,
56
- timestamp: Date.now(),
57
- });
58
- }
59
- cleanupCache();
60
- return suggestions.length;
61
- }
62
-
63
- /**
64
- * Get a suggestion by ID
65
- */
66
- function getSuggestion(id: string): Suggestion | undefined {
67
- const cached = suggestionCache.get(id);
68
- return cached?.suggestion;
69
- }
70
-
71
- /**
72
- * List all pending suggestions
73
- */
74
- export function listPendingSuggestions(options: { json?: boolean }): void {
75
- cleanupCache();
76
- const suggestions = Array.from(suggestionCache.values())
77
- .map((c) => c.suggestion)
78
- .filter((s) => s.status === "pending");
79
-
80
- if (options.json) {
81
- console.log(JSON.stringify({ success: true, suggestions }));
82
- return;
83
- }
84
-
85
- if (suggestions.length === 0) {
86
- console.log(theme.dim("No pending suggestions."));
87
- return;
88
- }
89
-
90
- console.log(theme.heading(`\nPending Suggestions (${suggestions.length}):\n`));
91
-
92
- for (const sug of suggestions) {
93
- console.log(`${theme.dim(sug.id)} -> draft ${theme.dim(sug.targetDraftId.slice(0, 8))}`);
94
- console.log(` ${theme.dim("Before:")} ${sug.before.slice(0, 40)}...`);
95
- console.log(` ${theme.success("After:")} ${sug.after.slice(0, 40)}...`);
96
- if (sug.rationale) {
97
- console.log(` ${theme.info("Why:")} ${sug.rationale}`);
98
- }
99
- console.log();
100
- }
101
- }
102
-
103
- /**
104
- * Preview a suggestion with diff display
105
- */
106
- export async function previewSuggestion(
107
- suggestionId: string,
108
- options: { json?: boolean },
109
- ): Promise<void> {
110
- const suggestion = getSuggestion(suggestionId);
111
-
112
- if (!suggestion) {
113
- throw new Error(`Suggestion not found: ${suggestionId}`);
114
- }
115
-
116
- if (options.json) {
117
- console.log(JSON.stringify({ success: true, suggestion }));
118
- return;
119
- }
120
-
121
- console.log(theme.heading("\nSuggestion Preview"));
122
- console.log(theme.dim(`Target: draft ${suggestion.targetDraftId.slice(0, 8)}`));
123
-
124
- if (suggestion.rationale) {
125
- console.log(theme.info(`\nRationale: ${suggestion.rationale}`));
126
- }
127
-
128
- displayInlineDiff(suggestion.before, suggestion.after);
129
- }
130
-
131
- /**
132
- * Apply a suggestion to its target draft
133
- */
134
- export async function applySuggestion(
135
- sessionId: string,
136
- suggestionId: string,
137
- options: { json?: boolean; force?: boolean },
138
- ): Promise<void> {
139
- const suggestion = getSuggestion(suggestionId);
140
-
141
- if (!suggestion) {
142
- throw new Error(`Suggestion not found: ${suggestionId}`);
143
- }
144
-
145
- if (suggestion.status !== "pending") {
146
- throw new Error(`Suggestion already ${suggestion.status}`);
147
- }
148
-
149
- // Show preview and confirm (unless --force)
150
- if (!options.force && !options.json) {
151
- await previewSuggestion(suggestionId, { json: false });
152
- const proceed = await confirm({
153
- message: "Apply this suggestion?",
154
- default: true,
155
- });
156
- if (!proceed) {
157
- console.log(theme.dim("Cancelled."));
158
- return;
159
- }
160
- }
161
-
162
- const spinner = ora("Applying suggestion...").start();
163
-
164
- try {
165
- await applyDraftPatch(sessionId, suggestion.targetDraftId, [
166
- {
167
- type: "replace",
168
- range: suggestion.range,
169
- content: suggestion.after,
170
- },
171
- ]);
172
-
173
- suggestion.status = "applied";
174
- spinner.succeed("Suggestion applied");
175
-
176
- if (options.json) {
177
- console.log(JSON.stringify({ success: true, suggestion }));
178
- }
179
- } catch (error) {
180
- spinner.fail("Failed to apply suggestion");
181
- throw error;
182
- }
183
- }
184
-
185
- /**
186
- * Dismiss a suggestion without applying
187
- */
188
- export function dismissSuggestion(suggestionId: string, options: { json?: boolean }): void {
189
- const suggestion = getSuggestion(suggestionId);
190
-
191
- if (!suggestion) {
192
- throw new Error(`Suggestion not found: ${suggestionId}`);
193
- }
194
-
195
- suggestion.status = "dismissed";
196
-
197
- if (options.json) {
198
- console.log(JSON.stringify({ success: true, dismissed: suggestionId }));
199
- } else {
200
- console.log(theme.dim(`Dismissed suggestion ${suggestionId}`));
201
- }
202
- }
203
-
204
- // Re-export parser functions
205
- export { hasSuggestions, parseSuggestions } from "./parser";
@@ -1,83 +0,0 @@
1
- // Suggestion parser with type-safe JSON handling
2
- // See plan: TypeScript review identified need for type guards
3
-
4
- import type { Suggestion } from "../types";
5
-
6
- // Suggestion syntax in AI responses:
7
- // ```suggestion[target=draft-id]
8
- // {"before": "old text", "after": "new text", "rationale": "why"}
9
- // ```
10
-
11
- const SUGGESTION_REGEX = /```suggestion\[target=([^\]]+)\]\n([\s\S]*?)```/g;
12
-
13
- // TYPESCRIPT: Type guard for safe JSON parsing
14
- interface SuggestionPayload {
15
- before?: string;
16
- after?: string;
17
- rationale?: string;
18
- range?: [number, number];
19
- }
20
-
21
- function isSuggestionPayload(value: unknown): value is SuggestionPayload {
22
- return (
23
- typeof value === "object" &&
24
- value !== null &&
25
- (!("before" in value) || typeof (value as SuggestionPayload).before === "string") &&
26
- (!("after" in value) || typeof (value as SuggestionPayload).after === "string")
27
- );
28
- }
29
-
30
- /**
31
- * Parse suggestions from AI response content
32
- */
33
- export function parseSuggestions(content: string): Suggestion[] {
34
- const suggestions: Suggestion[] = [];
35
- // Create fresh regex each call to avoid state issues with global flag
36
- const regex = new RegExp(SUGGESTION_REGEX.source, "g");
37
- let match: RegExpExecArray | null = regex.exec(content);
38
-
39
- while (match !== null) {
40
- const targetDraftId = match[1];
41
- const jsonContent = match[2]?.trim();
42
-
43
- // Guard against undefined captures
44
- if (!targetDraftId || !jsonContent) continue;
45
-
46
- try {
47
- const parsed: unknown = JSON.parse(jsonContent);
48
-
49
- // TYPESCRIPT: Validate shape before use
50
- if (!isSuggestionPayload(parsed)) {
51
- if (process.env.DEBUG) {
52
- console.debug("Invalid suggestion format");
53
- }
54
- continue;
55
- }
56
-
57
- suggestions.push({
58
- id: `sug-${Date.now()}-${suggestions.length}`,
59
- targetDraftId,
60
- before: parsed.before ?? "",
61
- after: parsed.after ?? "",
62
- rationale: parsed.rationale,
63
- range: parsed.range,
64
- status: "pending",
65
- });
66
- } catch {
67
- // Skip malformed suggestions
68
- if (process.env.DEBUG) {
69
- console.debug("Skipping malformed suggestion:", jsonContent.slice(0, 100));
70
- }
71
- }
72
- match = regex.exec(content);
73
- }
74
-
75
- return suggestions;
76
- }
77
-
78
- /**
79
- * Check if content contains suggestions
80
- */
81
- export function hasSuggestions(content: string): boolean {
82
- return new RegExp(SUGGESTION_REGEX.source).test(content);
83
- }
@@ -1,104 +0,0 @@
1
- // Tool call visualization with spinner management
2
- import type { Ora } from "ora";
3
- import ora from "ora";
4
- import { theme } from "../theme";
5
- import type { ToolCallUpdate } from "../types";
6
-
7
- // Tool icons for visual identification
8
- const TOOL_ICONS: Record<string, string> = {
9
- search_web: "S",
10
- read_file: "R",
11
- create_draft: "D",
12
- replace_draft_content: "E",
13
- analyze: "A",
14
- default: "T",
15
- };
16
-
17
- // Track active tool spinners
18
- const toolSpinners: Map<string, Ora> = new Map();
19
-
20
- /**
21
- * Render tool call start
22
- */
23
- export function renderToolStart(callId: string, toolName: string): void {
24
- const icon = TOOL_ICONS[toolName] || TOOL_ICONS.default;
25
- const spinner = ora({
26
- text: `[${icon}] ${toolName}`,
27
- prefixText: theme.dim(`[${callId.slice(0, 6)}]`),
28
- }).start();
29
- toolSpinners.set(callId, spinner);
30
- }
31
-
32
- /**
33
- * Render tool call completion
34
- */
35
- export function renderToolComplete(
36
- callId: string,
37
- toolName: string,
38
- result?: unknown,
39
- error?: string,
40
- ): void {
41
- const spinner = toolSpinners.get(callId);
42
- const icon = TOOL_ICONS[toolName] || TOOL_ICONS.default;
43
-
44
- if (error) {
45
- spinner?.fail(`[${icon}] ${toolName}: ${theme.error(error.slice(0, 50))}`);
46
- } else {
47
- const summary = formatResultSummary(result);
48
- spinner?.succeed(`[${icon}] ${toolName}${summary ? `: ${summary}` : ""}`);
49
- }
50
-
51
- toolSpinners.delete(callId);
52
- }
53
-
54
- /**
55
- * Handle tool call update event
56
- */
57
- export function handleToolCallUpdate(update: ToolCallUpdate): void {
58
- switch (update.status) {
59
- case "started":
60
- renderToolStart(update.callId, update.toolName);
61
- break;
62
- case "completed":
63
- renderToolComplete(update.callId, update.toolName, update.result);
64
- break;
65
- case "error":
66
- renderToolComplete(update.callId, update.toolName, undefined, update.error);
67
- break;
68
- case "arguments_delta":
69
- case "arguments_done":
70
- // Update spinner text if needed
71
- {
72
- const spinner = toolSpinners.get(update.callId);
73
- if (spinner) {
74
- const icon = TOOL_ICONS[update.toolName] || TOOL_ICONS.default;
75
- spinner.text = `[${icon}] ${update.toolName}...`;
76
- }
77
- }
78
- break;
79
- }
80
- }
81
-
82
- /**
83
- * Format result for summary display
84
- */
85
- function formatResultSummary(result: unknown): string {
86
- if (!result) return "";
87
- if (typeof result === "string") return result.slice(0, 50);
88
- if (Array.isArray(result)) return `${result.length} items`;
89
- if (typeof result === "object") {
90
- const keys = Object.keys(result);
91
- return `{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? "..." : ""}}`;
92
- }
93
- return String(result).slice(0, 50);
94
- }
95
-
96
- /**
97
- * Clean up all active tool spinners
98
- */
99
- export function cleanupToolSpinners(): void {
100
- for (const spinner of toolSpinners.values()) {
101
- spinner.stop();
102
- }
103
- toolSpinners.clear();
104
- }
@@ -1,55 +0,0 @@
1
- // Workspace management
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { fetchWorkspaces } from "../api";
5
- import { theme } from "../theme";
6
-
7
- export interface WorkspaceCommandOptions {
8
- json?: boolean;
9
- }
10
-
11
- /**
12
- * List all available workspaces
13
- */
14
- export async function listWorkspaces(options: WorkspaceCommandOptions): Promise<void> {
15
- const spinner = ora("Fetching workspaces...").start();
16
-
17
- try {
18
- const workspaces = await fetchWorkspaces();
19
- spinner.succeed("Workspaces loaded");
20
-
21
- if (options.json) {
22
- console.log(JSON.stringify({ success: true, workspaces }));
23
- return;
24
- }
25
-
26
- if (workspaces.length === 0) {
27
- console.log(theme.dim("No workspaces found."));
28
- return;
29
- }
30
-
31
- console.log(theme.heading("\nWorkspaces:\n"));
32
-
33
- // Group by type (case-insensitive comparison)
34
- const personal = workspaces.filter((w) => w.workspace_type?.toLowerCase() === "personal");
35
- const team = workspaces.filter((w) => w.workspace_type?.toLowerCase() === "team");
36
-
37
- if (personal.length > 0) {
38
- console.log(theme.info("Personal:"));
39
- for (const ws of personal) {
40
- console.log(` ${theme.dim(ws.id.slice(0, 8))} ${chalk.white(ws.name)}`);
41
- }
42
- console.log();
43
- }
44
-
45
- if (team.length > 0) {
46
- console.log(theme.info("Team:"));
47
- for (const ws of team) {
48
- console.log(` ${theme.dim(ws.id.slice(0, 8))} ${chalk.white(ws.name)}`);
49
- }
50
- }
51
- } catch (error) {
52
- spinner.fail("Failed to fetch workspaces");
53
- throw error;
54
- }
55
- }