@meshxdata/fops 0.0.5 → 0.0.7

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 (44) hide show
  1. package/package.json +3 -2
  2. package/src/auth/coda.js +4 -4
  3. package/src/auth/login.js +9 -6
  4. package/src/commands/index.js +158 -0
  5. package/src/doctor.js +340 -71
  6. package/src/feature-flags.js +3 -3
  7. package/src/lazy.js +12 -0
  8. package/src/plugins/bundled/coda/auth.js +85 -0
  9. package/src/plugins/bundled/coda/client.js +187 -0
  10. package/src/plugins/bundled/coda/fops.plugin.json +7 -0
  11. package/src/plugins/bundled/coda/index.js +284 -0
  12. package/src/plugins/bundled/coda/package.json +3 -0
  13. package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
  14. package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
  15. package/src/plugins/bundled/cursor/index.js +433 -0
  16. package/src/plugins/bundled/cursor/package.json +1 -0
  17. package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
  18. package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
  19. package/src/plugins/bundled/fops-plugin-1password/index.js +241 -0
  20. package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
  21. package/src/plugins/bundled/fops-plugin-1password/lib/op.js +119 -0
  22. package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
  23. package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +66 -0
  24. package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
  25. package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
  26. package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
  27. package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
  28. package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +146 -0
  29. package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
  30. package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
  31. package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
  32. package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
  33. package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
  34. package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
  35. package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
  36. package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
  37. package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
  38. package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
  39. package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
  40. package/src/plugins/loader.js +43 -3
  41. package/src/setup/aws.js +74 -46
  42. package/src/setup/setup.js +4 -2
  43. package/src/setup/wizard.js +16 -20
  44. package/src/wsl.js +82 -0
