xmemory-cli 0.1.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.
@@ -0,0 +1,179 @@
1
+ const ALLOWED_SCHEMES = ["http", "https"];
2
+ const BLOCKED_HOSTS = [
3
+ "169.254.169.254",
4
+ "metadata.google.internal",
5
+ "100.100.100.200",
6
+ "localhost",
7
+ "127.0.0.1",
8
+ ];
9
+
10
+ function isPrivateOrLoopbackIP(hostname: string): boolean {
11
+ const net = require("net");
12
+ const normalized = hostname.replace(/^\[|\]$/g, "");
13
+ const ipVersion = net.isIP(normalized);
14
+ if (ipVersion === 4) {
15
+ return (
16
+ normalized === "127.0.0.1" ||
17
+ normalized === "0.0.0.0" ||
18
+ normalized.startsWith("10.") ||
19
+ normalized.startsWith("192.168.") ||
20
+ normalized.startsWith("172.16.") ||
21
+ (normalized.startsWith("172.") &&
22
+ [
23
+ "172.16.",
24
+ "172.17.",
25
+ "172.18.",
26
+ "172.19.",
27
+ "172.20.",
28
+ "172.21.",
29
+ "172.22.",
30
+ "172.23.",
31
+ "172.24.",
32
+ "172.25.",
33
+ "172.26.",
34
+ "172.27.",
35
+ "172.28.",
36
+ "172.29.",
37
+ "172.30.",
38
+ "172.31.",
39
+ ].some((b) => normalized.startsWith(b)))
40
+ );
41
+ }
42
+ if (ipVersion === 6) {
43
+ return (
44
+ normalized === "::1" ||
45
+ normalized === "::" ||
46
+ normalized === "0::1" ||
47
+ normalized === "::ffff:127.0.0.1" ||
48
+ normalized.startsWith("fc") ||
49
+ normalized.startsWith("fe80:") ||
50
+ normalized.startsWith("::ffff:0:")
51
+ );
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export interface Config {
57
+ apiBaseUrl: string;
58
+ defaultSpace?: string;
59
+ apiKey: string;
60
+ }
61
+
62
+ function isUrlAllowed(urlString: string): boolean {
63
+ try {
64
+ const url = new URL(urlString);
65
+ if (!ALLOWED_SCHEMES.includes(url.protocol.replace(/:$/, ""))) {
66
+ return false;
67
+ }
68
+ const hostname = url.hostname.toLowerCase();
69
+ if (
70
+ BLOCKED_HOSTS.some((h) => hostname === h || hostname.endsWith(`.${h}`))
71
+ ) {
72
+ return false;
73
+ }
74
+ if (isPrivateOrLoopbackIP(hostname)) {
75
+ return false;
76
+ }
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function sanitizePath(filePath: string): string | null {
84
+ // Prevent reading sensitive files
85
+ const blocked = ["/etc/", "/root/", "/home/", "~", "/.ssh/", "/.aws/"];
86
+ const normalized = filePath.replace(/^~/, process.env.HOME || "");
87
+ if (blocked.some((b) => normalized.startsWith(b))) {
88
+ return null;
89
+ }
90
+ return normalized;
91
+ }
92
+
93
+ const DEFAULT_CONFIG: Partial<Config> = {
94
+ apiBaseUrl: process.env.API_BASE_URL || "http://localhost:50505/api",
95
+ defaultSpace: process.env.DEFAULT_SPACE || process.env.XMEMORY_SPACE_ID,
96
+ apiKey: process.env.XMEMORY_API_KEY || "",
97
+ };
98
+
99
+ function findConfigFile(): string | null {
100
+ const searchPaths = [
101
+ ".xmemory.json",
102
+ "./.xmemory.json",
103
+ `${process.env.HOME || ""}/.xmemory.json`,
104
+ `${process.env.USERPROFILE || ""}/.xmemory.json`,
105
+ ].filter(Boolean);
106
+
107
+ try {
108
+ const fs = require("fs");
109
+ for (const p of searchPaths) {
110
+ if (fs.existsSync(p)) {
111
+ return p;
112
+ }
113
+ }
114
+ } catch {
115
+ // ignore
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function loadConfigFile(): Partial<Config> {
121
+ const configPath = findConfigFile();
122
+ if (!configPath) {
123
+ return {};
124
+ }
125
+
126
+ const sanitized = sanitizePath(configPath);
127
+ if (!sanitized) {
128
+ console.error(`Blocked config path: ${configPath}`);
129
+ return {};
130
+ }
131
+
132
+ try {
133
+ const fs = require("fs");
134
+ const stat = fs.statSync(sanitized);
135
+ if ((stat.mode & 0o077) !== 0) {
136
+ console.warn(
137
+ `Warning: config file ${configPath} is readable by others. Run: chmod 600 ${configPath}`,
138
+ );
139
+ }
140
+ const content = fs.readFileSync(sanitized, "utf-8");
141
+ return JSON.parse(content);
142
+ } catch (e) {
143
+ console.error(`Failed to load config from ${configPath}:`, e);
144
+ return {};
145
+ }
146
+ }
147
+
148
+ export function loadConfig(cliOverrides?: {
149
+ apiBaseUrl?: string;
150
+ apiKey?: string;
151
+ defaultSpace?: string;
152
+ }): Config {
153
+ const fileConfig = loadConfigFile();
154
+ const baseUrl =
155
+ cliOverrides?.apiBaseUrl ||
156
+ fileConfig.apiBaseUrl ||
157
+ DEFAULT_CONFIG.apiBaseUrl ||
158
+ "http://localhost:50505/api";
159
+ if (!isUrlAllowed(baseUrl)) {
160
+ console.error(`Blocked API base URL: ${baseUrl}`);
161
+ process.exit(1);
162
+ }
163
+ const apiKey =
164
+ cliOverrides?.apiKey || DEFAULT_CONFIG.apiKey || fileConfig.apiKey || "";
165
+ if (!apiKey) {
166
+ console.error(
167
+ "Error: XMEMORY_API_KEY is not set. Set the environment variable or apiKey in config.",
168
+ );
169
+ process.exit(1);
170
+ }
171
+ return {
172
+ apiBaseUrl: baseUrl,
173
+ defaultSpace:
174
+ cliOverrides?.defaultSpace ||
175
+ DEFAULT_CONFIG.defaultSpace ||
176
+ fileConfig.defaultSpace,
177
+ apiKey,
178
+ };
179
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { Client } from "./api/client.js";
2
+ import { Config, loadConfig } from "./config/index.js";
3
+ import { SearchCommand } from "./commands/search.js";
4
+ import { AddCommand } from "./commands/add.js";
5
+ import { ListCommand } from "./commands/list.js";
6
+ import { DeleteCommand } from "./commands/delete.js";
7
+ import { GetCommand } from "./commands/get.js";
8
+ import { SpacesCommand } from "./commands/spaces.js";
9
+ import { TagsCommand } from "./commands/tags.js";
10
+ import { GraphCommand } from "./commands/graph.js";
11
+
12
+ export interface GlobalOptions {
13
+ config?: string;
14
+ apiKey?: string;
15
+ apiBaseUrl?: string;
16
+ space?: string;
17
+ output?: "json" | "text";
18
+ }
19
+
20
+ export { Config, loadConfig };
21
+ export { Client };
22
+ export {
23
+ SearchCommand,
24
+ AddCommand,
25
+ ListCommand,
26
+ DeleteCommand,
27
+ GetCommand,
28
+ SpacesCommand,
29
+ TagsCommand,
30
+ GraphCommand,
31
+ };