@xmemo/client 0.4.154 → 0.4.156

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/src/http.js ADDED
@@ -0,0 +1,160 @@
1
+ import {
2
+ CLI_VERSION,
3
+ COMMAND_NAME
4
+ } from './constants.js';
5
+ import { UsageError } from './errors.js';
6
+
7
+ export async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
8
+ const url = endpointUrl(baseUrl, '/mcp');
9
+ const controller = new AbortController();
10
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
11
+ try {
12
+ const response = await io.fetch(url, {
13
+ method: 'POST',
14
+ headers: {
15
+ accept: 'application/json, text/event-stream',
16
+ 'content-type': 'application/json',
17
+ authorization: `Bearer ${token}`,
18
+ 'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
19
+ },
20
+ body: JSON.stringify({
21
+ jsonrpc: '2.0',
22
+ id: 1,
23
+ method: 'initialize',
24
+ params: {
25
+ protocolVersion: '2024-11-05',
26
+ capabilities: {},
27
+ clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
28
+ }
29
+ }),
30
+ signal: controller.signal
31
+ });
32
+ return {
33
+ ok: response.ok,
34
+ detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
35
+ };
36
+ } catch (error) {
37
+ return {
38
+ ok: false,
39
+ detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
40
+ };
41
+ } finally {
42
+ clearTimeout(timeout);
43
+ }
44
+ }
45
+
46
+ export async function probe(url, timeoutMs, io) {
47
+ if (typeof io.fetch !== 'function') {
48
+ return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
49
+ }
50
+
51
+ const controller = new AbortController();
52
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
53
+
54
+ try {
55
+ const response = await io.fetch(url, {
56
+ headers: { accept: 'application/json' },
57
+ signal: controller.signal
58
+ });
59
+ return { url, ok: response.ok, status: response.status };
60
+ } catch (error) {
61
+ return {
62
+ url,
63
+ ok: false,
64
+ error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
65
+ };
66
+ } finally {
67
+ clearTimeout(timeout);
68
+ }
69
+ }
70
+
71
+ export async function fetchJson(url, timeoutMs, io) {
72
+ if (typeof io.fetch !== 'function') {
73
+ throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
74
+ }
75
+
76
+ const controller = new AbortController();
77
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
78
+
79
+ try {
80
+ const response = await io.fetch(url, {
81
+ headers: { accept: 'application/json' },
82
+ signal: controller.signal
83
+ });
84
+ if (!response.ok) {
85
+ throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
86
+ }
87
+ return await response.json();
88
+ } catch (error) {
89
+ if (error instanceof UsageError) {
90
+ throw error;
91
+ }
92
+ const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
93
+ throw new UsageError(`Discovery request failed: ${url} (${reason})`);
94
+ } finally {
95
+ clearTimeout(timeout);
96
+ }
97
+ }
98
+
99
+ export async function postJson(url, payload, timeoutMs, io, options = {}) {
100
+ if (typeof io.fetch !== 'function') {
101
+ throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
102
+ }
103
+
104
+ const controller = new AbortController();
105
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
106
+
107
+ try {
108
+ const response = await io.fetch(url, {
109
+ method: 'POST',
110
+ headers: {
111
+ accept: 'application/json',
112
+ 'content-type': 'application/json'
113
+ },
114
+ body: JSON.stringify(payload),
115
+ signal: controller.signal
116
+ });
117
+ const responsePayload = await response.json();
118
+ if (!response.ok) {
119
+ const error = responsePayload?.error ?? responsePayload?.detail ?? `HTTP ${response.status}`;
120
+ if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
121
+ return { error };
122
+ }
123
+ throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
124
+ }
125
+ return responsePayload;
126
+ } catch (error) {
127
+ if (error instanceof UsageError) {
128
+ throw error;
129
+ }
130
+ const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
131
+ throw new UsageError(`Request failed: ${url} (${reason})`);
132
+ } finally {
133
+ clearTimeout(timeout);
134
+ }
135
+ }
136
+
137
+ export function normalizeBaseUrl(input) {
138
+ try {
139
+ const parsed = new URL(input);
140
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
141
+ throw new UsageError('URL must use http or https.');
142
+ }
143
+ parsed.hash = '';
144
+ parsed.search = '';
145
+ return parsed.toString().replace(/\/$/, '');
146
+ } catch (error) {
147
+ if (error instanceof UsageError) {
148
+ throw error;
149
+ }
150
+ throw new UsageError(`Invalid URL: ${input}`);
151
+ }
152
+ }
153
+
154
+ export function endpointUrl(baseUrl, pathname) {
155
+ const url = new URL(baseUrl);
156
+ url.pathname = pathname;
157
+ url.hash = '';
158
+ url.search = '';
159
+ return url.toString();
160
+ }
package/src/io.js ADDED
@@ -0,0 +1,16 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export function defaultIo() {
4
+ return {
5
+ env: process.env,
6
+ stdin: process.stdin,
7
+ stdout: process.stdout,
8
+ stderr: process.stderr,
9
+ fetch: globalThis.fetch,
10
+ spawn
11
+ };
12
+ }
13
+
14
+ export function writeLine(stream, line) {
15
+ stream.write(`${line}\n`);
16
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ appendTomlServerConfig,
3
+ codexTomlSnippet
4
+ } from './codex.js';
5
+ import {
6
+ hermesYamlSnippet,
7
+ mergeHermesMcpConfig
8
+ } from './hermes.js';
9
+ import {
10
+ JSON_MCP_CLIENT_DEFINITIONS,
11
+ jsonClientSnippet,
12
+ mergeJsonClientMcpConfig
13
+ } from './json-clients.js';
14
+ import {
15
+ defaultAntigravity2ConfigPath,
16
+ defaultAntigravityCliConfigPath,
17
+ defaultAntigravityConfigPath,
18
+ defaultAntigravityIdeConfigPath,
19
+ defaultClaudeConfigPath,
20
+ defaultClaudecodeConfigPath,
21
+ defaultClineConfigPath,
22
+ defaultCodexConfigPath,
23
+ defaultContinueConfigPath,
24
+ defaultCursorConfigPath,
25
+ defaultGeminiConfigPath,
26
+ defaultHermesConfigPath,
27
+ defaultJetbrainsConfigPath,
28
+ defaultKiroConfigPath,
29
+ defaultOpencodeConfigPath,
30
+ defaultOpenclawConfigPath,
31
+ defaultQwenConfigPath,
32
+ defaultTraeConfigPath,
33
+ defaultTraeSoloConfigPath,
34
+ defaultWindsurfConfigPath,
35
+ defaultZedConfigPath
36
+ } from '../path-config.js';
37
+ import {
38
+ createMcpClients,
39
+ supportedMcpClientIds as registrySupportedMcpClientIds,
40
+ supportedMcpClients as registrySupportedMcpClients
41
+ } from './registry.js';
42
+
43
+ export const MCP_CLIENTS = createMcpClients({
44
+ JSON_MCP_CLIENT_DEFINITIONS,
45
+ defaultCodexConfigPath,
46
+ codexTomlSnippet,
47
+ appendTomlServerConfig,
48
+ defaultHermesConfigPath,
49
+ hermesYamlSnippet,
50
+ mergeHermesMcpConfig,
51
+ jsonClientSnippet,
52
+ mergeJsonClientMcpConfig,
53
+ defaultCursorConfigPath,
54
+ defaultGeminiConfigPath,
55
+ defaultAntigravityConfigPath,
56
+ defaultAntigravityIdeConfigPath,
57
+ defaultAntigravity2ConfigPath,
58
+ defaultAntigravityCliConfigPath,
59
+ defaultWindsurfConfigPath,
60
+ defaultClineConfigPath,
61
+ defaultContinueConfigPath,
62
+ defaultClaudeConfigPath,
63
+ defaultOpenclawConfigPath,
64
+ defaultKiroConfigPath,
65
+ defaultZedConfigPath,
66
+ defaultJetbrainsConfigPath,
67
+ defaultOpencodeConfigPath,
68
+ defaultQwenConfigPath,
69
+ defaultTraeConfigPath,
70
+ defaultTraeSoloConfigPath,
71
+ defaultClaudecodeConfigPath
72
+ });
73
+
74
+ export function supportedMcpClients() {
75
+ return registrySupportedMcpClients(MCP_CLIENTS);
76
+ }
77
+
78
+ export function supportedMcpClientIds() {
79
+ return registrySupportedMcpClientIds(MCP_CLIENTS);
80
+ }
@@ -0,0 +1,147 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ COMMAND_NAME,
6
+ LEGACY_MCP_SERVER_NAMES,
7
+ MCP_SERVER_NAME,
8
+ TOKEN_ENV_VAR
9
+ } from '../constants.js';
10
+ import { UsageError } from '../errors.js';
11
+ import {
12
+ bestEffortChmod,
13
+ escapeRegExp,
14
+ escapeTomlString,
15
+ fileExists,
16
+ readTextIfExists,
17
+ unescapeTomlString
18
+ } from '../runtime.js';
19
+ import { agentInstanceIdentityPath } from './identity.js';
20
+
21
+ export function codexTomlSnippet(mcpUrl) {
22
+ return `[mcp_servers.${MCP_SERVER_NAME}]
23
+ url = "${escapeTomlString(mcpUrl)}"
24
+ bearer_token_env_var = "${TOKEN_ENV_VAR}"
25
+ `;
26
+ }
27
+
28
+ export async function codexSmokeReport(configPath, env) {
29
+ const configText = await readTextIfExists(configPath);
30
+ const serverBlock = findTomlServerBlock(configText);
31
+ const block = serverBlock.block;
32
+ const mcpUrl = block ? tomlStringValue(block, 'url') : null;
33
+ const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
34
+ const tokenValue = env[TOKEN_ENV_VAR] ?? '';
35
+ const identityPath = agentInstanceIdentityPath(env, 'codex');
36
+ const identityPresent = await fileExists(identityPath);
37
+ const checks = [
38
+ {
39
+ name: 'config_present',
40
+ ok: configText.trim().length > 0,
41
+ required: true,
42
+ detail: configText.trim().length > 0 ? 'found' : 'missing'
43
+ },
44
+ {
45
+ name: 'memory_os_server_present',
46
+ ok: Boolean(block),
47
+ required: true,
48
+ detail: block ? `[mcp_servers.${serverBlock.name}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
49
+ },
50
+ {
51
+ name: 'mcp_url_present',
52
+ ok: Boolean(mcpUrl),
53
+ required: true,
54
+ detail: mcpUrl ?? 'missing url'
55
+ },
56
+ {
57
+ name: 'bearer_token_env_var',
58
+ ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
59
+ required: true,
60
+ detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
61
+ },
62
+ {
63
+ name: 'token_env_present',
64
+ ok: Boolean(env[TOKEN_ENV_VAR]),
65
+ required: true,
66
+ detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
67
+ },
68
+ {
69
+ name: 'token_not_embedded_in_config',
70
+ ok: !tokenValue || !configText.includes(tokenValue),
71
+ required: true,
72
+ detail: 'token value not printed or embedded'
73
+ },
74
+ {
75
+ name: 'agent_instance_identity_file',
76
+ ok: identityPresent,
77
+ required: false,
78
+ detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
79
+ }
80
+ ];
81
+
82
+ return {
83
+ ok: checks.every((check) => !check.required || check.ok),
84
+ client: 'codex',
85
+ configPath,
86
+ serverName: serverBlock.name ?? MCP_SERVER_NAME,
87
+ mcpUrl,
88
+ tokenEnvVar: TOKEN_ENV_VAR,
89
+ agentInstanceIdPath: identityPath,
90
+ checks
91
+ };
92
+ }
93
+
94
+ export async function appendTomlServerConfig(configPath, mcpUrl) {
95
+ const snippet = codexTomlSnippet(mcpUrl);
96
+ const existing = await readTextIfExists(configPath);
97
+ const existingName = existingTomlMcpServerName(existing);
98
+ if (existingName) {
99
+ throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
100
+ }
101
+
102
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
103
+ const prefix = existing.trim().length === 0 ? '' : '\n\n';
104
+ await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
105
+ await bestEffortChmod(configPath, 0o600);
106
+ }
107
+
108
+ function knownMcpServerNames() {
109
+ return [MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES];
110
+ }
111
+
112
+ function existingTomlMcpServerName(content) {
113
+ return knownMcpServerNames().find((name) => content.includes(`[mcp_servers.${name}]`));
114
+ }
115
+
116
+ function findTomlServerBlock(content) {
117
+ const name = existingTomlMcpServerName(content);
118
+ return {
119
+ name: name ?? null,
120
+ block: name ? tomlServerBlock(content, name) : ''
121
+ };
122
+ }
123
+
124
+ function tomlServerBlock(content, serverName) {
125
+ const header = `[mcp_servers.${serverName}]`;
126
+ const lines = content.split(/\r?\n/);
127
+ const start = lines.findIndex((line) => line.trim() === header);
128
+ if (start === -1) {
129
+ return '';
130
+ }
131
+
132
+ const block = [];
133
+ for (let index = start + 1; index < lines.length; index += 1) {
134
+ const line = lines[index];
135
+ if (/^\s*\[/.test(line)) {
136
+ break;
137
+ }
138
+ block.push(line);
139
+ }
140
+ return block.join('\n');
141
+ }
142
+
143
+ function tomlStringValue(block, key) {
144
+ const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
145
+ const match = block.match(pattern);
146
+ return match ? unescapeTomlString(match[1]) : null;
147
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { MCP_SERVER_NAME } from '../constants.js';
5
+ import { UsageError } from '../errors.js';
6
+ import {
7
+ bestEffortChmod,
8
+ isPlainObject,
9
+ parseJsonConfig,
10
+ readTextIfExists
11
+ } from '../runtime.js';
12
+ import { existingJsonMcpServerName } from './names.js';
13
+ export { mcpProxyCommand } from './proxy.js';
14
+
15
+ export async function mergeCopilotMcpConfig(configPath, proxyUrl) {
16
+ const existing = await readTextIfExists(configPath);
17
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
18
+
19
+ if (!isPlainObject(parsed)) {
20
+ throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
21
+ }
22
+
23
+ if (!isPlainObject(parsed.mcpServers)) {
24
+ parsed.mcpServers = {};
25
+ }
26
+
27
+ const existingName = existingJsonMcpServerName(parsed.mcpServers);
28
+ if (existingName) {
29
+ throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
30
+ }
31
+
32
+ parsed.mcpServers[MCP_SERVER_NAME] = copilotLocalProxyServerConfig(proxyUrl);
33
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
34
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
35
+ await bestEffortChmod(configPath, 0o600);
36
+ }
37
+
38
+ export function copilotLocalProxyServerConfig(proxyUrl) {
39
+ return {
40
+ type: 'http',
41
+ url: proxyUrl
42
+ };
43
+ }
@@ -0,0 +1,50 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { fileExists } from '../runtime.js';
5
+ import { defaultCopilotConfigPath } from './paths.js';
6
+
7
+ export async function detectClient(clientId, env, mcpClients) {
8
+ let filePaths = [];
9
+ if (clientId === 'copilot-cli' || clientId === 'copilot') {
10
+ if (process.platform === 'win32' && env.APPDATA) {
11
+ filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'mcp.json'));
12
+ } else {
13
+ const home = env.HOME || os.homedir();
14
+ if (process.platform === 'darwin') {
15
+ filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'));
16
+ }
17
+ filePaths.push(path.join(home, '.config', 'Code', 'User', 'mcp.json'));
18
+ }
19
+ filePaths.push(defaultCopilotConfigPath(env));
20
+ } else {
21
+ const client = mcpClients.get(clientId);
22
+ if (client) {
23
+ filePaths.push(client.defaultConfigPath(env));
24
+ }
25
+ }
26
+
27
+ if (clientId === 'cline') {
28
+ if (process.platform === 'win32' && env.APPDATA) {
29
+ filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
30
+ } else {
31
+ const home = env.HOME || os.homedir();
32
+ if (process.platform === 'darwin') {
33
+ filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
34
+ }
35
+ filePaths.push(path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
36
+ }
37
+ }
38
+
39
+ for (const filePath of filePaths) {
40
+ if (await fileExists(filePath)) {
41
+ return { detected: true, path: filePath };
42
+ }
43
+ const parentDir = path.dirname(filePath);
44
+ if (await fileExists(parentDir)) {
45
+ return { detected: true, path: filePath };
46
+ }
47
+ }
48
+
49
+ return { detected: false };
50
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ AGENT_INSTANCE_ENV_VAR,
6
+ MCP_SERVER_NAME,
7
+ TOKEN_ENV_VAR
8
+ } from '../constants.js';
9
+ import { UsageError } from '../errors.js';
10
+ import {
11
+ bestEffortChmod,
12
+ readTextIfExists
13
+ } from '../runtime.js';
14
+ import { envReferenceIdentity } from './identity.js';
15
+
16
+ export function hermesYamlSnippet(mcpUrl, identity = envReferenceIdentity('hermes')) {
17
+ return `mcp_servers:
18
+ ${MCP_SERVER_NAME}:
19
+ command: npx
20
+ args:
21
+ - -y
22
+ - mcp-remote
23
+ - ${mcpUrl}
24
+ - --header
25
+ - "Authorization:Bearer \${${TOKEN_ENV_VAR}}"
26
+ - --header
27
+ - "X-Memory-OS-Agent-ID:${identity.agentId}"
28
+ - --header
29
+ - "X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}"
30
+ env:
31
+ ${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
32
+ ${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"
33
+ `;
34
+ }
35
+
36
+ export async function mergeHermesMcpConfig(configPath, mcpUrl, identity) {
37
+ const existing = await readTextIfExists(configPath);
38
+
39
+ if (existing.includes(`${MCP_SERVER_NAME}:`) || existing.includes('memory_os:') || existing.includes('memory-os:')) {
40
+ throw new UsageError(`MCP config already contains ${MCP_SERVER_NAME} in mcp_servers. Edit ${configPath} manually to avoid duplicate server definitions.`);
41
+ }
42
+
43
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
44
+
45
+ if (existing.trim().length === 0) {
46
+ await fs.writeFile(configPath, hermesYamlSnippet(mcpUrl, identity), { mode: 0o600 });
47
+ } else if (existing.includes('mcp_servers:')) {
48
+ const replacement = `mcp_servers:
49
+ ${MCP_SERVER_NAME}:
50
+ command: npx
51
+ args:
52
+ - -y
53
+ - mcp-remote
54
+ - ${mcpUrl}
55
+ - --header
56
+ - "Authorization:Bearer \${${TOKEN_ENV_VAR}}"
57
+ - --header
58
+ - "X-Memory-OS-Agent-ID:${identity.agentId}"
59
+ - --header
60
+ - "X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}"
61
+ env:
62
+ ${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
63
+ ${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"`;
64
+ const updated = existing.replace('mcp_servers:', replacement);
65
+ await fs.writeFile(configPath, updated, { mode: 0o600 });
66
+ } else {
67
+ const prefix = existing.endsWith('\n') ? '' : '\n';
68
+ await fs.appendFile(configPath, `${prefix}${hermesYamlSnippet(mcpUrl, identity)}`, { mode: 0o600 });
69
+ }
70
+ await bestEffortChmod(configPath, 0o600);
71
+ }
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { AGENT_INSTANCE_ENV_VAR } from '../constants.js';
6
+ import { bestEffortChmod, parseJsonConfig, readTextIfExists } from '../runtime.js';
7
+ import { stringValue } from '../args.js';
8
+ import { configRoot } from './paths.js';
9
+
10
+ function identityClientId(clientId) {
11
+ return (clientId === 'antigravity-ide' || clientId === 'antigravity2' || clientId === 'antigravity-cli')
12
+ ? 'antigravity'
13
+ : clientId;
14
+ }
15
+
16
+ export async function agentIdentity(clientId, env) {
17
+ const targetClientId = identityClientId(clientId);
18
+ const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
19
+ if (configuredInstanceId) {
20
+ return {
21
+ agentId: targetClientId,
22
+ agentInstanceId: configuredInstanceId,
23
+ path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
24
+ };
25
+ }
26
+
27
+ const identityPath = agentInstanceIdentityPath(env, targetClientId);
28
+ const existing = await readAgentInstanceIdentity(identityPath);
29
+ if (existing) {
30
+ return { agentId: targetClientId, agentInstanceId: existing, path: identityPath };
31
+ }
32
+
33
+ const generated = `xmemo-${targetClientId}-${randomUUID()}`;
34
+ await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
35
+ await bestEffortChmod(path.dirname(identityPath), 0o700);
36
+ await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: targetClientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
37
+ await bestEffortChmod(identityPath, 0o600);
38
+ return { agentId: targetClientId, agentInstanceId: generated, path: identityPath };
39
+ }
40
+
41
+ export async function readAgentInstanceIdentity(identityPath) {
42
+ const existing = await readTextIfExists(identityPath);
43
+ if (!existing.trim()) {
44
+ return null;
45
+ }
46
+ const parsed = parseJsonConfig(existing, identityPath);
47
+ const value = stringValue(parsed, ['agentInstanceId']);
48
+ return value || null;
49
+ }
50
+
51
+ export function agentInstanceIdentityPath(env, clientId) {
52
+ return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
53
+ }
54
+
55
+ export function envReferenceIdentity(clientId) {
56
+ const targetClientId = identityClientId(clientId);
57
+ return {
58
+ agentId: targetClientId,
59
+ agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
60
+ path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
61
+ };
62
+ }