@@ -0,0 +1,180 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import {
6
+ awsVersion,
7
+ detectEcrRegistry,
8
+ detectSsoProfiles,
9
+ stsIdentity,
10
+ ssoLogin,
11
+ ecrLogin,
12
+ } from "./aws.js";
13
+ import { getInquirer } from "../../../../lazy.js";
14
+
15
+ /**
16
+ * Read ~/.fops.json config (full file).
17
+ */
18
+ function readFopsConfig() {
19
+ const configPath = path.join(os.homedir(), ".fops.json");
20
+ try {
21
+ if (fs.existsSync(configPath)) {
22
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
23
+ }
24
+ } catch {}
25
+ return {};
26
+ }
27
+
28
+ /**
29
+ * Save plugin config into ~/.fops.json (deep-merged).
30
+ */
31
+ function saveFopsConfig(updates) {
32
+ const configPath = path.join(os.homedir(), ".fops.json");
33
+ const existing = readFopsConfig();
34
+ const merged = { ...existing, ...updates };
35
+ if (updates.plugins) {
36
+ merged.plugins = { ...existing.plugins, ...updates.plugins };
37
+ merged.plugins.entries = {
38
+ ...existing?.plugins?.entries,
39
+ ...updates.plugins.entries,
40
+ };
41
+ }
42
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
43
+ }
44
+
45
+ /**
46
+ * Interactive setup wizard for the ECR plugin.
47
+ */
48
+ export async function runSetupWizard(root) {
49
+ console.log(chalk.bold.cyan("\n ECR Plugin Setup\n"));
50
+
51
+ // Step 1: Check AWS CLI
52
+ console.log(chalk.dim(" Checking AWS CLI..."));
53
+ let version = await awsVersion();
54
+ if (!version) {
55
+ console.log(chalk.red(" ✗ AWS CLI not found."));
56
+ const { install } = await (await getInquirer()).prompt([{
57
+ type: "confirm", name: "install", message: "Install via Homebrew?", default: true,
58
+ }]);
59
+ if (install) {
60
+ console.log(chalk.cyan(" ▶ brew install awscli"));
61
+ const { execa: execaFn } = await import("execa");
62
+ try {
63
+ await execaFn("brew", ["install", "awscli"], { stdio: "inherit", timeout: 120000 });
64
+ version = await awsVersion();
65
+ } catch (err) {
66
+ console.log(chalk.red(` Install failed: ${err.message}`));
67
+ return;
68
+ }
69
+ } else {
70
+ console.log(chalk.dim(" Install manually: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"));
71
+ return;
72
+ }
73
+ }
74
+ console.log(chalk.green(` ✓ AWS CLI v${version}`));
75
+
76
+ // Step 2: Check for existing SSO profiles
77
+ console.log(chalk.dim("\n Checking SSO profiles..."));
78
+ let profiles = detectSsoProfiles();
79
+ let selectedProfile;
80
+
81
+ if (profiles.length > 0) {
82
+ console.log(chalk.green(` ✓ Found ${profiles.length} SSO profile(s)`));
83
+ if (profiles.length === 1) {
84
+ selectedProfile = profiles[0].name;
85
+ console.log(chalk.dim(` Using profile: ${selectedProfile}`));
86
+ } else {
87
+ const choices = profiles.map((p) => ({ name: p.name, value: p.name }));
88
+ const { profile } = await (await getInquirer()).prompt([{
89
+ type: "list", name: "profile", message: "Select AWS profile:", choices,
90
+ }]);
91
+ selectedProfile = profile;
92
+ }
93
+ } else {
94
+ // No profiles — prompt for SSO config
95
+ console.log(chalk.yellow(" No SSO profiles found. Let's configure one."));
96
+
97
+ const answers = await (await getInquirer()).prompt([
98
+ { type: "input", name: "sessionName", message: "SSO session name:", default: "me-central-1" },
99
+ { type: "input", name: "ssoStartUrl", message: "SSO start URL:", validate: (v) => v?.trim() ? true : "Required." },
100
+ { type: "input", name: "ssoRegion", message: "SSO region:", default: "us-east-1" },
101
+ { type: "input", name: "accountId", message: "AWS account ID:", validate: (v) => /^\d{12}$/.test(v?.trim()) ? true : "Must be 12 digits." },
102
+ { type: "input", name: "roleName", message: "SSO role name:", default: "AdministratorAccess" },
103
+ { type: "input", name: "profileName", message: "Profile name:", default: "dev" },
104
+ { type: "input", name: "region", message: "Default region:", default: (a) => a.ssoRegion },
105
+ ]);
106
+
107
+ // Ensure ~/.aws directory exists
108
+ const awsDir = path.join(os.homedir(), ".aws");
109
+ if (!fs.existsSync(awsDir)) fs.mkdirSync(awsDir, { mode: 0o700 });
110
+
111
+ const configPath = path.join(awsDir, "config");
112
+ const block = `[sso-session ${answers.sessionName.trim()}]
113
+ sso_start_url = ${answers.ssoStartUrl.trim()}
114
+ sso_region = ${answers.ssoRegion.trim()}
115
+ sso_registration_scopes = sso:account:access
116
+
117
+ [profile ${answers.profileName.trim()}]
118
+ sso_session = ${answers.sessionName.trim()}
119
+ sso_account_id = ${answers.accountId.trim()}
120
+ sso_role_name = ${answers.roleName.trim()}
121
+ region = ${answers.region.trim()}
122
+ output = json
123
+ `;
124
+ fs.writeFileSync(configPath, block);
125
+ console.log(chalk.green(` ✓ Written to ~/.aws/config (profile: ${answers.profileName})`));
126
+ selectedProfile = answers.profileName.trim();
127
+ }
128
+
129
+ // Step 3: SSO login
130
+ console.log(chalk.dim("\n Checking AWS session..."));
131
+ const sts = await stsIdentity(selectedProfile);
132
+ if (!sts.valid) {
133
+ console.log(chalk.yellow(" Session expired — logging in..."));
134
+ await ssoLogin(selectedProfile);
135
+
136
+ const retry = await stsIdentity(selectedProfile);
137
+ if (!retry.valid) {
138
+ console.log(chalk.red(" ✗ SSO login failed."));
139
+ return;
140
+ }
141
+ }
142
+ console.log(chalk.green(" ✓ AWS session valid"));
143
+
144
+ // Step 4: ECR docker login
145
+ const ecr = detectEcrRegistry(root);
146
+ if (ecr) {
147
+ console.log(chalk.dim("\n Logging in to ECR..."));
148
+ const login = await ecrLogin(ecr.accountId, ecr.region, selectedProfile);
149
+ if (login.success) {
150
+ console.log(chalk.green(` ✓ ECR authenticated (${login.url})`));
151
+ } else {
152
+ console.log(chalk.red(` ✗ ECR login failed for ${login.url}`));
153
+ }
154
+ }
155
+
156
+ // Step 5: Auto-login preference
157
+ const { autoLogin } = await (await getInquirer()).prompt([{
158
+ type: "confirm", name: "autoLogin", message: "Auto-login to ECR before `fops up`?", default: true,
159
+ }]);
160
+
161
+ // Step 6: Save config
162
+ saveFopsConfig({
163
+ plugins: {
164
+ entries: {
165
+ "fops-plugin-ecr": {
166
+ enabled: true,
167
+ config: {
168
+ profile: selectedProfile,
169
+ autoLogin,
170
+ },
171
+ },
172
+ },
173
+ },
174
+ });
175
+
176
+ console.log(chalk.green("\n ✓ Config saved to ~/.fops.json"));
177
+ console.log(chalk.dim(` profile: ${selectedProfile}`));
178
+ console.log(chalk.dim(` autoLogin: ${autoLogin}`));
179
+ console.log(chalk.bold.green("\n Setup complete! Run: fops ecr sync\n"));
180
+ }
@@ -0,0 +1,74 @@
1
+ import chalk from "chalk";
2
+ import { execa } from "execa";
3
+ import {
4
+ detectEcrRegistry,
5
+ detectSsoProfiles,
6
+ stsIdentity,
7
+ ssoLogin,
8
+ ecrLogin,
9
+ } from "./aws.js";
10
+
11
+ /**
12
+ * Authenticate to ECR and pull latest images.
13
+ *
14
+ * 1. Detect ECR registry from compose file
15
+ * 2. Verify SSO session (auto-login if expired)
16
+ * 3. ECR docker login
17
+ * 4. docker compose pull from project root
18
+ * 5. Report results
19
+ */
20
+ export async function syncImages(root, config = {}) {
21
+ // 1. Detect registry
22
+ const ecr = detectEcrRegistry(root);
23
+ if (!ecr) {
24
+ console.log(chalk.yellow(" No ECR images found in docker-compose.yaml."));
25
+ return { success: false, reason: "no-ecr-images" };
26
+ }
27
+
28
+ const ecrUrl = `${ecr.accountId}.dkr.ecr.${ecr.region}.amazonaws.com`;
29
+ console.log(chalk.dim(` Registry: ${ecrUrl}`));
30
+
31
+ // 2. Determine profile
32
+ const profile = config.profile || detectSsoProfiles()[0]?.name || null;
33
+ if (profile) {
34
+ console.log(chalk.dim(` Profile: ${profile}`));
35
+ }
36
+
37
+ // 3. Verify SSO session
38
+ const sts = await stsIdentity(profile);
39
+ if (!sts.valid) {
40
+ console.log(chalk.yellow(" AWS session expired — logging in via SSO..."));
41
+ await ssoLogin(profile);
42
+
43
+ const retry = await stsIdentity(profile);
44
+ if (!retry.valid) {
45
+ console.log(chalk.red(" ✗ SSO login failed. Run: aws sso login"));
46
+ return { success: false, reason: "sso-failed" };
47
+ }
48
+ }
49
+ console.log(chalk.green(" ✓ AWS session valid"));
50
+
51
+ // 4. ECR docker login
52
+ const login = await ecrLogin(ecr.accountId, ecr.region, profile);
53
+ if (!login.success) {
54
+ console.log(chalk.red(" ✗ ECR docker login failed"));
55
+ return { success: false, reason: "ecr-login-failed" };
56
+ }
57
+ console.log(chalk.green(` ✓ ECR authenticated`));
58
+
59
+ // 5. Pull images
60
+ console.log(chalk.cyan("\n Pulling images...\n"));
61
+ const { exitCode } = await execa(
62
+ "docker",
63
+ ["compose", "pull"],
64
+ { cwd: root, stdio: "inherit", reject: false, timeout: 600_000 },
65
+ );
66
+
67
+ if (exitCode === 0) {
68
+ console.log(chalk.green("\n ✓ All images pulled successfully."));
69
+ return { success: true };
70
+ } else {
71
+ console.log(chalk.yellow("\n ⚠ Some images may have failed to pull."));
72
+ return { success: false, reason: "pull-failed" };
73
+ }
74
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: ecr
3
+ description: AWS ECR authentication and container image management
4
+ requires: aws
5
+ ---
6
+
7
+ ## ECR Plugin for fops
8
+
9
+ Manages AWS ECR authentication and container image pulls for the Foundation stack.
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ fops ecr setup # Interactive wizard: install AWS CLI, configure SSO, ECR login
15
+ fops ecr sync # Authenticate to ECR and pull latest images
16
+ fops ecr status # Show AWS CLI, SSO session, ECR auth, image freshness
17
+ ```
18
+
19
+ ## Auth Flow
20
+
21
+ ECR authentication uses AWS SSO:
22
+
23
+ 1. **SSO login**: `aws sso login --profile <name>` — opens browser for authentication
24
+ 2. **Get ECR password**: `aws ecr get-login-password --region <region>`
25
+ 3. **Docker login**: `docker login --username AWS --password-stdin <registry-url>`
26
+
27
+ The `before:up` hook handles this automatically when `autoLogin: true`.
28
+
29
+ ## Config
30
+
31
+ Stored in `~/.fops.json`:
32
+
33
+ ```json
34
+ {
35
+ "plugins": {
36
+ "entries": {
37
+ "fops-plugin-ecr": {
38
+ "enabled": true,
39
+ "config": {
40
+ "profile": "dev",
41
+ "autoLogin": true
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ - `profile` — AWS CLI profile name (must have `sso_session` in `~/.aws/config`)
50
+ - `autoLogin` — When true, auto-authenticate to ECR before `fops up`
51
+
52
+ ## AWS SSO Configuration
53
+
54
+ The plugin reads `~/.aws/config` for SSO profiles. A typical setup:
55
+
56
+ ```ini
57
+ [sso-session meshx]
58
+ sso_start_url = https://myorg.awsapps.com/start
59
+ sso_region = us-east-1
60
+ sso_registration_scopes = sso:account:access
61
+
62
+ [profile dev]
63
+ sso_session = meshx
64
+ sso_account_id = 676206939231
65
+ sso_role_name = AdministratorAccess
66
+ region = me-central-1
67
+ output = json
68
+ ```
69
+
70
+ ## ECR Registry
71
+
72
+ Images in `docker-compose.yaml` follow the pattern:
73
+
74
+ ```
75
+ 676206939231.dkr.ecr.me-central-1.amazonaws.com/foundation/<service>:<tag>
76
+ ```
77
+
78
+ The plugin auto-detects the account ID and region from compose image references.
79
+
80
+ ## Setup
81
+
82
+ Run `fops ecr setup` to:
83
+
84
+ 1. Check/install AWS CLI (offers Homebrew install)
85
+ 2. Detect or create SSO profile in `~/.aws/config`
86
+ 3. Authenticate via SSO
87
+ 4. Docker login to ECR
88
+ 5. Set auto-login preference
89
+ 6. Save config to `~/.fops.json`
90
+
91
+ ## Image Freshness
92
+
93
+ `fops ecr status` checks local Docker images against ECR references. Images older than 7 days are flagged as stale. Run `fops ecr sync` to pull fresh copies.
94
+
95
+ ## Troubleshooting
96
+
97
+ **AWS CLI not found**: Install via `brew install awscli` or see https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
98
+
99
+ **SSO session expired**: Run `fops ecr sync` (auto-logins) or `aws sso login --profile dev`
100
+
101
+ **ECR login failed**: Check that your SSO profile has permission to access ECR. Verify with `aws sts get-caller-identity --profile dev`
102
+
103
+ **Images not pulling**: Ensure Docker is running and you're authenticated. Run `fops ecr status` to diagnose. Check that the registry URL matches the images in `docker-compose.yaml`.
104
+
105
+ **Auto-login not firing**: Check `~/.fops.json` has `autoLogin: true` under the plugin config.
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "fops-plugin-memory",
3
+ "name": "Agent Memory",
4
+ "version": "0.1.0",
5
+ "description": "Persistent memory for the fops agent across sessions",
6
+ "skills": ["skills/memory"]
7
+ }
@@ -0,0 +1,148 @@
1
+ import chalk from "chalk";
2
+ import { loadMemories, addMemory, removeMemory, clearMemories } from "./lib/store.js";
3
+ import { searchMemories } from "./lib/relevance.js";
4
+
5
+ export function register(api) {
6
+ // ── Command: fops memory ───────────────────────────
7
+ api.registerCommand((program) => {
8
+ const cmd = program
9
+ .command("memory")
10
+ .alias("mem")
11
+ .description("Manage agent memory across sessions");
12
+
13
+ // fops memory save <text> [--tag <tag>...]
14
+ cmd
15
+ .command("save <text...>")
16
+ .description("Save a memory")
17
+ .option("-t, --tag <tags...>", "tags for categorization")
18
+ .action((textParts, opts) => {
19
+ const text = textParts.join(" ");
20
+ const tags = opts.tag || [];
21
+ const entry = addMemory(text, tags);
22
+ console.log(chalk.green(` ✓ Saved memory ${entry.id}`));
23
+ console.log(chalk.gray(` "${entry.text}"`));
24
+ if (tags.length) console.log(chalk.gray(` tags: ${tags.join(", ")}`));
25
+ });
26
+
27
+ // fops memory list
28
+ cmd
29
+ .command("list")
30
+ .description("List all memories")
31
+ .action(() => {
32
+ const memories = loadMemories();
33
+ if (memories.length === 0) {
34
+ console.log(chalk.gray(" No memories saved."));
35
+ return;
36
+ }
37
+ console.log(chalk.bold.cyan(`\n Agent Memory (${memories.length})\n`));
38
+ for (const m of memories) {
39
+ const age = daysSince(m.createdAt);
40
+ const tags = m.tags?.length ? chalk.blue(` [${m.tags.join(", ")}]`) : "";
41
+ console.log(chalk.gray(` ${m.id}`) + ` ${m.text}` + tags + chalk.gray(` — ${age}`));
42
+ }
43
+ console.log("");
44
+ });
45
+
46
+ // fops memory search <query>
47
+ cmd
48
+ .command("search <query...>")
49
+ .description("Search memories by relevance")
50
+ .action((queryParts) => {
51
+ const query = queryParts.join(" ");
52
+ const memories = loadMemories();
53
+ const results = searchMemories(memories, query);
54
+ if (results.length === 0) {
55
+ console.log(chalk.gray(" No matching memories."));
56
+ return;
57
+ }
58
+ console.log(chalk.bold.cyan(`\n Search: "${query}" (${results.length} match${results.length > 1 ? "es" : ""})\n`));
59
+ for (const { memory: m, score } of results) {
60
+ const tags = m.tags?.length ? chalk.blue(` [${m.tags.join(", ")}]`) : "";
61
+ const pct = Math.round(score * 100);
62
+ console.log(chalk.gray(` ${m.id} (${pct}%)`) + ` ${m.text}` + tags);
63
+ }
64
+ console.log("");
65
+ });
66
+
67
+ // fops memory forget <id>
68
+ cmd
69
+ .command("forget <id>")
70
+ .description("Remove a memory by ID")
71
+ .action((id) => {
72
+ const removed = removeMemory(id);
73
+ if (removed) {
74
+ console.log(chalk.green(` ✓ Removed: "${removed.text}"`));
75
+ } else {
76
+ console.log(chalk.red(` ✗ No memory found with ID: ${id}`));
77
+ }
78
+ });
79
+
80
+ // fops memory clear
81
+ cmd
82
+ .command("clear")
83
+ .description("Clear all memories")
84
+ .action(() => {
85
+ const count = clearMemories();
86
+ console.log(chalk.green(` ✓ Cleared ${count} memory(s).`));
87
+ });
88
+ });
89
+
90
+ // ── Knowledge source — inject relevant memories ────
91
+ api.registerKnowledgeSource({
92
+ name: "Agent Memory",
93
+ description: "Persistent memories from past sessions",
94
+ search(query) {
95
+ const memories = loadMemories();
96
+ if (memories.length === 0) return [];
97
+
98
+ const results = searchMemories(memories, query, { maxResults: 5, threshold: 0.15 });
99
+ if (results.length === 0) return [];
100
+
101
+ const lines = results.map(({ memory: m, score }) => {
102
+ const tags = m.tags?.length ? ` [${m.tags.join(", ")}]` : "";
103
+ const age = daysSince(m.createdAt);
104
+ return `- ${m.text}${tags} (${age})`;
105
+ });
106
+
107
+ return [
108
+ {
109
+ title: "Memories from past sessions",
110
+ content: [
111
+ "## Agent Memory",
112
+ "",
113
+ "These are things you remembered from previous sessions:",
114
+ "",
115
+ ...lines,
116
+ "",
117
+ "Use these memories to inform your responses. If a memory is outdated or wrong, suggest `fops memory forget <id>` to remove it.",
118
+ ].join("\n"),
119
+ score: 0.85,
120
+ },
121
+ ];
122
+ },
123
+ });
124
+
125
+ // ── Auto-run pattern — let agent save memories without confirmation ──
126
+ api.registerAutoRunPattern("fops memory");
127
+ api.registerAutoRunPattern("fops mem");
128
+
129
+ // ── Doctor check ───────────────────────────────────
130
+ api.registerDoctorCheck({
131
+ name: "Agent Memory",
132
+ fn: async (ok, warn) => {
133
+ const memories = loadMemories();
134
+ if (memories.length > 0) {
135
+ ok(`Agent memory`, `${memories.length} memory(s) stored`);
136
+ } else {
137
+ ok("Agent memory", "empty — agent will save learnings over time");
138
+ }
139
+ },
140
+ });
141
+ }
142
+
143
+ function daysSince(isoDate) {
144
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 86400000);
145
+ if (days === 0) return "today";
146
+ if (days === 1) return "1d ago";
147
+ return `${days}d ago`;
148
+ }
@@ -0,0 +1,72 @@
1
+ const STOP_WORDS = new Set([
2
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
3
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
4
+ "should", "may", "might", "shall", "can", "need", "must",
5
+ "i", "me", "my", "we", "our", "you", "your", "he", "she", "it",
6
+ "they", "them", "this", "that", "these", "those", "what", "which",
7
+ "who", "whom", "how", "when", "where", "why",
8
+ "and", "or", "but", "not", "no", "if", "then", "so", "than",
9
+ "to", "of", "in", "for", "on", "with", "at", "by", "from", "up",
10
+ "about", "into", "through", "after", "before", "between",
11
+ "all", "any", "some", "each", "every", "both", "few", "more",
12
+ "just", "also", "very", "too", "quite", "really", "only",
13
+ ]);
14
+
15
+ /**
16
+ * Tokenize text into meaningful words.
17
+ */
18
+ function tokenize(text) {
19
+ return text
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\-_/.]+/g, " ")
22
+ .split(/\s+/)
23
+ .filter((w) => w.length > 1 && !STOP_WORDS.has(w));
24
+ }
25
+
26
+ /**
27
+ * Score a memory against a query.
28
+ * Returns 0-1 score based on word overlap + tag boost.
29
+ */
30
+ function scoreMemory(memory, queryTokens) {
31
+ const memTokens = new Set(tokenize(memory.text));
32
+ // Add tags as tokens too
33
+ for (const tag of memory.tags || []) {
34
+ for (const t of tokenize(tag)) memTokens.add(t);
35
+ }
36
+
37
+ if (queryTokens.length === 0 || memTokens.size === 0) return 0;
38
+
39
+ let matches = 0;
40
+ for (const qt of queryTokens) {
41
+ // Exact match or substring match for compound terms
42
+ if (memTokens.has(qt)) {
43
+ matches++;
44
+ } else {
45
+ for (const mt of memTokens) {
46
+ if (mt.includes(qt) || qt.includes(mt)) {
47
+ matches += 0.5;
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ return matches / queryTokens.length;
55
+ }
56
+
57
+ /**
58
+ * Search memories by relevance to a query.
59
+ * Returns top-K memories sorted by score, filtered by threshold.
60
+ */
61
+ export function searchMemories(memories, query, { maxResults = 5, threshold = 0.2 } = {}) {
62
+ const queryTokens = tokenize(query);
63
+ if (queryTokens.length === 0) return [];
64
+
65
+ const scored = memories
66
+ .map((m) => ({ memory: m, score: scoreMemory(m, queryTokens) }))
67
+ .filter((r) => r.score >= threshold)
68
+ .sort((a, b) => b.score - a.score)
69
+ .slice(0, maxResults);
70
+
71
+ return scored;
72
+ }
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+
6
+ const MEMORY_DIR = path.join(os.homedir(), ".fops", "memory");
7
+ const MEMORY_FILE = path.join(MEMORY_DIR, "memories.json");
8
+
9
+ function ensureDir() {
10
+ if (!fs.existsSync(MEMORY_DIR)) {
11
+ fs.mkdirSync(MEMORY_DIR, { recursive: true });
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Load all memories from disk.
17
+ * Returns [] if file doesn't exist.
18
+ */
19
+ export function loadMemories() {
20
+ try {
21
+ if (fs.existsSync(MEMORY_FILE)) {
22
+ return JSON.parse(fs.readFileSync(MEMORY_FILE, "utf8"));
23
+ }
24
+ } catch {}
25
+ return [];
26
+ }
27
+
28
+ /**
29
+ * Save full memories array to disk.
30
+ */
31
+ function saveMemories(memories) {
32
+ ensureDir();
33
+ fs.writeFileSync(MEMORY_FILE, JSON.stringify(memories, null, 2) + "\n");
34
+ }
35
+
36
+ /**
37
+ * Add a new memory.
38
+ * Returns the created memory object.
39
+ */
40
+ export function addMemory(text, tags = []) {
41
+ const memories = loadMemories();
42
+ const entry = {
43
+ id: crypto.randomUUID().split("-")[0],
44
+ text: text.trim(),
45
+ tags: tags.map((t) => t.trim().toLowerCase()).filter(Boolean),
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ memories.push(entry);
49
+ saveMemories(memories);
50
+ return entry;
51
+ }
52
+
53
+ /**
54
+ * Remove a memory by id (prefix match).
55
+ * Returns the removed entry or null.
56
+ */
57
+ export function removeMemory(id) {
58
+ const memories = loadMemories();
59
+ const idx = memories.findIndex((m) => m.id === id || m.id.startsWith(id));
60
+ if (idx === -1) return null;
61
+ const [removed] = memories.splice(idx, 1);
62
+ saveMemories(memories);
63
+ return removed;
64
+ }
65
+
66
+ /**
67
+ * Clear all memories.
68
+ * Returns the count of removed entries.
69
+ */
70
+ export function clearMemories() {
71
+ const memories = loadMemories();
72
+ const count = memories.length;
73
+ saveMemories([]);
74
+ return count;
75
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }