@every-env/spiral-cli 0.2.0 → 1.0.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/README.md +35 -6
- package/package.json +5 -14
- package/src/api.ts +82 -474
- package/src/auth.ts +49 -214
- package/src/cli.ts +264 -948
- package/src/config.ts +29 -45
- package/src/output.ts +162 -0
- package/src/types.ts +87 -117
- package/src/attachments/index.ts +0 -174
- package/src/drafts/editor.ts +0 -105
- package/src/drafts/index.ts +0 -208
- package/src/notes/index.ts +0 -130
- package/src/styles/index.ts +0 -45
- package/src/suggestions/diff.ts +0 -33
- package/src/suggestions/index.ts +0 -205
- package/src/suggestions/parser.ts +0 -83
- package/src/tools/renderer.ts +0 -104
- package/src/workspaces/index.ts +0 -55
package/src/suggestions/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tools/renderer.ts
DELETED
|
@@ -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
|
-
}
|
package/src/workspaces/index.ts
DELETED
|
@@ -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
|
-
}
|