@jellyos/agent 0.1.4 → 0.1.5

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.
Files changed (94) hide show
  1. package/README.npm.md +212 -0
  2. package/bin/jellyos-mcp +26 -0
  3. package/dist/api/ExtensionAPI.d.ts +6 -0
  4. package/dist/cli.js +114 -48
  5. package/dist/index.d.ts +15 -2
  6. package/dist/index.js +13 -3
  7. package/dist/mcp/entry.d.ts +2 -0
  8. package/dist/mcp/entry.js +71 -0
  9. package/dist/mcp/server.d.ts +31 -0
  10. package/dist/mcp/server.js +128 -0
  11. package/dist/models/ModelRegistry.d.ts +12 -1
  12. package/dist/models/ModelRegistry.js +105 -9
  13. package/dist/runner/AgentRunner.d.ts +19 -2
  14. package/dist/runner/AgentRunner.js +247 -17
  15. package/dist/runner/ModelClient.d.ts +10 -1
  16. package/dist/runner/ModelClient.js +79 -6
  17. package/dist/runner/SwarmRouter.d.ts +6 -6
  18. package/dist/runner/SwarmRouter.js +73 -24
  19. package/dist/runner/ToolDispatcher.d.ts +10 -0
  20. package/dist/runner/ToolDispatcher.js +106 -2
  21. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  22. package/dist/scheduler/AgentScheduler.js +253 -0
  23. package/dist/session/ContextStore.d.ts +96 -0
  24. package/dist/session/ContextStore.js +207 -0
  25. package/dist/session/GoalManager.d.ts +101 -0
  26. package/dist/session/GoalManager.js +167 -0
  27. package/dist/session/MemoryStore.d.ts +48 -0
  28. package/dist/session/MemoryStore.js +166 -0
  29. package/dist/session/SessionManager.d.ts +45 -4
  30. package/dist/session/SessionManager.js +151 -8
  31. package/dist/telemetry/Tracer.d.ts +48 -0
  32. package/dist/telemetry/Tracer.js +102 -0
  33. package/dist/tests/ContextStore.test.d.ts +2 -0
  34. package/dist/tests/ContextStore.test.js +74 -0
  35. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  36. package/dist/tests/ModelRegistry.test.js +69 -0
  37. package/dist/tests/SessionManager.test.d.ts +2 -0
  38. package/dist/tests/SessionManager.test.js +108 -0
  39. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  40. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  41. package/dist/tools/MarketSentiment.d.ts +166 -0
  42. package/dist/tools/MarketSentiment.js +209 -0
  43. package/dist/tools/NewsSentiment.js +40 -13
  44. package/dist/tools/PriceFeed.d.ts +2 -0
  45. package/dist/tools/PriceFeed.js +79 -27
  46. package/dist/tools/TechnicalAnalysis.d.ts +37 -0
  47. package/dist/tools/TechnicalAnalysis.js +85 -0
  48. package/dist/tui/App.d.ts +2 -2
  49. package/dist/tui/App.js +280 -117
  50. package/dist/tui/REPL.d.ts +2 -1
  51. package/dist/tui/REPL.js +11 -6
  52. package/package.json +9 -4
  53. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  54. package/dist/api/ExtensionAPI.js.map +0 -1
  55. package/dist/api/Registry.d.ts.map +0 -1
  56. package/dist/api/Registry.js.map +0 -1
  57. package/dist/cli.d.ts.map +0 -1
  58. package/dist/cli.js.map +0 -1
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/loader.d.ts.map +0 -1
  62. package/dist/loader.js.map +0 -1
  63. package/dist/models/CostTracker.d.ts.map +0 -1
  64. package/dist/models/CostTracker.js.map +0 -1
  65. package/dist/models/ModelRegistry.d.ts.map +0 -1
  66. package/dist/models/ModelRegistry.js.map +0 -1
  67. package/dist/models/index.d.ts.map +0 -1
  68. package/dist/models/index.js.map +0 -1
  69. package/dist/runner/AgentRunner.d.ts.map +0 -1
  70. package/dist/runner/AgentRunner.js.map +0 -1
  71. package/dist/runner/ModelClient.d.ts.map +0 -1
  72. package/dist/runner/ModelClient.js.map +0 -1
  73. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  74. package/dist/runner/SwarmRouter.js.map +0 -1
  75. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  76. package/dist/runner/ToolDispatcher.js.map +0 -1
  77. package/dist/session/SessionManager.d.ts.map +0 -1
  78. package/dist/session/SessionManager.js.map +0 -1
  79. package/dist/tools/NewsSentiment.d.ts.map +0 -1
  80. package/dist/tools/NewsSentiment.js.map +0 -1
  81. package/dist/tools/PriceFeed.d.ts.map +0 -1
  82. package/dist/tools/PriceFeed.js.map +0 -1
  83. package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
  84. package/dist/tools/TechnicalAnalysis.js.map +0 -1
  85. package/dist/tools/index.d.ts.map +0 -1
  86. package/dist/tools/index.js.map +0 -1
  87. package/dist/tui/App.d.ts.map +0 -1
  88. package/dist/tui/App.js.map +0 -1
  89. package/dist/tui/REPL.d.ts.map +0 -1
  90. package/dist/tui/REPL.js.map +0 -1
  91. package/dist/tui/StatusBar.d.ts.map +0 -1
  92. package/dist/tui/StatusBar.js.map +0 -1
  93. package/dist/tui/theme.d.ts.map +0 -1
  94. package/dist/tui/theme.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ContextStore — Ephemeral task context folders. (#31, #39)
3
+ *
4
+ * When an agent task takes >2 tool rounds, a ~/.jelly/tasks/<id>/context.md
5
+ * file is created. Intermediate tool results are appended there instead of
6
+ * bloating the message history. The model gets a compact file reference
7
+ * instead of 10KB of raw JSON. The folder auto-deletes on task completion
8
+ * unless the user marked it /keep.
9
+ *
10
+ * This is the primary mechanism that allows turbo/max mode to stay within
11
+ * context budget even on complex multi-step research tasks.
12
+ */
13
+ export interface TaskContext {
14
+ taskId: string;
15
+ taskDir: string;
16
+ contextMd: string;
17
+ createdAt: number;
18
+ title: string;
19
+ keep: boolean;
20
+ findings: number;
21
+ }
22
+ export declare class ContextStore {
23
+ private activeTasks;
24
+ constructor();
25
+ /** Open a new ephemeral task folder. Returns the TaskContext. */
26
+ openTask(title: string, keep?: boolean): TaskContext;
27
+ /** Append a finding / tool result to the task's context.md */
28
+ appendFinding(taskId: string, section: string, content: string): void;
29
+ /**
30
+ * Get a compact reference string for injection into the model context.
31
+ * Returns something like:
32
+ * "[Task ctx: ~/.jelly/tasks/.../context.md — 4 findings, 3.2KB. Use read_task_context("abc123")]"
33
+ */
34
+ getReference(taskId: string): string;
35
+ /**
36
+ * Mark a task complete and optionally delete its folder.
37
+ * Deletion is deferred 5 seconds to allow model to read final state.
38
+ */
39
+ closeTask(taskId: string): void;
40
+ /** Permanently keep a task folder (user called /keep <taskId>) */
41
+ keepTask(taskId: string): boolean;
42
+ getActiveTasks(): TaskContext[];
43
+ getTask(taskId: string): TaskContext | undefined;
44
+ readContextTool(_id: string, { taskId }: {
45
+ taskId: string;
46
+ }): Promise<{
47
+ content: {
48
+ type: "text";
49
+ text: string;
50
+ }[];
51
+ details: {
52
+ taskId: string;
53
+ path: string;
54
+ sizeBytes: number;
55
+ status: string;
56
+ findings?: undefined;
57
+ };
58
+ } | {
59
+ content: {
60
+ type: "text";
61
+ text: string;
62
+ }[];
63
+ details: {
64
+ taskId?: undefined;
65
+ path?: undefined;
66
+ sizeBytes?: undefined;
67
+ status?: undefined;
68
+ findings?: undefined;
69
+ };
70
+ } | {
71
+ content: {
72
+ type: "text";
73
+ text: string;
74
+ }[];
75
+ details: {
76
+ taskId: string;
77
+ path: string;
78
+ sizeBytes: number;
79
+ findings: number;
80
+ status: string;
81
+ };
82
+ }>;
83
+ listTasksTool(): Promise<{
84
+ content: {
85
+ type: "text";
86
+ text: string;
87
+ }[];
88
+ details: {
89
+ activeTasks: number;
90
+ completedOnDisk: number;
91
+ };
92
+ }>;
93
+ }
94
+ /** Singleton — one store per process */
95
+ export declare const contextStore: ContextStore;
96
+ //# sourceMappingURL=ContextStore.d.ts.map
@@ -0,0 +1,207 @@
1
+ /**
2
+ * ContextStore — Ephemeral task context folders. (#31, #39)
3
+ *
4
+ * When an agent task takes >2 tool rounds, a ~/.jelly/tasks/<id>/context.md
5
+ * file is created. Intermediate tool results are appended there instead of
6
+ * bloating the message history. The model gets a compact file reference
7
+ * instead of 10KB of raw JSON. The folder auto-deletes on task completion
8
+ * unless the user marked it /keep.
9
+ *
10
+ * This is the primary mechanism that allows turbo/max mode to stay within
11
+ * context budget even on complex multi-step research tasks.
12
+ */
13
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, readdirSync, } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { randomUUID } from "node:crypto";
17
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
18
+ const TASKS_DIR = join(JELLY_HOME, "tasks");
19
+ export class ContextStore {
20
+ activeTasks = new Map();
21
+ constructor() {
22
+ mkdirSync(TASKS_DIR, { recursive: true });
23
+ }
24
+ // ── Task lifecycle ─────────────────────────────────────────────────────────
25
+ /** Open a new ephemeral task folder. Returns the TaskContext. */
26
+ openTask(title, keep = false) {
27
+ const taskId = randomUUID().slice(0, 8);
28
+ const taskDir = join(TASKS_DIR, `${Date.now()}-${taskId}`);
29
+ const contextMd = join(taskDir, "context.md");
30
+ mkdirSync(taskDir, { recursive: true });
31
+ writeFileSync(contextMd, [
32
+ `# Task: ${title.slice(0, 120)}`,
33
+ `**ID:** ${taskId}`,
34
+ `**Started:** ${new Date().toISOString()}`,
35
+ `**Status:** in_progress`,
36
+ ``,
37
+ `## Findings`,
38
+ ``,
39
+ ].join("\n"), "utf-8");
40
+ const ctx = {
41
+ taskId, taskDir, contextMd,
42
+ createdAt: Date.now(), title, keep, findings: 0,
43
+ };
44
+ this.activeTasks.set(taskId, ctx);
45
+ return ctx;
46
+ }
47
+ /** Append a finding / tool result to the task's context.md */
48
+ appendFinding(taskId, section, content) {
49
+ const ctx = this.activeTasks.get(taskId);
50
+ if (!ctx || !existsSync(ctx.contextMd))
51
+ return;
52
+ // Cap individual entries at 3KB to prevent runaway growth
53
+ const cappedContent = content.length > 3000
54
+ ? content.slice(0, 3000) + `\n\n…[truncated ${content.length - 3000} chars]`
55
+ : content;
56
+ const entry = [
57
+ `### ${section}`,
58
+ `_${new Date().toLocaleTimeString()}_`,
59
+ ``,
60
+ cappedContent,
61
+ ``,
62
+ `---`,
63
+ ``,
64
+ ].join("\n");
65
+ const existing = readFileSync(ctx.contextMd, "utf-8");
66
+ writeFileSync(ctx.contextMd, existing + entry, "utf-8");
67
+ ctx.findings++;
68
+ }
69
+ /**
70
+ * Get a compact reference string for injection into the model context.
71
+ * Returns something like:
72
+ * "[Task ctx: ~/.jelly/tasks/.../context.md — 4 findings, 3.2KB. Use read_task_context("abc123")]"
73
+ */
74
+ getReference(taskId) {
75
+ const ctx = this.activeTasks.get(taskId);
76
+ if (!ctx || !existsSync(ctx.contextMd))
77
+ return "";
78
+ const content = readFileSync(ctx.contextMd, "utf-8");
79
+ const sizeKB = (content.length / 1024).toFixed(1);
80
+ return `[Task context saved: ${ctx.contextMd} — ${ctx.findings} findings, ${sizeKB}KB. ` +
81
+ `To read back: use tool read_task_context with taskId="${taskId}"]`;
82
+ }
83
+ /**
84
+ * Mark a task complete and optionally delete its folder.
85
+ * Deletion is deferred 5 seconds to allow model to read final state.
86
+ */
87
+ closeTask(taskId) {
88
+ const ctx = this.activeTasks.get(taskId);
89
+ if (!ctx)
90
+ return;
91
+ // Update status in the file
92
+ if (existsSync(ctx.contextMd)) {
93
+ try {
94
+ const content = readFileSync(ctx.contextMd, "utf-8");
95
+ writeFileSync(ctx.contextMd, content
96
+ .replace("**Status:** in_progress", `**Status:** completed\n**Completed:** ${new Date().toISOString()}`), "utf-8");
97
+ }
98
+ catch { /* best effort */ }
99
+ }
100
+ if (!ctx.keep) {
101
+ // Deferred delete — give agent 5s to read final context if needed
102
+ setTimeout(() => {
103
+ try {
104
+ rmSync(ctx.taskDir, { recursive: true, force: true });
105
+ }
106
+ catch { /* ignore */ }
107
+ }, 5_000);
108
+ }
109
+ this.activeTasks.delete(taskId);
110
+ }
111
+ /** Permanently keep a task folder (user called /keep <taskId>) */
112
+ keepTask(taskId) {
113
+ const ctx = this.activeTasks.get(taskId);
114
+ if (!ctx)
115
+ return false;
116
+ ctx.keep = true;
117
+ return true;
118
+ }
119
+ getActiveTasks() {
120
+ return [...this.activeTasks.values()];
121
+ }
122
+ getTask(taskId) {
123
+ return this.activeTasks.get(taskId);
124
+ }
125
+ // ── Tool: read_task_context ────────────────────────────────────────────────
126
+ async readContextTool(_id, { taskId }) {
127
+ const ctx = this.activeTasks.get(taskId);
128
+ if (!ctx) {
129
+ // Try to find a completed task folder on disk by taskId suffix
130
+ const dirs = existsSync(TASKS_DIR) ? readdirSync(TASKS_DIR) : [];
131
+ const dir = dirs.find(d => d.endsWith(`-${taskId}`));
132
+ if (dir) {
133
+ const mdPath = join(TASKS_DIR, dir, "context.md");
134
+ if (existsSync(mdPath)) {
135
+ const content = readFileSync(mdPath, "utf-8");
136
+ return {
137
+ content: [{ type: "text", text: content }],
138
+ details: { taskId, path: mdPath, sizeBytes: content.length, status: "archived" },
139
+ };
140
+ }
141
+ }
142
+ return {
143
+ content: [{ type: "text", text: `Task "${taskId}" not found. It may have been deleted after completion.` }],
144
+ details: {},
145
+ };
146
+ }
147
+ if (!existsSync(ctx.contextMd)) {
148
+ return {
149
+ content: [{ type: "text", text: `Context file missing for task "${taskId}".` }],
150
+ details: {},
151
+ };
152
+ }
153
+ const content = readFileSync(ctx.contextMd, "utf-8");
154
+ return {
155
+ content: [{ type: "text", text: content }],
156
+ details: {
157
+ taskId,
158
+ path: ctx.contextMd,
159
+ sizeBytes: content.length,
160
+ findings: ctx.findings,
161
+ status: "active",
162
+ },
163
+ };
164
+ }
165
+ // ── Tool: list_tasks ───────────────────────────────────────────────────────
166
+ async listTasksTool() {
167
+ const active = this.getActiveTasks();
168
+ // Also scan disk for recent completed tasks
169
+ const onDisk = [];
170
+ if (existsSync(TASKS_DIR)) {
171
+ const dirs = readdirSync(TASKS_DIR).slice(-10); // last 10
172
+ for (const d of dirs) {
173
+ const mdPath = join(TASKS_DIR, d, "context.md");
174
+ if (existsSync(mdPath)) {
175
+ const size = (readFileSync(mdPath, "utf-8").length / 1024).toFixed(1);
176
+ const id = d.split("-").pop() ?? d;
177
+ if (!this.activeTasks.has(id))
178
+ onDisk.push({ dir: d, id, sizeKB: size });
179
+ }
180
+ }
181
+ }
182
+ const lines = [];
183
+ if (active.length > 0) {
184
+ lines.push(`Active tasks (${active.length}):`);
185
+ for (const t of active) {
186
+ const mdContent = existsSync(t.contextMd) ? readFileSync(t.contextMd, "utf-8") : "";
187
+ const sizeKB = (mdContent.length / 1024).toFixed(1);
188
+ lines.push(` 📁 [${t.taskId}] ${t.title.slice(0, 50)} — ${t.findings} findings, ${sizeKB}KB`);
189
+ }
190
+ }
191
+ if (onDisk.length > 0) {
192
+ lines.push(`\nRecent completed tasks:`);
193
+ for (const t of onDisk) {
194
+ lines.push(` 📄 [${t.id}] ${t.dir.slice(14, 64)} — ${t.sizeKB}KB`);
195
+ }
196
+ }
197
+ if (lines.length === 0)
198
+ lines.push("No active or recent task contexts.");
199
+ return {
200
+ content: [{ type: "text", text: lines.join("\n") }],
201
+ details: { activeTasks: active.length, completedOnDisk: onDisk.length },
202
+ };
203
+ }
204
+ }
205
+ /** Singleton — one store per process */
206
+ export const contextStore = new ContextStore();
207
+ //# sourceMappingURL=ContextStore.js.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * GoalManager — cross-session persistent goal tracking. (#12)
3
+ *
4
+ * Goals set in one session survive restarts and are injected into the
5
+ * system prompt on every subsequent session. This gives the agent
6
+ * continuity — it knows what it was asked to watch or do.
7
+ *
8
+ * Stored in ~/.jelly/goals.json
9
+ * Tools: set_goal, complete_goal, list_goals, update_goal_notes
10
+ * Commands: /goals, /goal add <text>, /goal done <id>
11
+ */
12
+ import { type Static } from "@sinclair/typebox";
13
+ export interface Goal {
14
+ id: string;
15
+ text: string;
16
+ createdAt: number;
17
+ updatedAt: number;
18
+ status: "active" | "completed" | "paused";
19
+ notes: string[];
20
+ }
21
+ export declare class GoalManager {
22
+ private goals;
23
+ constructor();
24
+ private load;
25
+ private save;
26
+ add(text: string): Goal;
27
+ complete(id: string): boolean;
28
+ pause(id: string): boolean;
29
+ addNote(id: string, note: string): boolean;
30
+ getActive(): Goal[];
31
+ getAll(): Goal[];
32
+ get(id: string): Goal | undefined;
33
+ /** Build an active goals block for injection into the system prompt */
34
+ buildContextBlock(): string;
35
+ readonly setGoalParams: import("@sinclair/typebox").TObject<{
36
+ text: import("@sinclair/typebox").TString;
37
+ }>;
38
+ setGoalTool(_id: string, params: Static<typeof this.setGoalParams>): Promise<{
39
+ content: {
40
+ type: "text";
41
+ text: string;
42
+ }[];
43
+ details: {
44
+ goalId: string;
45
+ text: string;
46
+ };
47
+ }>;
48
+ readonly completeGoalParams: import("@sinclair/typebox").TObject<{
49
+ id: import("@sinclair/typebox").TString;
50
+ note: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
51
+ }>;
52
+ completeGoalTool(_id: string, params: Static<typeof this.completeGoalParams>): Promise<{
53
+ content: {
54
+ type: "text";
55
+ text: string;
56
+ }[];
57
+ details: {
58
+ goalId: string;
59
+ success: boolean;
60
+ };
61
+ }>;
62
+ readonly listGoalsParams: import("@sinclair/typebox").TObject<{
63
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
64
+ }>;
65
+ listGoalsTool(_id: string, params: Static<typeof this.listGoalsParams>): Promise<{
66
+ content: {
67
+ type: "text";
68
+ text: string;
69
+ }[];
70
+ details: {
71
+ count?: undefined;
72
+ goals?: undefined;
73
+ };
74
+ } | {
75
+ content: {
76
+ type: "text";
77
+ text: string;
78
+ }[];
79
+ details: {
80
+ count: number;
81
+ goals: Goal[];
82
+ };
83
+ }>;
84
+ readonly addGoalNoteParams: import("@sinclair/typebox").TObject<{
85
+ id: import("@sinclair/typebox").TString;
86
+ note: import("@sinclair/typebox").TString;
87
+ }>;
88
+ addGoalNoteTool(_id: string, params: Static<typeof this.addGoalNoteParams>): Promise<{
89
+ content: {
90
+ type: "text";
91
+ text: string;
92
+ }[];
93
+ details: {
94
+ goalId: string;
95
+ success: boolean;
96
+ };
97
+ }>;
98
+ }
99
+ /** Singleton */
100
+ export declare const goalManager: GoalManager;
101
+ //# sourceMappingURL=GoalManager.d.ts.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * GoalManager — cross-session persistent goal tracking. (#12)
3
+ *
4
+ * Goals set in one session survive restarts and are injected into the
5
+ * system prompt on every subsequent session. This gives the agent
6
+ * continuity — it knows what it was asked to watch or do.
7
+ *
8
+ * Stored in ~/.jelly/goals.json
9
+ * Tools: set_goal, complete_goal, list_goals, update_goal_notes
10
+ * Commands: /goals, /goal add <text>, /goal done <id>
11
+ */
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { randomUUID } from "node:crypto";
16
+ import { Type } from "@sinclair/typebox";
17
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
18
+ const GOALS_FILE = join(JELLY_HOME, "goals.json");
19
+ export class GoalManager {
20
+ goals = [];
21
+ constructor() {
22
+ this.load();
23
+ }
24
+ // ── Persistence ────────────────────────────────────────────────────────────
25
+ load() {
26
+ try {
27
+ if (!existsSync(GOALS_FILE))
28
+ return;
29
+ const raw = JSON.parse(readFileSync(GOALS_FILE, "utf-8"));
30
+ if (Array.isArray(raw))
31
+ this.goals = raw;
32
+ }
33
+ catch { /* start fresh */ }
34
+ }
35
+ save() {
36
+ try {
37
+ mkdirSync(JELLY_HOME, { recursive: true });
38
+ writeFileSync(GOALS_FILE, JSON.stringify(this.goals, null, 2), "utf-8");
39
+ }
40
+ catch { /* best effort */ }
41
+ }
42
+ // ── CRUD ───────────────────────────────────────────────────────────────────
43
+ add(text) {
44
+ const goal = {
45
+ id: randomUUID().slice(0, 8),
46
+ text: text.trim().slice(0, 500),
47
+ createdAt: Date.now(),
48
+ updatedAt: Date.now(),
49
+ status: "active",
50
+ notes: [],
51
+ };
52
+ this.goals.push(goal);
53
+ this.save();
54
+ return goal;
55
+ }
56
+ complete(id) {
57
+ const goal = this.goals.find(g => g.id === id);
58
+ if (!goal)
59
+ return false;
60
+ goal.status = "completed";
61
+ goal.updatedAt = Date.now();
62
+ this.save();
63
+ return true;
64
+ }
65
+ pause(id) {
66
+ const goal = this.goals.find(g => g.id === id);
67
+ if (!goal)
68
+ return false;
69
+ goal.status = "paused";
70
+ goal.updatedAt = Date.now();
71
+ this.save();
72
+ return true;
73
+ }
74
+ addNote(id, note) {
75
+ const goal = this.goals.find(g => g.id === id);
76
+ if (!goal)
77
+ return false;
78
+ goal.notes.push(`[${new Date().toLocaleDateString()}] ${note.slice(0, 300)}`);
79
+ goal.updatedAt = Date.now();
80
+ if (goal.notes.length > 20)
81
+ goal.notes = goal.notes.slice(-20); // keep last 20
82
+ this.save();
83
+ return true;
84
+ }
85
+ getActive() {
86
+ return this.goals.filter(g => g.status === "active");
87
+ }
88
+ getAll() {
89
+ return [...this.goals];
90
+ }
91
+ get(id) {
92
+ return this.goals.find(g => g.id === id);
93
+ }
94
+ // ── System Prompt Block ────────────────────────────────────────────────────
95
+ /** Build an active goals block for injection into the system prompt */
96
+ buildContextBlock() {
97
+ const active = this.getActive();
98
+ if (active.length === 0)
99
+ return "";
100
+ const lines = ["\n## Active Goals (set by user — monitor and act on these)"];
101
+ for (const g of active) {
102
+ lines.push(`- [${g.id}] ${g.text}`);
103
+ if (g.notes.length > 0) {
104
+ lines.push(` Last note: ${g.notes[g.notes.length - 1]}`);
105
+ }
106
+ }
107
+ lines.push("");
108
+ return lines.join("\n");
109
+ }
110
+ // ── Tools ──────────────────────────────────────────────────────────────────
111
+ setGoalParams = Type.Object({
112
+ text: Type.String({ description: "Goal text e.g. 'Watch ETH for breakout above $4000'" }),
113
+ });
114
+ async setGoalTool(_id, params) {
115
+ const goal = this.add(params.text);
116
+ return {
117
+ content: [{ type: "text", text: `Goal set: [${goal.id}] ${goal.text}` }],
118
+ details: { goalId: goal.id, text: goal.text },
119
+ };
120
+ }
121
+ completeGoalParams = Type.Object({
122
+ id: Type.String({ description: "Goal ID to mark complete" }),
123
+ note: Type.Optional(Type.String({ description: "Completion note" })),
124
+ });
125
+ async completeGoalTool(_id, params) {
126
+ if (params.note)
127
+ this.addNote(params.id, params.note);
128
+ const ok = this.complete(params.id);
129
+ return {
130
+ content: [{ type: "text", text: ok ? `Goal ${params.id} marked complete.` : `Goal ${params.id} not found.` }],
131
+ details: { goalId: params.id, success: ok },
132
+ };
133
+ }
134
+ listGoalsParams = Type.Object({
135
+ status: Type.Optional(Type.String({ description: "Filter: active | completed | paused | all (default: active)" })),
136
+ });
137
+ async listGoalsTool(_id, params) {
138
+ const filter = params.status ?? "active";
139
+ const goals = filter === "all" ? this.getAll() : this.goals.filter(g => g.status === filter);
140
+ if (goals.length === 0) {
141
+ return { content: [{ type: "text", text: `No ${filter} goals.` }], details: {} };
142
+ }
143
+ const lines = goals.map(g => {
144
+ const icon = g.status === "active" ? "🎯" : g.status === "completed" ? "✅" : "⏸";
145
+ const notes = g.notes.length > 0 ? `\n Notes: ${g.notes[g.notes.length - 1]}` : "";
146
+ return `${icon} [${g.id}] ${g.text}${notes}`;
147
+ });
148
+ return {
149
+ content: [{ type: "text", text: `Goals (${filter}):\n${lines.join("\n")}` }],
150
+ details: { count: goals.length, goals },
151
+ };
152
+ }
153
+ addGoalNoteParams = Type.Object({
154
+ id: Type.String({ description: "Goal ID" }),
155
+ note: Type.String({ description: "Progress note to add" }),
156
+ });
157
+ async addGoalNoteTool(_id, params) {
158
+ const ok = this.addNote(params.id, params.note);
159
+ return {
160
+ content: [{ type: "text", text: ok ? `Note added to goal ${params.id}.` : `Goal ${params.id} not found.` }],
161
+ details: { goalId: params.id, success: ok },
162
+ };
163
+ }
164
+ }
165
+ /** Singleton */
166
+ export const goalManager = new GoalManager();
167
+ //# sourceMappingURL=GoalManager.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * MemoryStore — persistent long-term memory using Node 22+ built-in SQLite.
3
+ * (#7 — cross-session memory)
4
+ *
5
+ * Stores all conversation messages, searchable by keyword and session.
6
+ * Injected into system prompt at session_start to give the agent awareness
7
+ * of past decisions, prices seen, strategies discussed.
8
+ *
9
+ * Uses node:sqlite (experimental in Node 22, stable in Node 24) — zero deps.
10
+ */
11
+ export interface MemoryEntry {
12
+ id: number;
13
+ sessionId: string;
14
+ role: string;
15
+ content: string;
16
+ tokens: number;
17
+ ts: number;
18
+ tags: string[];
19
+ }
20
+ export interface RecentSession {
21
+ sessionId: string;
22
+ summary: string;
23
+ msgCount: number;
24
+ ts: number;
25
+ }
26
+ export declare class MemoryStore {
27
+ private db;
28
+ private available;
29
+ constructor();
30
+ private init;
31
+ get isAvailable(): boolean;
32
+ save(sessionId: string, role: string, content: string, tags?: string[]): void;
33
+ /** Keyword search across all sessions */
34
+ search(query: string, limit?: number): MemoryEntry[];
35
+ /** Get summaries of recent sessions (for system prompt injection) */
36
+ getRecentSessions(limit?: number): RecentSession[];
37
+ /** Get all messages for a specific session */
38
+ getSession(sessionId: string): MemoryEntry[];
39
+ /** Build a short memory context block for system prompt injection */
40
+ buildContextBlock(currentSessionId: string): string;
41
+ getStats(): {
42
+ totalMessages: number;
43
+ totalSessions: number;
44
+ };
45
+ }
46
+ /** Singleton */
47
+ export declare const memoryStore: MemoryStore;
48
+ //# sourceMappingURL=MemoryStore.d.ts.map