@saccolabs/tars 1.0.33 → 1.0.35
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/context/agents/scaffolder.md +7 -7
- package/context/config/settings.json-template +12 -10
- package/dist/discord/discord-bot.js +1 -1
- package/dist/discord/message-formatter.js +40 -6
- package/dist/discord/message-formatter.js.map +1 -1
- package/dist/supervisor/gemini-cli.d.ts +0 -11
- package/dist/supervisor/gemini-cli.js +0 -154
- package/dist/supervisor/gemini-cli.js.map +1 -1
- package/dist/supervisor/heartbeat-service.d.ts +14 -1
- package/dist/supervisor/heartbeat-service.js +48 -16
- package/dist/supervisor/heartbeat-service.js.map +1 -1
- package/dist/supervisor/main.js +20 -23
- package/dist/supervisor/main.js.map +1 -1
- package/dist/supervisor/session-manager.d.ts +16 -1
- package/dist/supervisor/session-manager.js +43 -1
- package/dist/supervisor/session-manager.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +4 -6
- package/dist/supervisor/supervisor.js +24 -29
- package/dist/supervisor/supervisor.js.map +1 -1
- package/extensions/memory/dist/server.d.ts +1 -0
- package/extensions/memory/dist/server.js +190 -0
- package/extensions/memory/dist/server.js.map +1 -0
- package/extensions/memory/dist/store.d.ts +25 -0
- package/extensions/memory/dist/store.js +128 -0
- package/extensions/memory/dist/store.js.map +1 -0
- package/extensions/memory/gemini-extension.json +14 -0
- package/extensions/memory/package-lock.json +1173 -0
- package/extensions/memory/package.json +17 -0
- package/extensions/memory/src/server.ts +210 -0
- package/extensions/memory/src/store.ts +161 -0
- package/extensions/memory/tsconfig.json +14 -0
- package/package.json +2 -2
- package/src/prompts/system.md +7 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tars-memory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Memory management MCP server for Tars",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.0.1"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^22.10.2",
|
|
15
|
+
"typescript": "^5.7.2"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { MemoryStore } from './store.js';
|
|
5
|
+
|
|
6
|
+
const store = new MemoryStore();
|
|
7
|
+
const server = new Server(
|
|
8
|
+
{
|
|
9
|
+
name: 'tars-memory',
|
|
10
|
+
version: '1.0.0'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tool Definitions
|
|
21
|
+
*/
|
|
22
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
23
|
+
return {
|
|
24
|
+
tools: [
|
|
25
|
+
{
|
|
26
|
+
name: 'memory_store_fact',
|
|
27
|
+
description:
|
|
28
|
+
'Store or overwrite a core fact about the user. Facts are key-value pairs that persist across sessions. Use for preferences, identity, rules, and durable information. If the key already exists, the value is overwritten (not appended).',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
key: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description:
|
|
35
|
+
'A unique, snake_case identifier for the fact (e.g. "favorite_color", "employer", "timezone")'
|
|
36
|
+
},
|
|
37
|
+
value: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'The value of the fact'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ['key', 'value']
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'memory_delete_fact',
|
|
47
|
+
description: 'Delete a stored fact by its key.',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
key: { type: 'string', description: 'The key of the fact to delete' }
|
|
52
|
+
},
|
|
53
|
+
required: ['key']
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'memory_list_facts',
|
|
58
|
+
description:
|
|
59
|
+
'List all stored core facts. Use this to review what the user has asked you to remember.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'memory_add_note',
|
|
67
|
+
description:
|
|
68
|
+
"Append a timestamped note to today's daily log. Use for project context, decisions, observations, and anything that does not need to be loaded into every session. Notes are searchable but not injected into the main context window.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
content: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'The note content to record'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ['content']
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'memory_search',
|
|
82
|
+
description:
|
|
83
|
+
'Search across all stored facts and daily notes by keyword. Returns matching entries from both long-term facts and short-term daily notes.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
query: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'The search query (keyword match)'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['query']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Tool Handlers
|
|
101
|
+
*/
|
|
102
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
103
|
+
const { name, arguments: args } = request.params;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
switch (name) {
|
|
107
|
+
case 'memory_store_fact': {
|
|
108
|
+
const { key, value } = args as any;
|
|
109
|
+
if (!key || !value) throw new Error('Both key and value are required.');
|
|
110
|
+
|
|
111
|
+
const fact = await store.storeFact(key.trim(), value.trim());
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: `✅ Stored fact: "${fact.key}" = "${fact.value}"`
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'memory_delete_fact': {
|
|
123
|
+
const { key } = args as any;
|
|
124
|
+
const deleted = await store.deleteFact(key);
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: deleted
|
|
130
|
+
? `✅ Deleted fact: "${key}"`
|
|
131
|
+
: `❌ Fact "${key}" not found.`
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'memory_list_facts': {
|
|
138
|
+
const facts = await store.listFacts();
|
|
139
|
+
if (facts.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: 'No facts stored yet.' }]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const text = facts
|
|
146
|
+
.map((f) => `• **${f.key}**: ${f.value} _(updated ${f.updatedAt})_`)
|
|
147
|
+
.join('\n');
|
|
148
|
+
|
|
149
|
+
return { content: [{ type: 'text', text }] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'memory_add_note': {
|
|
153
|
+
const { content } = args as any;
|
|
154
|
+
if (!content) throw new Error('Content is required.');
|
|
155
|
+
|
|
156
|
+
const fileName = await store.addNote(content.trim());
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: `✅ Note added to ${fileName}`
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'memory_search': {
|
|
168
|
+
const { query } = args as any;
|
|
169
|
+
if (!query) throw new Error('Query is required.');
|
|
170
|
+
|
|
171
|
+
const results = await store.search(query.trim());
|
|
172
|
+
if (results.length === 0) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: 'text', text: `No results found for "${query}".` }]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const text = results.map((r) => `• ${r}`).join('\n');
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: `Found ${results.length} result(s):\n${text}`
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
default:
|
|
190
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
191
|
+
}
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
|
|
195
|
+
isError: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Start Server
|
|
201
|
+
async function main() {
|
|
202
|
+
const transport = new StdioServerTransport();
|
|
203
|
+
await server.connect(transport);
|
|
204
|
+
console.error('tars-memory MCP server running on stdio');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
main().catch((error) => {
|
|
208
|
+
console.error('Server error:', error);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
// ── Types ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface Fact {
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FactsFile {
|
|
14
|
+
facts: Record<string, Fact>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Store ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export class MemoryStore {
|
|
20
|
+
private readonly dataDir: string;
|
|
21
|
+
private readonly factsPath: string;
|
|
22
|
+
private readonly notesDir: string;
|
|
23
|
+
private lock: Promise<void> = Promise.resolve();
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
const tarsHome = process.env.TARS_HOME || path.join(os.homedir(), '.tars');
|
|
27
|
+
this.dataDir = path.join(tarsHome, 'data', 'memory');
|
|
28
|
+
this.factsPath = path.join(this.dataDir, 'facts.json');
|
|
29
|
+
this.notesDir = path.join(this.dataDir, 'notes');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async withLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
33
|
+
const result = this.lock.then(fn);
|
|
34
|
+
this.lock = result.then(
|
|
35
|
+
() => {},
|
|
36
|
+
() => {}
|
|
37
|
+
);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Facts (Key-Value) ─────────────────────────────
|
|
42
|
+
|
|
43
|
+
private async _loadFacts(): Promise<FactsFile> {
|
|
44
|
+
try {
|
|
45
|
+
const data = await fs.readFile(this.factsPath, 'utf-8');
|
|
46
|
+
return JSON.parse(data);
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
if (error.code === 'ENOENT') {
|
|
49
|
+
return { facts: {} };
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async _saveFacts(file: FactsFile): Promise<void> {
|
|
56
|
+
await fs.mkdir(path.dirname(this.factsPath), { recursive: true });
|
|
57
|
+
await fs.writeFile(this.factsPath, JSON.stringify(file, null, 2), 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async storeFact(key: string, value: string): Promise<Fact> {
|
|
61
|
+
return this.withLock(async () => {
|
|
62
|
+
const file = await this._loadFacts();
|
|
63
|
+
const fact: Fact = {
|
|
64
|
+
key,
|
|
65
|
+
value,
|
|
66
|
+
updatedAt: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
file.facts[key] = fact;
|
|
69
|
+
await this._saveFacts(file);
|
|
70
|
+
return fact;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async deleteFact(key: string): Promise<boolean> {
|
|
75
|
+
return this.withLock(async () => {
|
|
76
|
+
const file = await this._loadFacts();
|
|
77
|
+
if (!(key in file.facts)) return false;
|
|
78
|
+
delete file.facts[key];
|
|
79
|
+
await this._saveFacts(file);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async listFacts(): Promise<Fact[]> {
|
|
85
|
+
return this.withLock(async () => {
|
|
86
|
+
const file = await this._loadFacts();
|
|
87
|
+
return Object.values(file.facts);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Notes (Daily Append-Only) ─────────────────────
|
|
92
|
+
|
|
93
|
+
private getTodayFileName(): string {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const yyyy = now.getFullYear();
|
|
96
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
97
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
98
|
+
return `${yyyy}-${mm}-${dd}.md`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private getTimestamp(): string {
|
|
102
|
+
const now = new Date();
|
|
103
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
104
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
105
|
+
return `${hh}:${mm}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public async addNote(content: string): Promise<string> {
|
|
109
|
+
await fs.mkdir(this.notesDir, { recursive: true });
|
|
110
|
+
const fileName = this.getTodayFileName();
|
|
111
|
+
const filePath = path.join(this.notesDir, fileName);
|
|
112
|
+
const entry = `- [${this.getTimestamp()}] ${content}\n`;
|
|
113
|
+
await fs.appendFile(filePath, entry, 'utf-8');
|
|
114
|
+
return fileName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Search ────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
public async search(query: string): Promise<string[]> {
|
|
120
|
+
const results: string[] = [];
|
|
121
|
+
const queryLower = query.toLowerCase();
|
|
122
|
+
|
|
123
|
+
// Search facts
|
|
124
|
+
const file = await this._loadFacts();
|
|
125
|
+
for (const fact of Object.values(file.facts)) {
|
|
126
|
+
if (
|
|
127
|
+
fact.key.toLowerCase().includes(queryLower) ||
|
|
128
|
+
fact.value.toLowerCase().includes(queryLower)
|
|
129
|
+
) {
|
|
130
|
+
results.push(`[Fact] ${fact.key}: ${fact.value}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Search notes
|
|
135
|
+
try {
|
|
136
|
+
const noteFiles = await fs.readdir(this.notesDir);
|
|
137
|
+
const mdFiles = noteFiles
|
|
138
|
+
.filter((f) => f.endsWith('.md'))
|
|
139
|
+
.sort()
|
|
140
|
+
.reverse();
|
|
141
|
+
|
|
142
|
+
// Search last 30 days of notes
|
|
143
|
+
for (const noteFile of mdFiles.slice(0, 30)) {
|
|
144
|
+
const filePath = path.join(this.notesDir, noteFile);
|
|
145
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
146
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
147
|
+
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (line.toLowerCase().includes(queryLower)) {
|
|
150
|
+
results.push(`[Note ${noteFile}] ${line.replace(/^- /, '')}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
// Notes dir may not exist yet
|
|
156
|
+
if (error.code !== 'ENOENT') throw error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"sourceMap": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
package/package.json
CHANGED
package/src/prompts/system.md
CHANGED
|
@@ -5,13 +5,18 @@ You are **Tars**, a personal AI assistant. You are autonomous, proactive, and ca
|
|
|
5
5
|
## Core Directives
|
|
6
6
|
|
|
7
7
|
1. **Be Helpful & Efficient**: Save the user time. Provide accurate, useful info.
|
|
8
|
-
2. **Be Adaptable**: Adjust your tone, style, and approach based on the user's current request and preferences stored in memory (
|
|
8
|
+
2. **Be Adaptable**: Adjust your tone, style, and approach based on the user's current request and preferences stored in memory (via the `tars-memory` extension).
|
|
9
9
|
3. **Be Proactive**: Suggest follow-ups or improvements when relevant.
|
|
10
10
|
4. **Be Secure**: Never expose secrets or sensitive info.
|
|
11
|
+
5. **Be Concise**: Never explain basic concepts unless asked. Do not output walls of text. Provide only the direct answer, the code needed, or a brief summary of actions. Assume the user is reading your response on a mobile device and hates scrolling.
|
|
11
12
|
|
|
12
13
|
## Operational Rules
|
|
13
14
|
|
|
14
|
-
- **Memory**:
|
|
15
|
+
- **Memory**: Use the `tars-memory` MCP tools for all memory operations:
|
|
16
|
+
- `memory_store_fact` / `memory_delete_fact` / `memory_list_facts` for core preferences, identity facts, and durable rules.
|
|
17
|
+
- `memory_add_note` for daily observations, project decisions, and ephemeral context.
|
|
18
|
+
- `memory_search` to recall past facts and notes.
|
|
19
|
+
- Do NOT use `save_memory` or write to `GEMINI.md` directly.
|
|
15
20
|
- **Safety**: Do **NOT** run `gemini` CLI commands or manage the `tars` supervisor process (start/stop) directly. Use internal tools or config files.
|
|
16
21
|
- **Tools**: Use absolute file paths. Maximize parallelism and tool usage. Use background processes (`&`) for long-running shell commands.
|
|
17
22
|
|