astrabot 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.
- package/README.md +411 -0
- package/ai/ai.config.ts +27 -0
- package/ai/auto-retry.ts +117 -0
- package/ai/config-loader.ts +132 -0
- package/ai/index.ts +4 -0
- package/ai/retry-prompt.ts +30 -0
- package/bin/astra +2 -0
- package/core/retry/error-classifier.ts +208 -0
- package/core/retry/index.ts +29 -0
- package/core/retry/retry-config.ts +142 -0
- package/core/retry/retry-engine.ts +215 -0
- package/game/index.html +573 -0
- package/game/neon-breaker.html +1037 -0
- package/index.ts +140 -0
- package/modes/agent/action-tracker.ts +47 -0
- package/modes/agent/agent-tools.ts +338 -0
- package/modes/agent/approval.ts +184 -0
- package/modes/agent/diff-view.ts +34 -0
- package/modes/agent/orchestrator.ts +234 -0
- package/modes/agent/tool-executor.ts +993 -0
- package/modes/agent/types.ts +68 -0
- package/modes/ask/orchestrator.ts +230 -0
- package/modes/auto.ts +88 -0
- package/modes/cli.ts +43 -0
- package/modes/multi/agent-pool-manager.ts +337 -0
- package/modes/multi/examples.ts +441 -0
- package/modes/multi/message-broker.ts +179 -0
- package/modes/multi/multi-agent-orchestrator.ts +891 -0
- package/modes/multi/orchestrator.ts +414 -0
- package/modes/multi/types.ts +245 -0
- package/modes/multi/workflow-builder.ts +569 -0
- package/modes/plan/orchestrator.ts +198 -0
- package/modes/plan/planner.ts +121 -0
- package/modes/plan/selection.ts +43 -0
- package/modes/plan/types.ts +13 -0
- package/modes/plan/web-tools.ts +132 -0
- package/modes/setup.ts +210 -0
- package/package.json +62 -0
- package/session/index.ts +45 -0
- package/session/session-context.ts +188 -0
- package/session/session-manager.ts +374 -0
- package/session/session-tools.ts +109 -0
- package/session/store.ts +278 -0
- package/tsconfig.json +30 -0
- package/tui/spinner.ts +182 -0
- package/tui/terminal-md.ts +17 -0
- package/tui/wakeup.ts +231 -0
package/modes/setup.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { intro, outro, text, confirm, isCancel, autocomplete } from "@clack/prompts";
|
|
3
|
+
import {
|
|
4
|
+
getEnv,
|
|
5
|
+
getConfigPath,
|
|
6
|
+
saveConfig,
|
|
7
|
+
} from "../ai/config-loader";
|
|
8
|
+
import { withSpinner } from "../tui/spinner"; // Adjust path as necessary
|
|
9
|
+
|
|
10
|
+
// 1. Updated interface to include OpenRouter's exact pricing object structure
|
|
11
|
+
interface OpenRouterModel {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
architecture?: {
|
|
15
|
+
modality?: string;
|
|
16
|
+
output_modalities?: string[];
|
|
17
|
+
};
|
|
18
|
+
pricing?: {
|
|
19
|
+
prompt: string; // Price per individual token
|
|
20
|
+
completion: string; // Price per individual token
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runSetup(): Promise<void> {
|
|
25
|
+
intro(chalk.bold("astra setup"));
|
|
26
|
+
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.dim(
|
|
29
|
+
`Config will be saved to ${getConfigPath()}\n` +
|
|
30
|
+
`Existing values will be preserved when possible.\n`
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const currentKey = getEnv("OPENROUTER_API_KEY") ?? "";
|
|
35
|
+
const currentModel = getEnv("OPENROUTER_DEFAULT_MODEL") ?? "";
|
|
36
|
+
const currentFirecrawl = getEnv("FIRECRAWL_API_KEY") ?? "";
|
|
37
|
+
const currentSkillsDirs = getEnv("SKILLS_DIRS") ?? "";
|
|
38
|
+
|
|
39
|
+
// ── OpenRouter API Key ──────────────────────────────────────────
|
|
40
|
+
const setKey = await confirm({
|
|
41
|
+
message: "Set OpenRouter API key?",
|
|
42
|
+
initialValue: !currentKey,
|
|
43
|
+
});
|
|
44
|
+
if (isCancel(setKey)) return outro(chalk.dim("Setup cancelled."));
|
|
45
|
+
|
|
46
|
+
let apiKey = currentKey;
|
|
47
|
+
if (setKey) {
|
|
48
|
+
const val = await text({
|
|
49
|
+
message: "OpenRouter API key",
|
|
50
|
+
placeholder: "sk-or-...",
|
|
51
|
+
initialValue: currentKey,
|
|
52
|
+
validate: (v) =>
|
|
53
|
+
(v ?? "").trim() ? undefined : "API key is required",
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
|
|
56
|
+
apiKey = val.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Fetching All OpenRouter Models Dynamically ───────────────────
|
|
60
|
+
let modelOptions: Array<{ value: string; label: string; hint?: string }> = [];
|
|
61
|
+
|
|
62
|
+
const setModel = await confirm({
|
|
63
|
+
message: "Set default OpenRouter model?",
|
|
64
|
+
initialValue: !currentModel,
|
|
65
|
+
});
|
|
66
|
+
if (isCancel(setModel)) return outro(chalk.dim("Setup cancelled."));
|
|
67
|
+
|
|
68
|
+
if (setModel) {
|
|
69
|
+
try {
|
|
70
|
+
modelOptions = await withSpinner(
|
|
71
|
+
{
|
|
72
|
+
message: "Fetching all available OpenRouter models & pricing...",
|
|
73
|
+
doneMessage: "Loaded models successfully.",
|
|
74
|
+
failMessage: "Failed to fetch dynamic list. Dropping back to fallback entry.",
|
|
75
|
+
},
|
|
76
|
+
async () => {
|
|
77
|
+
const response = await fetch("https://openrouter.ai/api/v1/models");
|
|
78
|
+
if (!response.ok) throw new Error("Failed to communicate with OpenRouter registry");
|
|
79
|
+
|
|
80
|
+
const json = (await response.json()) as { data: OpenRouterModel[] };
|
|
81
|
+
|
|
82
|
+
return json.data
|
|
83
|
+
.filter((model) => {
|
|
84
|
+
const outModalities = model.architecture?.output_modalities;
|
|
85
|
+
const modalityStr = model.architecture?.modality || "";
|
|
86
|
+
|
|
87
|
+
if (outModalities) return outModalities.includes("text");
|
|
88
|
+
if (modalityStr) return modalityStr.endsWith("->text");
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
})
|
|
92
|
+
.map((model) => {
|
|
93
|
+
const provider = model.id.split("/")[0];
|
|
94
|
+
|
|
95
|
+
// Convert price-per-token strings to a clean price per 1 Million tokens
|
|
96
|
+
const promptPriceNum = parseFloat(model.pricing?.prompt || "0") * 1_000_000;
|
|
97
|
+
const completionPriceNum = parseFloat(model.pricing?.completion || "0") * 1_000_000;
|
|
98
|
+
|
|
99
|
+
let pricingHint = "Free";
|
|
100
|
+
if (promptPriceNum > 0 || completionPriceNum > 0) {
|
|
101
|
+
pricingHint = `In: $${promptPriceNum.toFixed(2)}, Out: $${completionPriceNum.toFixed(2)} /1M`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
value: model.id,
|
|
106
|
+
label: model.name || model.id,
|
|
107
|
+
hint: `${provider} (${pricingHint})`,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
modelOptions = [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let modelId = currentModel;
|
|
117
|
+
|
|
118
|
+
if (modelOptions.length > 0) {
|
|
119
|
+
const selectedModel = await autocomplete({
|
|
120
|
+
message: "Select an OpenRouter text model (Type to search & compare pricing)",
|
|
121
|
+
options: [
|
|
122
|
+
...modelOptions,
|
|
123
|
+
{ value: "custom", label: "Custom Entry...", hint: "Type manual ID" }
|
|
124
|
+
],
|
|
125
|
+
placeholder: "Search e.g. 'claude', 'gpt', 'llama'...",
|
|
126
|
+
});
|
|
127
|
+
if (isCancel(selectedModel)) return outro(chalk.dim("Setup cancelled."));
|
|
128
|
+
|
|
129
|
+
if (selectedModel === "custom") {
|
|
130
|
+
const customVal = await text({
|
|
131
|
+
message: "Enter custom OpenRouter Model ID",
|
|
132
|
+
placeholder: "provider/model-name",
|
|
133
|
+
initialValue: currentModel,
|
|
134
|
+
validate: (v) => ((v ?? "").trim() ? undefined : "Model ID is required"),
|
|
135
|
+
});
|
|
136
|
+
if (isCancel(customVal)) return outro(chalk.dim("Setup cancelled."));
|
|
137
|
+
modelId = customVal.trim();
|
|
138
|
+
} else {
|
|
139
|
+
modelId = selectedModel as string;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const fallbackVal = await text({
|
|
143
|
+
message: "Enter OpenRouter Model ID",
|
|
144
|
+
placeholder: "anthropic/claude-3.5-sonnet",
|
|
145
|
+
initialValue: currentModel || "anthropic/claude-3.5-sonnet",
|
|
146
|
+
validate: (v) => ((v ?? "").trim() ? undefined : "Model ID is required"),
|
|
147
|
+
});
|
|
148
|
+
if (isCancel(fallbackVal)) return outro(chalk.dim("Setup cancelled."));
|
|
149
|
+
modelId = fallbackVal.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var finalModelId = modelId;
|
|
153
|
+
} else {
|
|
154
|
+
var finalModelId = currentModel;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Firecrawl API Key (optional) ────────────────────────────────
|
|
158
|
+
const setFirecrawl = await confirm({
|
|
159
|
+
message: "Set Firecrawl API key? (optional — enables web search & crawl)",
|
|
160
|
+
initialValue: false,
|
|
161
|
+
});
|
|
162
|
+
if (isCancel(setFirecrawl)) return outro(chalk.dim("Setup cancelled."));
|
|
163
|
+
|
|
164
|
+
let firecrawlKey = currentFirecrawl;
|
|
165
|
+
if (setFirecrawl) {
|
|
166
|
+
const val = await text({
|
|
167
|
+
message: "Firecrawl API key",
|
|
168
|
+
placeholder: "fc-...",
|
|
169
|
+
initialValue: currentFirecrawl,
|
|
170
|
+
validate: (_v) => undefined,
|
|
171
|
+
});
|
|
172
|
+
if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
|
|
173
|
+
firecrawlKey = (val ?? "").trim();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Skills Directories (optional) ───────────────────────────────
|
|
177
|
+
const setSkills = await confirm({
|
|
178
|
+
message: "Set custom skills directories? (optional)",
|
|
179
|
+
initialValue: false,
|
|
180
|
+
});
|
|
181
|
+
if (isCancel(setSkills)) return outro(chalk.dim("Setup cancelled."));
|
|
182
|
+
|
|
183
|
+
let skillsDirs = currentSkillsDirs;
|
|
184
|
+
if (setSkills) {
|
|
185
|
+
const val = await text({
|
|
186
|
+
message: "Skills directories (semicolon-separated)",
|
|
187
|
+
placeholder: "/path/to/skills;/another/dir",
|
|
188
|
+
initialValue: currentSkillsDirs,
|
|
189
|
+
validate: (_v) => undefined,
|
|
190
|
+
});
|
|
191
|
+
if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
|
|
192
|
+
skillsDirs = (val ?? "").trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Save ────────────────────────────────────────────────────────
|
|
196
|
+
const entries: Record<string, string> = {};
|
|
197
|
+
if (apiKey) entries.OPENROUTER_API_KEY = apiKey;
|
|
198
|
+
if (finalModelId) entries.OPENROUTER_DEFAULT_MODEL = finalModelId;
|
|
199
|
+
if (firecrawlKey) entries.FIRECRAWL_API_KEY = firecrawlKey;
|
|
200
|
+
if (skillsDirs) entries.SKILLS_DIRS = skillsDirs;
|
|
201
|
+
|
|
202
|
+
saveConfig(entries);
|
|
203
|
+
|
|
204
|
+
outro(
|
|
205
|
+
chalk.green(
|
|
206
|
+
`\n✔ Configuration saved to ${getConfigPath()}\n` +
|
|
207
|
+
` You can now run "astra wakeup" to get started.\n`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astrabot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-native development companion — Agent, Ask, and Plan modes in your terminal.",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"astra": "bin/astra"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/astra",
|
|
12
|
+
"index.ts",
|
|
13
|
+
"ai/",
|
|
14
|
+
"modes/",
|
|
15
|
+
"tui/",
|
|
16
|
+
"session/",
|
|
17
|
+
"core/",
|
|
18
|
+
"game/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"tsconfig.json"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "bun run index.ts",
|
|
24
|
+
"setup": "bun run index.ts setup",
|
|
25
|
+
"wakeup": "bun run index.ts wakeup",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"prepublishOnly": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@clack/core": "^1.3.1",
|
|
37
|
+
"@clack/prompts": "^1.4.0",
|
|
38
|
+
"@mendable/firecrawl-js": "^4.25.1",
|
|
39
|
+
"@openrouter/ai-sdk-provider": "^2.9.0",
|
|
40
|
+
"@types/marked-terminal": "^6.1.1",
|
|
41
|
+
"@types/node": "^25.9.1",
|
|
42
|
+
"chalk": "^5.6.2",
|
|
43
|
+
"commander": "^15.0.0",
|
|
44
|
+
"diff": "^9.0.0",
|
|
45
|
+
"docx": "^9.7.1",
|
|
46
|
+
"dotenv": "^17.4.2",
|
|
47
|
+
"figlet": "^1.11.0",
|
|
48
|
+
"marked": "^18.0.4",
|
|
49
|
+
"marked-terminal": "^7.3.0"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"ai",
|
|
53
|
+
"cli",
|
|
54
|
+
"agent",
|
|
55
|
+
"openrouter",
|
|
56
|
+
"coding-assistant"
|
|
57
|
+
],
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"engines": {
|
|
60
|
+
"bun": ">=1.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/session/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ── Store (raw persistence layer) ─────────────────────────────────────────
|
|
2
|
+
export {
|
|
3
|
+
listSessions,
|
|
4
|
+
getSession,
|
|
5
|
+
getMostRecentSession,
|
|
6
|
+
createSession,
|
|
7
|
+
updateSession,
|
|
8
|
+
deleteSession,
|
|
9
|
+
clearAllSessions,
|
|
10
|
+
appendTranscript,
|
|
11
|
+
readSessionActions,
|
|
12
|
+
} from "./store";
|
|
13
|
+
export type {
|
|
14
|
+
SessionMode,
|
|
15
|
+
SessionStatus,
|
|
16
|
+
SessionEntry,
|
|
17
|
+
TranscriptMessage,
|
|
18
|
+
SessionStoreIndex,
|
|
19
|
+
} from "./store";
|
|
20
|
+
|
|
21
|
+
// ── Context building ───────────────────────────────────────────────────────
|
|
22
|
+
export {
|
|
23
|
+
captureSessionContext,
|
|
24
|
+
buildContextSummary,
|
|
25
|
+
buildSessionOneliner,
|
|
26
|
+
} from "./session-context";
|
|
27
|
+
export type { SessionContextData } from "./session-context";
|
|
28
|
+
|
|
29
|
+
// ── Session lifecycle ──────────────────────────────────────────────────────
|
|
30
|
+
export {
|
|
31
|
+
beginSession,
|
|
32
|
+
endSession,
|
|
33
|
+
endMultiSession,
|
|
34
|
+
markSessionInterrupted,
|
|
35
|
+
recordUserMessage,
|
|
36
|
+
recordAgentMessage,
|
|
37
|
+
getResumableSession,
|
|
38
|
+
getSessionHistory,
|
|
39
|
+
removeSession,
|
|
40
|
+
formatSessionLine,
|
|
41
|
+
} from "./session-manager";
|
|
42
|
+
export type { BeginSessionResult } from "./session-manager";
|
|
43
|
+
|
|
44
|
+
// ── Agent tools ────────────────────────────────────────────────────────────
|
|
45
|
+
export { createSessionTools } from "./session-tools";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { ActionTracker } from "../modes/agent/action-tracker";
|
|
2
|
+
import type { SessionEntry, TranscriptMessage } from "./store";
|
|
3
|
+
|
|
4
|
+
export type { TranscriptMessage };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rich context persisted alongside a session entry.
|
|
8
|
+
*/
|
|
9
|
+
export interface SessionContextData {
|
|
10
|
+
transcript: TranscriptMessage[];
|
|
11
|
+
activeFiles: string[];
|
|
12
|
+
pendingTasks: string[];
|
|
13
|
+
lastAgentResponse: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the full context payload from the current session state.
|
|
18
|
+
* This is called at session end so all fields are populated.
|
|
19
|
+
*/
|
|
20
|
+
export function captureSessionContext(
|
|
21
|
+
tracker: ActionTracker,
|
|
22
|
+
transcript: TranscriptMessage[],
|
|
23
|
+
lastAgentResponse: string,
|
|
24
|
+
pendingTasks: string[] = []
|
|
25
|
+
): SessionContextData {
|
|
26
|
+
const actions = tracker.getActions();
|
|
27
|
+
const activeFiles = [
|
|
28
|
+
...new Set(
|
|
29
|
+
actions
|
|
30
|
+
.filter(
|
|
31
|
+
(a) =>
|
|
32
|
+
a.type === "file_create" ||
|
|
33
|
+
a.type === "file_modify" ||
|
|
34
|
+
a.type === "file_delete" ||
|
|
35
|
+
a.type === "code_analysis"
|
|
36
|
+
)
|
|
37
|
+
.map((a) => a.path)
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
transcript,
|
|
44
|
+
activeFiles,
|
|
45
|
+
pendingTasks,
|
|
46
|
+
lastAgentResponse,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Context summary builders ────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the FULL resumption context block injected into the agent's system
|
|
54
|
+
* prompt when continuing an existing session.
|
|
55
|
+
*
|
|
56
|
+
* Includes:
|
|
57
|
+
* - session metadata
|
|
58
|
+
* - pending tasks
|
|
59
|
+
* - recent transcript (last N turns, configurable)
|
|
60
|
+
* - touched files
|
|
61
|
+
* - action counts
|
|
62
|
+
* - the agent's last response
|
|
63
|
+
*/
|
|
64
|
+
export function buildContextSummary(
|
|
65
|
+
entry: SessionEntry,
|
|
66
|
+
opts: { transcriptTurns?: number } = {}
|
|
67
|
+
): string {
|
|
68
|
+
const { transcriptTurns = 10 } = opts;
|
|
69
|
+
const parts: string[] = [];
|
|
70
|
+
|
|
71
|
+
parts.push("╔══════════════════════════════════════╗");
|
|
72
|
+
parts.push("║ RESUMED SESSION CONTEXT ║");
|
|
73
|
+
parts.push("╚══════════════════════════════════════╝");
|
|
74
|
+
parts.push("");
|
|
75
|
+
|
|
76
|
+
// ── Core metadata
|
|
77
|
+
parts.push(`Mode : ${entry.mode}`);
|
|
78
|
+
parts.push(`Session ID : ${entry.id}`);
|
|
79
|
+
parts.push(`Started : ${new Date(entry.createdAt).toLocaleString()}`);
|
|
80
|
+
parts.push("");
|
|
81
|
+
|
|
82
|
+
// ── Goals (all goals across session, most recent last)
|
|
83
|
+
if (entry.allGoals && entry.allGoals.length > 1) {
|
|
84
|
+
parts.push("Goals in this session:");
|
|
85
|
+
entry.allGoals.forEach((g, i) => parts.push(` ${i + 1}. ${g}`));
|
|
86
|
+
} else {
|
|
87
|
+
parts.push(`Goal: ${entry.lastGoal}`);
|
|
88
|
+
}
|
|
89
|
+
parts.push("");
|
|
90
|
+
|
|
91
|
+
// ── Summary (LLM-generated)
|
|
92
|
+
if (entry.summary) {
|
|
93
|
+
parts.push("What happened:");
|
|
94
|
+
parts.push(` ${entry.summary}`);
|
|
95
|
+
parts.push("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Pending tasks
|
|
99
|
+
if (entry.pendingTasks && entry.pendingTasks.length > 0) {
|
|
100
|
+
parts.push("⚠ Pending / incomplete tasks:");
|
|
101
|
+
entry.pendingTasks.forEach((t) => parts.push(` • ${t}`));
|
|
102
|
+
parts.push("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Files
|
|
106
|
+
if (entry.touchedFiles.length > 0) {
|
|
107
|
+
const shown = entry.touchedFiles.slice(0, 20);
|
|
108
|
+
parts.push(`Files touched (${entry.touchedFiles.length}):`);
|
|
109
|
+
shown.forEach((f) => parts.push(` • ${f}`));
|
|
110
|
+
if (entry.touchedFiles.length > 20) {
|
|
111
|
+
parts.push(` … and ${entry.touchedFiles.length - 20} more`);
|
|
112
|
+
}
|
|
113
|
+
parts.push("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Action counts
|
|
117
|
+
if (entry.appliedActions > 0 || entry.rejectedActions > 0) {
|
|
118
|
+
parts.push(
|
|
119
|
+
`Actions: ${entry.appliedActions} applied, ${entry.rejectedActions} rejected.`
|
|
120
|
+
);
|
|
121
|
+
parts.push("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Transcript (most recent N turns)
|
|
125
|
+
if (entry.transcript && entry.transcript.length > 0) {
|
|
126
|
+
const turns = entry.transcript.slice(-transcriptTurns * 2);
|
|
127
|
+
parts.push(`Recent conversation (last ${Math.floor(turns.length / 2)} turns):`);
|
|
128
|
+
parts.push("─".repeat(40));
|
|
129
|
+
for (const msg of turns) {
|
|
130
|
+
const label =
|
|
131
|
+
msg.role === "user"
|
|
132
|
+
? "User"
|
|
133
|
+
: msg.role === "agent"
|
|
134
|
+
? "Agent"
|
|
135
|
+
: "System";
|
|
136
|
+
const snippet = msg.content.length > 400
|
|
137
|
+
? msg.content.slice(0, 400) + "…"
|
|
138
|
+
: msg.content;
|
|
139
|
+
parts.push(`[${label}] ${snippet}`);
|
|
140
|
+
}
|
|
141
|
+
parts.push("─".repeat(40));
|
|
142
|
+
parts.push("");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Last agent response (in case transcript was trimmed)
|
|
146
|
+
if (entry.lastAgentResponse && entry.transcript.length === 0) {
|
|
147
|
+
parts.push("Agent's last response:");
|
|
148
|
+
parts.push(
|
|
149
|
+
entry.lastAgentResponse.length > 800
|
|
150
|
+
? entry.lastAgentResponse.slice(0, 800) + "…"
|
|
151
|
+
: entry.lastAgentResponse
|
|
152
|
+
);
|
|
153
|
+
parts.push("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
parts.push("You are now resuming this session. Continue where you left off.");
|
|
157
|
+
parts.push(
|
|
158
|
+
entry.pendingTasks && entry.pendingTasks.length > 0
|
|
159
|
+
? "Address the pending tasks above unless the user specifies otherwise."
|
|
160
|
+
: "Await the user's next instruction."
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return parts.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Lightweight one-line summary for display in the CLI session picker.
|
|
168
|
+
*/
|
|
169
|
+
export function buildSessionOneliner(entry: SessionEntry): string {
|
|
170
|
+
const age = humanAge(entry.updatedAt);
|
|
171
|
+
const pending = entry.pendingTasks?.length
|
|
172
|
+
? ` [${entry.pendingTasks.length} pending]`
|
|
173
|
+
: "";
|
|
174
|
+
return `${age.padEnd(8)} [${entry.mode}] ${entry.lastGoal.slice(0, 60)}${pending}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function humanAge(isoString: string): string {
|
|
180
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
181
|
+
const minutes = Math.floor(diff / 60_000);
|
|
182
|
+
if (minutes < 1) return "just now";
|
|
183
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
184
|
+
const hours = Math.floor(minutes / 60);
|
|
185
|
+
if (hours < 24) return `${hours}h ago`;
|
|
186
|
+
const days = Math.floor(hours / 24);
|
|
187
|
+
return `${days}d ago`;
|
|
188
|
+
}
|