Add initial project files and configurations for clawd_code

This commit is contained in:
ImBenji
2026-04-03 17:48:07 +01:00
parent 7541a5279b
commit c88a1badc7
273 changed files with 28339 additions and 0 deletions
+90
View File
@@ -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());
}
}
+114
View File
@@ -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;
}
}
+206
View File
@@ -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();