Add initial project files and configurations for clawd_code
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "session_types.dart";
|
||||
|
||||
// in-memory conversation history for the current session
|
||||
// does not auto-persist - caller has to call SessionStore.saveSession
|
||||
class ConversationHistory {
|
||||
ConversationHistory({ConversationSession? session}) : _session = session;
|
||||
|
||||
ConversationSession? _session;
|
||||
|
||||
ConversationSession? get session => _session;
|
||||
|
||||
bool get hasSession => _session != null;
|
||||
|
||||
List<Message> getMessages() {
|
||||
return _session?.messages ?? <Message>[];
|
||||
}
|
||||
|
||||
void setSession(ConversationSession s) {
|
||||
_session = s;
|
||||
}
|
||||
|
||||
void addMessage(String role, String content, {int? tokens}) {
|
||||
if (_session == null) return;
|
||||
|
||||
final msg = Message(role: role, content: content, tokens: tokens);
|
||||
|
||||
_session!.messages.add(msg);
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
void removeLastMessage() {
|
||||
if (_session == null || _session!.messages.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_session!.messages.removeLast();
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
void truncateMessages(int length) {
|
||||
if (_session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final targetLength = length.clamp(0, _session!.messages.length);
|
||||
_session!.messages.removeRange(targetLength, _session!.messages.length);
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
// remove all messages but keep the session metadata
|
||||
void clear() {
|
||||
if (_session == null) return;
|
||||
_session!.messages.clear();
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
// export as plain text - one [role]: content block per message
|
||||
String exportToText() {
|
||||
final buf = StringBuffer();
|
||||
|
||||
if (_session != null) {
|
||||
buf.writeln("Session: ${_session!.name}");
|
||||
buf.writeln("ID: ${_session!.id}");
|
||||
buf.writeln("Created: ${_session!.created.toIso8601String()}");
|
||||
buf.writeln("Messages: ${_session!.messageCount}");
|
||||
buf.writeln("---");
|
||||
buf.writeln();
|
||||
}
|
||||
|
||||
for (final msg in getMessages()) {
|
||||
buf.writeln("[${msg.role}]");
|
||||
buf.writeln(msg.content);
|
||||
buf.writeln();
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
String exportToJson() {
|
||||
if (_session == null) {
|
||||
return const JsonEncoder.withIndent(
|
||||
" ",
|
||||
).convert(<String, dynamic>{"messages": <dynamic>[]});
|
||||
}
|
||||
|
||||
return const JsonEncoder.withIndent(" ").convert(_session!.toJson());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import "../local_state.dart";
|
||||
import "session_types.dart";
|
||||
|
||||
const _encoder = JsonEncoder.withIndent(" ");
|
||||
|
||||
// sessions live in ~/.clawd_code/sessions/{id}.json
|
||||
String getSessionsDir() {
|
||||
return joinPath(getConfigHomeDir(), "sessions");
|
||||
}
|
||||
|
||||
String _sessionPath(String id) {
|
||||
return joinPath(getSessionsDir(), "$id.json");
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
SessionStore._();
|
||||
|
||||
static final SessionStore instance = SessionStore._();
|
||||
|
||||
|
||||
Future<void> saveSession(ConversationSession session) async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
final file = File(_sessionPath(session.id));
|
||||
final json = _encoder.convert(session.toJson());
|
||||
await file.writeAsString("$json\n");
|
||||
}
|
||||
|
||||
Future<ConversationSession?> loadSession(String id) async {
|
||||
final file = File(_sessionPath(id));
|
||||
if (!await file.exists()) return null;
|
||||
|
||||
try {
|
||||
final raw = await file.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return ConversationSession.fromJson(decoded);
|
||||
}
|
||||
} catch (_) {
|
||||
// corrupt file - just return null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// returns summaries sorted newest first
|
||||
Future<List<SessionSummary>> listSessions() async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
if (!await dir.exists()) return <SessionSummary>[];
|
||||
|
||||
final summaries = <SessionSummary>[];
|
||||
|
||||
await for (final entity in dir.list()) {
|
||||
if (entity is! File) continue;
|
||||
if (!entity.path.endsWith(".json")) continue;
|
||||
|
||||
try {
|
||||
final raw = await entity.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final sess = ConversationSession.fromJson(decoded);
|
||||
summaries.add(SessionSummary.fromSession(sess));
|
||||
}
|
||||
} catch (_) {
|
||||
// skip unreadable files
|
||||
}
|
||||
}
|
||||
|
||||
summaries.sort((a, b) => b.updated.compareTo(a.updated));
|
||||
return summaries;
|
||||
}
|
||||
|
||||
Future<bool> deleteSession(String id) async {
|
||||
final file = File(_sessionPath(id));
|
||||
if (!await file.exists()) return false;
|
||||
|
||||
await file.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// case insensitive search by name
|
||||
Future<ConversationSession?> findSessionByName(String name) async {
|
||||
final dir = Directory(getSessionsDir());
|
||||
if (!await dir.exists()) return null;
|
||||
|
||||
final lowerName = name.toLowerCase();
|
||||
|
||||
await for (final entity in dir.list()) {
|
||||
if (entity is! File) continue;
|
||||
if (!entity.path.endsWith(".json")) continue;
|
||||
|
||||
try {
|
||||
final raw = await entity.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final sess = ConversationSession.fromJson(decoded);
|
||||
if (sess.name.toLowerCase() == lowerName) {
|
||||
return sess;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// message roles - same as what the API uses
|
||||
const validRoles = <String>["user", "assistant", "system", "tool"];
|
||||
|
||||
class Message {
|
||||
Message({
|
||||
required this.role,
|
||||
required this.content,
|
||||
DateTime? timestamp,
|
||||
this.tokens,
|
||||
}) : timestamp = timestamp ?? DateTime.now().toUtc();
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) {
|
||||
return Message(
|
||||
role: json["role"] as String,
|
||||
content: json["content"] as String,
|
||||
timestamp: json["timestamp"] != null
|
||||
? DateTime.tryParse(json["timestamp"] as String)
|
||||
: null,
|
||||
tokens: json["tokens"] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
final String role;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
|
||||
// approx token count - may be null if not tracked
|
||||
final int? tokens;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": timestamp.toIso8601String(),
|
||||
if (tokens != null) "tokens": tokens,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "[$role] $content";
|
||||
}
|
||||
|
||||
class ConversationSession {
|
||||
ConversationSession({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.created,
|
||||
required this.updated,
|
||||
List<Message>? messages,
|
||||
this.cost,
|
||||
this.model,
|
||||
this.workingDirectory,
|
||||
}) : messages = messages ?? <Message>[];
|
||||
|
||||
factory ConversationSession.fromJson(Map<String, dynamic> json) {
|
||||
final rawMessages = json["messages"];
|
||||
final msgs = <Message>[];
|
||||
if (rawMessages is List) {
|
||||
for (final m in rawMessages) {
|
||||
if (m is Map<String, dynamic>) {
|
||||
msgs.add(Message.fromJson(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConversationSession(
|
||||
id: json["id"] as String,
|
||||
name: json["name"] as String? ?? "Unnamed Session",
|
||||
created:
|
||||
DateTime.tryParse(json["created"] as String? ?? "") ??
|
||||
DateTime.now().toUtc(),
|
||||
updated:
|
||||
DateTime.tryParse(json["updated"] as String? ?? "") ??
|
||||
DateTime.now().toUtc(),
|
||||
messages: msgs,
|
||||
cost: (json["cost"] as num?)?.toDouble(),
|
||||
model: json["model"] as String?,
|
||||
workingDirectory: json["workingDirectory"] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
String name;
|
||||
final DateTime created;
|
||||
DateTime updated;
|
||||
final List<Message> messages;
|
||||
|
||||
// total cost in USD - optional
|
||||
double? cost;
|
||||
String? model;
|
||||
String? workingDirectory;
|
||||
|
||||
int get messageCount => messages.length;
|
||||
|
||||
// rough token total from tracked messages
|
||||
int get totalTokens {
|
||||
int t = 0;
|
||||
for (final m in messages) {
|
||||
t += m.tokens ?? 0;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
ConversationSession copyWith({
|
||||
String? name,
|
||||
DateTime? updated,
|
||||
List<Message>? messages,
|
||||
double? cost,
|
||||
String? model,
|
||||
Object? workingDirectory = _sessionSentinel,
|
||||
}) {
|
||||
return ConversationSession(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
created: created,
|
||||
updated: updated ?? this.updated,
|
||||
messages: messages ?? this.messages,
|
||||
cost: cost ?? this.cost,
|
||||
model: model ?? this.model,
|
||||
workingDirectory: identical(workingDirectory, _sessionSentinel)
|
||||
? this.workingDirectory
|
||||
: workingDirectory as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"created": created.toIso8601String(),
|
||||
"updated": updated.toIso8601String(),
|
||||
"messages": messages.map((m) => m.toJson()).toList(),
|
||||
if (cost != null) "cost": cost,
|
||||
if (model != null) "model": model,
|
||||
if (workingDirectory != null) "workingDirectory": workingDirectory,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// lightweight summary for listing - no messages loaded
|
||||
class SessionSummary {
|
||||
const SessionSummary({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.created,
|
||||
required this.updated,
|
||||
required this.messageCount,
|
||||
this.cost,
|
||||
this.model,
|
||||
this.workingDirectory,
|
||||
});
|
||||
|
||||
factory SessionSummary.fromJson(Map<String, dynamic> json) {
|
||||
return SessionSummary(
|
||||
id: json["id"] as String,
|
||||
name: json["name"] as String? ?? "Unnamed Session",
|
||||
created:
|
||||
DateTime.tryParse(json["created"] as String? ?? "") ??
|
||||
DateTime.now().toUtc(),
|
||||
updated:
|
||||
DateTime.tryParse(json["updated"] as String? ?? "") ??
|
||||
DateTime.now().toUtc(),
|
||||
messageCount: json["messageCount"] as int? ?? 0,
|
||||
cost: (json["cost"] as num?)?.toDouble(),
|
||||
model: json["model"] as String?,
|
||||
workingDirectory: json["workingDirectory"] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime created;
|
||||
final DateTime updated;
|
||||
final int messageCount;
|
||||
final double? cost;
|
||||
final String? model;
|
||||
final String? workingDirectory;
|
||||
|
||||
static SessionSummary fromSession(ConversationSession s) {
|
||||
return SessionSummary(
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
created: s.created,
|
||||
updated: s.updated,
|
||||
messageCount: s.messageCount,
|
||||
cost: s.cost,
|
||||
model: s.model,
|
||||
workingDirectory: s.workingDirectory,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"created": created.toIso8601String(),
|
||||
"updated": updated.toIso8601String(),
|
||||
"messageCount": messageCount,
|
||||
if (cost != null) "cost": cost,
|
||||
if (model != null) "model": model,
|
||||
if (workingDirectory != null) "workingDirectory": workingDirectory,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const Object _sessionSentinel = Object();
|
||||
Reference in New Issue
Block a user