loom-agent 1.2.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/.env.example +25 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/LOOM.md +235 -0
- package/README.md +433 -0
- package/bin/loom-tui.js +43 -0
- package/bin/loom.js +44 -0
- package/docs/acp.md +151 -0
- package/docs/web.md +205 -0
- package/package.json +97 -0
- package/scripts/acp-smoke.js +146 -0
- package/src/acp/acp-server.js +287 -0
- package/src/config/provider-cmd.js +37 -0
- package/src/config/settings.js +164 -0
- package/src/core/agents.js +361 -0
- package/src/core/background-tasks.js +103 -0
- package/src/core/cli.js +579 -0
- package/src/core/custom-commands.js +70 -0
- package/src/core/errors.js +29 -0
- package/src/core/events.js +24 -0
- package/src/core/file-diffs.js +282 -0
- package/src/core/format.js +206 -0
- package/src/core/graph.js +257 -0
- package/src/core/hooks.js +82 -0
- package/src/core/lsp.js +385 -0
- package/src/core/memory.js +87 -0
- package/src/core/model-router.js +87 -0
- package/src/core/permissions.js +327 -0
- package/src/core/platform.js +33 -0
- package/src/core/plugin-cmd.js +380 -0
- package/src/core/restore.js +207 -0
- package/src/core/session-store.js +167 -0
- package/src/core/session.js +910 -0
- package/src/core/subagent-log.js +134 -0
- package/src/core/tokens.js +31 -0
- package/src/core/update.js +6 -0
- package/src/core/usage.js +166 -0
- package/src/index.js +41 -0
- package/src/mcp/mcp-client.js +201 -0
- package/src/mcp/mcp-manager.js +193 -0
- package/src/providers/anthropic.js +243 -0
- package/src/providers/google.js +29 -0
- package/src/providers/index.js +175 -0
- package/src/providers/local.js +27 -0
- package/src/providers/nvidia.js +85 -0
- package/src/providers/openai-compat.js +269 -0
- package/src/providers/openai.js +35 -0
- package/src/providers/openrouter.js +43 -0
- package/src/providers/registry.js +196 -0
- package/src/providers/tokenrouter.js +19 -0
- package/src/skills/skill-matcher.js +133 -0
- package/src/skills/skills-manager.js +213 -0
- package/src/tools/index.js +543 -0
- package/src/tui/App.tsx +1578 -0
- package/src/tui/components/BreadcrumbBar.tsx +34 -0
- package/src/tui/components/ChatArea.tsx +518 -0
- package/src/tui/components/InputBar.tsx +354 -0
- package/src/tui/components/MdText.tsx +105 -0
- package/src/tui/components/Modals.tsx +851 -0
- package/src/tui/components/PermissionPopup.tsx +264 -0
- package/src/tui/components/Sidebar.tsx +182 -0
- package/src/tui/components/SplashScreen.tsx +51 -0
- package/src/tui/components/SubagentPanel.tsx +217 -0
- package/src/tui/components/ToastOverlay.tsx +34 -0
- package/src/tui/keybinds.ts +318 -0
- package/src/tui/mcp-presets.ts +189 -0
- package/src/tui/md-render.ts +228 -0
- package/src/tui/store.ts +714 -0
- package/src/tui/suite-home.ts +20 -0
- package/src/tui/theme.ts +313 -0
- package/src/tui/themes.generated.ts +968 -0
- package/src/tui/tool-display.ts +176 -0
- package/src/tui/toolname.ts +60 -0
- package/src/tui/tui-config.ts +28 -0
- package/src/tui-open.tsx +51 -0
- package/src/web/attach.js +242 -0
- package/src/web/graph-view.html +262 -0
- package/src/web/index.html +824 -0
- package/src/web/web-server.js +470 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const SESSION_SCHEMA_VERSION = 2;
|
|
6
|
+
|
|
7
|
+
const LOOM_DIR = path.join(os.homedir(), '.loom');
|
|
8
|
+
const SESSIONS_DIR = path.join(LOOM_DIR, 'sessions');
|
|
9
|
+
|
|
10
|
+
// Re-evaluated on every call so tests can isolate with LOOM_CONFIG_DIR.
|
|
11
|
+
function sessionsDir() {
|
|
12
|
+
return path.join(process.env.LOOM_CONFIG_DIR || LOOM_DIR, 'sessions');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
const d = sessionsDir();
|
|
17
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function convId() {
|
|
21
|
+
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Coerce whatever came off disk (hand-edited, corrupt, older schema) into the
|
|
25
|
+
// guaranteed current shape: { id, createdAt, schemaVersion, messages } with
|
|
26
|
+
// every message having { role, content } while preserving any extra fields.
|
|
27
|
+
function normalizeSession(data) {
|
|
28
|
+
if (!data || typeof data !== 'object') return null;
|
|
29
|
+
const raw = Array.isArray(data.messages) ? data.messages : [];
|
|
30
|
+
const messages = raw.map((m) => {
|
|
31
|
+
if (!m || typeof m !== 'object') return { role: 'user', content: '' };
|
|
32
|
+
const { role, content, ...rest } = m;
|
|
33
|
+
return { role: role || 'user', content: typeof content === 'undefined' ? '' : content, ...rest };
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
id: typeof data.id === 'string' ? data.id : null,
|
|
37
|
+
createdAt: typeof data.createdAt === 'string' ? data.createdAt : null,
|
|
38
|
+
schemaVersion: typeof data.schemaVersion === 'number' ? data.schemaVersion : 1,
|
|
39
|
+
messages,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Session IDs become file names — keep them strictly alphanumeric to rule out
|
|
44
|
+
// path traversal via loadSession/deleteSession (e.g. "../../secrets").
|
|
45
|
+
const ID_RX = /^[a-zA-Z0-9_-]{3,128}$/;
|
|
46
|
+
function isValidSessionId(id) {
|
|
47
|
+
return typeof id === 'string' && ID_RX.test(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveSession(session) {
|
|
51
|
+
ensureDir();
|
|
52
|
+
const id = isValidSessionId(session.conversationId) ? session.conversationId : convId();
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
// Overwriting an existing session keeps its original createdAt (it is the
|
|
55
|
+
// same conversation); updatedAt always reflects this write.
|
|
56
|
+
let createdAt = now;
|
|
57
|
+
const existingFile = path.join(sessionsDir(), id + '.json');
|
|
58
|
+
if (fs.existsSync(existingFile)) {
|
|
59
|
+
try {
|
|
60
|
+
const prev = JSON.parse(fs.readFileSync(existingFile, 'utf8'));
|
|
61
|
+
if (prev && typeof prev.createdAt === 'string') createdAt = prev.createdAt;
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
const data = {
|
|
65
|
+
id,
|
|
66
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
67
|
+
createdAt,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
messages: session.messages || [],
|
|
70
|
+
provider: session.config ? session.config.provider : null,
|
|
71
|
+
model: session.config && session.config.model ? session.config.model[session.config.provider] : null,
|
|
72
|
+
};
|
|
73
|
+
const f = path.join(sessionsDir(), id + '.json');
|
|
74
|
+
fs.writeFileSync(f, JSON.stringify(data, null, 2));
|
|
75
|
+
return { id, file: f };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listSessions() {
|
|
79
|
+
ensureDir();
|
|
80
|
+
const files = fs.readdirSync(sessionsDir()).filter((f) => f.endsWith('.json'));
|
|
81
|
+
const list = files.map((f) => {
|
|
82
|
+
const p = path.join(sessionsDir(), f);
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
85
|
+
let mtime = null;
|
|
86
|
+
try { mtime = fs.statSync(p).mtime.toISOString(); } catch {}
|
|
87
|
+
return {
|
|
88
|
+
id: data.id || f.replace('.json', ''),
|
|
89
|
+
createdAt: data.createdAt,
|
|
90
|
+
updatedAt: data.updatedAt,
|
|
91
|
+
mtime,
|
|
92
|
+
messageCount: (data.messages || []).length,
|
|
93
|
+
provider: data.provider,
|
|
94
|
+
model: data.model,
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
let mtime = null;
|
|
98
|
+
try { mtime = fs.statSync(p).mtime.toISOString(); } catch {}
|
|
99
|
+
return { id: f.replace('.json', ''), createdAt: null, updatedAt: null, mtime, messageCount: 0 };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Newest first by the last write time (updatedAt, else createdAt, else the
|
|
103
|
+
// file's own mtime). Sorting by file NAME put legacy "share-*" exports on
|
|
104
|
+
// top forever ("s" > "m"), burying the real, recent conversations.
|
|
105
|
+
const stamp = (s) => s.updatedAt || s.createdAt || s.mtime || '';
|
|
106
|
+
return list.filter((s) => isValidSessionId(s.id)).sort((a, b) => (stamp(b) < stamp(a) ? -1 : stamp(b) > stamp(a) ? 1 : 0));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadSession(id) {
|
|
110
|
+
if (!isValidSessionId(id)) return null;
|
|
111
|
+
const f = path.join(sessionsDir(), id + '.json');
|
|
112
|
+
if (!fs.existsSync(f)) return null;
|
|
113
|
+
try {
|
|
114
|
+
const raw = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
115
|
+
const norm = normalizeSession(raw);
|
|
116
|
+
if (!norm) return null;
|
|
117
|
+
norm.id = norm.id || id;
|
|
118
|
+
norm.file = f;
|
|
119
|
+
return norm;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function deleteSession(id) {
|
|
126
|
+
if (!isValidSessionId(id)) return false;
|
|
127
|
+
const f = path.join(sessionsDir(), id + '.json');
|
|
128
|
+
if (fs.existsSync(f)) {
|
|
129
|
+
fs.unlinkSync(f);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exportChat(session, format) {
|
|
136
|
+
const type = format || 'md';
|
|
137
|
+
const msgs = session.messages || [];
|
|
138
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
139
|
+
const file = path.join(process.cwd(), 'loom-chat-' + ts + '.' + type);
|
|
140
|
+
if (type === 'md') {
|
|
141
|
+
const lines = ['# Loom Code Chat Export', '', '**Date:** ' + new Date().toLocaleString(), ''];
|
|
142
|
+
for (const m of msgs) {
|
|
143
|
+
const role = m.role || 'unknown';
|
|
144
|
+
lines.push('### ' + role.charAt(0).toUpperCase() + role.slice(1));
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(m.content || '(empty)');
|
|
147
|
+
lines.push('');
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(file, lines.join('\n'));
|
|
150
|
+
} else {
|
|
151
|
+
const exportLines = msgs.map((m) => m.role + ': ' + (m.content || ''));
|
|
152
|
+
fs.writeFileSync(file, exportLines.join('\n'));
|
|
153
|
+
}
|
|
154
|
+
return file;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
saveSession,
|
|
159
|
+
listSessions,
|
|
160
|
+
loadSession,
|
|
161
|
+
deleteSession,
|
|
162
|
+
exportChat,
|
|
163
|
+
normalizeSession,
|
|
164
|
+
isValidSessionId,
|
|
165
|
+
SESSIONS_DIR,
|
|
166
|
+
SESSION_SCHEMA_VERSION,
|
|
167
|
+
};
|