@magclaw/cli-core 0.1.36 → 0.1.38
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/package.json +1 -1
- package/src/cli.js +627 -6
- package/src/team-memory-hooks.js +319 -0
- package/src/team-sharing.js +663 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { installTeamMemoryHookConfig } from './team-memory-hooks.js';
|
|
9
|
+
|
|
10
|
+
export const TEAM_SHARING_PACKAGE_NAME = 'team-sharing';
|
|
11
|
+
export const TEAM_SHARING_INTEGRATION = 'team-sharing';
|
|
12
|
+
const DEFAULT_PROFILE = 'default';
|
|
13
|
+
const DEFAULT_SERVER_URL = 'http://127.0.0.1:6543';
|
|
14
|
+
|
|
15
|
+
function now() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function homeDirForEnv(env = process.env) {
|
|
20
|
+
return env.HOME || env.USERPROFILE || os.homedir();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeProfileName(value = DEFAULT_PROFILE) {
|
|
24
|
+
return String(value || DEFAULT_PROFILE).trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || DEFAULT_PROFILE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeServerUrl(value = '') {
|
|
28
|
+
return String(value || DEFAULT_SERVER_URL).trim().replace(/\/+$/, '') || DEFAULT_SERVER_URL;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeRuntime(value = '') {
|
|
32
|
+
const runtime = String(value || '').trim().toLowerCase();
|
|
33
|
+
if (runtime === 'claude' || runtime === 'claude-code') return 'claude_code';
|
|
34
|
+
if (runtime === 'claude_code') return 'claude_code';
|
|
35
|
+
if (runtime === 'codex') return 'codex';
|
|
36
|
+
return runtime || 'codex';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function boolFlag(value, fallback = true) {
|
|
40
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
41
|
+
return !['0', 'false', 'no', 'off'].includes(String(value).trim().toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function yamlScalar(value) {
|
|
45
|
+
if (value === true) return 'true';
|
|
46
|
+
if (value === false) return 'false';
|
|
47
|
+
if (value === null || value === undefined) return '';
|
|
48
|
+
const text = String(value);
|
|
49
|
+
if (!text) return '';
|
|
50
|
+
if (/^[A-Za-z0-9_./:@?=&%+\-]+$/.test(text)) return text;
|
|
51
|
+
return JSON.stringify(text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeYamlLines(value, indent = 0) {
|
|
55
|
+
const pad = ' '.repeat(indent);
|
|
56
|
+
const lines = [];
|
|
57
|
+
for (const [key, item] of Object.entries(value || {})) {
|
|
58
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
59
|
+
lines.push(`${pad}${key}:`);
|
|
60
|
+
lines.push(...writeYamlLines(item, indent + 2));
|
|
61
|
+
} else {
|
|
62
|
+
lines.push(`${pad}${key}: ${yamlScalar(item)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stringifyTeamSharingYaml(value = {}) {
|
|
69
|
+
return `${writeYamlLines(value).join('\n')}\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseScalar(value = '') {
|
|
73
|
+
const text = String(value || '').trim();
|
|
74
|
+
if (!text) return '';
|
|
75
|
+
if (text === 'true') return true;
|
|
76
|
+
if (text === 'false') return false;
|
|
77
|
+
if (text === 'null') return null;
|
|
78
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return text.slice(1, -1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseTeamSharingYaml(text = '') {
|
|
89
|
+
const root = {};
|
|
90
|
+
const stack = [{ indent: -1, value: root }];
|
|
91
|
+
for (const rawLine of String(text || '').split(/\r?\n/)) {
|
|
92
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith('#')) continue;
|
|
93
|
+
const match = rawLine.match(/^(\s*)([^:]+):(.*)$/);
|
|
94
|
+
if (!match) continue;
|
|
95
|
+
const indent = match[1].length;
|
|
96
|
+
const key = match[2].trim();
|
|
97
|
+
const rest = match[3].trim();
|
|
98
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
99
|
+
const parent = stack[stack.length - 1].value;
|
|
100
|
+
if (!rest) {
|
|
101
|
+
const child = {};
|
|
102
|
+
parent[key] = child;
|
|
103
|
+
stack.push({ indent, value: child });
|
|
104
|
+
} else {
|
|
105
|
+
parent[key] = parseScalar(rest);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return root;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readYamlFile(file, fallback = null) {
|
|
112
|
+
try {
|
|
113
|
+
return parseTeamSharingYaml(await readFile(file, 'utf8'));
|
|
114
|
+
} catch {
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeYamlFile(file, value, { privateFile = false } = {}) {
|
|
120
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
121
|
+
await writeFile(file, stringifyTeamSharingYaml(value));
|
|
122
|
+
if (privateFile) await chmod(file, 0o600).catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function teamSharingPaths({ profile = DEFAULT_PROFILE, cwd = process.cwd(), env = process.env } = {}) {
|
|
126
|
+
const home = homeDirForEnv(env);
|
|
127
|
+
const sharingHome = path.resolve(env.MAGCLAW_TEAM_SHARING_HOME || path.join(home, '.magclaw', 'team-sharing'));
|
|
128
|
+
const projectDir = path.resolve(cwd || process.cwd());
|
|
129
|
+
const cleanProfile = safeProfileName(profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
130
|
+
return {
|
|
131
|
+
profile: cleanProfile,
|
|
132
|
+
sharingHome,
|
|
133
|
+
profileConfig: path.join(sharingHome, 'profiles', cleanProfile, 'config.yaml'),
|
|
134
|
+
projectsConfig: path.join(sharingHome, 'projects.yaml'),
|
|
135
|
+
versionCache: path.join(sharingHome, 'version-cache.json'),
|
|
136
|
+
projectConfig: path.join(projectDir, '.magclaw', 'team-sharing.yaml'),
|
|
137
|
+
legacyProjectConfig: path.join(projectDir, '.magclaw', 'team-memory.json'),
|
|
138
|
+
projectCursor: path.join(projectDir, '.magclaw', 'team-memory-cursor.json'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function readTeamSharingProfileConfig(profile = DEFAULT_PROFILE, env = process.env) {
|
|
143
|
+
const paths = teamSharingPaths({ profile, env });
|
|
144
|
+
return {
|
|
145
|
+
paths,
|
|
146
|
+
config: await readYamlFile(paths.profileConfig, {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function writeTeamSharingProfileConfig(profile, config, env = process.env) {
|
|
151
|
+
const paths = teamSharingPaths({ profile, env });
|
|
152
|
+
await writeYamlFile(paths.profileConfig, config, { privateFile: true });
|
|
153
|
+
return paths.profileConfig;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function readTeamSharingProjectConfig({ profile = DEFAULT_PROFILE, cwd = process.cwd(), env = process.env } = {}) {
|
|
157
|
+
const paths = teamSharingPaths({ profile, cwd, env });
|
|
158
|
+
return {
|
|
159
|
+
paths,
|
|
160
|
+
config: await readYamlFile(paths.projectConfig, null),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function convertTeamSharingProjectToMemoryConfig(config = {}) {
|
|
165
|
+
if (!config) return null;
|
|
166
|
+
return {
|
|
167
|
+
version: Number(config.version || 1),
|
|
168
|
+
enabled: config.enabled !== false,
|
|
169
|
+
profile: safeProfileName(config.profile || DEFAULT_PROFILE),
|
|
170
|
+
serverUrl: normalizeServerUrl(config.server_url || config.serverUrl),
|
|
171
|
+
workspaceId: String(config.workspace_id || config.workspaceId || 'local'),
|
|
172
|
+
channelId: String(config.channel?.id || ''),
|
|
173
|
+
channelPath: String(config.channel?.path || ''),
|
|
174
|
+
routingMode: String(config.routing_mode || config.routingMode || 'fixed_single_channel'),
|
|
175
|
+
projectKey: String(config.project_key || config.projectKey || 'default'),
|
|
176
|
+
enabledSince: String(config.enabled_since || config.enabledSince || ''),
|
|
177
|
+
runtimes: {
|
|
178
|
+
codex: {
|
|
179
|
+
hooksEnabled: config.runtimes?.codex?.hooks_enabled !== false,
|
|
180
|
+
skillsEnabled: config.runtimes?.codex?.skills_enabled !== false,
|
|
181
|
+
},
|
|
182
|
+
claude_code: {
|
|
183
|
+
hooksEnabled: config.runtimes?.claude_code?.hooks_enabled !== false,
|
|
184
|
+
skillsEnabled: config.runtimes?.claude_code?.skills_enabled !== false,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function registerTeamSharingProject(paths, config) {
|
|
191
|
+
const registry = await readYamlFile(paths.projectsConfig, {});
|
|
192
|
+
registry.version = 1;
|
|
193
|
+
registry.projects = registry.projects && typeof registry.projects === 'object' ? registry.projects : {};
|
|
194
|
+
const key = String(config.project_key || config.projectKey || path.basename(path.dirname(path.dirname(paths.projectConfig)))).replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
195
|
+
registry.projects[key || 'default'] = {
|
|
196
|
+
path: path.dirname(path.dirname(paths.projectConfig)),
|
|
197
|
+
project_key: config.project_key || key || 'default',
|
|
198
|
+
channel_path: config.channel?.path || '',
|
|
199
|
+
channel_id: config.channel?.id || '',
|
|
200
|
+
profile: config.profile || DEFAULT_PROFILE,
|
|
201
|
+
updated_at: now(),
|
|
202
|
+
};
|
|
203
|
+
await writeYamlFile(paths.projectsConfig, registry, { privateFile: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function initTeamSharingProject(flags = {}, env = process.env) {
|
|
207
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
208
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
209
|
+
const paths = teamSharingPaths({ profile, cwd, env });
|
|
210
|
+
const existing = await readYamlFile(paths.projectConfig, {});
|
|
211
|
+
const channel = String(flags.channel || flags.channelPath || flags.channelId || flags._?.[1] || existing.channel?.path || existing.channel?.id || '').trim();
|
|
212
|
+
if (!channel) throw new Error('Usage: magclaw team-sharing init --channel <channelPathOrId>');
|
|
213
|
+
const channelIsPath = /^(https?|feishu|lark|mc):/i.test(channel);
|
|
214
|
+
const projectKey = String(flags.projectKey || flags.project || existing.project_key || path.basename(cwd)).trim();
|
|
215
|
+
const config = {
|
|
216
|
+
version: 1,
|
|
217
|
+
enabled: boolFlag(flags.enabled, existing.enabled !== false),
|
|
218
|
+
profile,
|
|
219
|
+
server_url: normalizeServerUrl(flags.serverUrl || existing.server_url || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL),
|
|
220
|
+
workspace_id: String(flags.workspaceId || flags.workspace || existing.workspace_id || env.MAGCLAW_WORKSPACE_ID || 'local').trim(),
|
|
221
|
+
project_key: projectKey,
|
|
222
|
+
routing_mode: 'fixed_single_channel',
|
|
223
|
+
channel: {
|
|
224
|
+
id: String(flags.channelId || (!channelIsPath ? channel : existing.channel?.id || '')).trim(),
|
|
225
|
+
path: String(flags.channelPath || (channelIsPath ? channel : existing.channel?.path || '')).trim(),
|
|
226
|
+
},
|
|
227
|
+
runtimes: {
|
|
228
|
+
codex: {
|
|
229
|
+
hooks_enabled: boolFlag(flags.codexHooksEnabled, existing.runtimes?.codex?.hooks_enabled !== false),
|
|
230
|
+
skills_enabled: boolFlag(flags.codexSkillsEnabled, existing.runtimes?.codex?.skills_enabled !== false),
|
|
231
|
+
},
|
|
232
|
+
claude_code: {
|
|
233
|
+
hooks_enabled: boolFlag(flags.claudeHooksEnabled, existing.runtimes?.claude_code?.hooks_enabled !== false),
|
|
234
|
+
skills_enabled: boolFlag(flags.claudeSkillsEnabled, existing.runtimes?.claude_code?.skills_enabled !== false),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
enabled_since: String(flags.enabledSince || existing.enabled_since || now()),
|
|
238
|
+
upgrade: {
|
|
239
|
+
check_interval_hours: String(flags.upgradeCheckIntervalHours || existing.upgrade?.check_interval_hours || 24),
|
|
240
|
+
},
|
|
241
|
+
created_at: existing.created_at || now(),
|
|
242
|
+
updated_at: now(),
|
|
243
|
+
};
|
|
244
|
+
await writeYamlFile(paths.projectConfig, config);
|
|
245
|
+
await registerTeamSharingProject(paths, config);
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
projectConfig: paths.projectConfig,
|
|
249
|
+
projectsConfig: paths.projectsConfig,
|
|
250
|
+
profile,
|
|
251
|
+
serverUrl: config.server_url,
|
|
252
|
+
workspaceId: config.workspace_id,
|
|
253
|
+
channelId: config.channel.id,
|
|
254
|
+
channelPath: config.channel.path,
|
|
255
|
+
projectKey,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function listTeamSharingProjects(flags = {}, env = process.env) {
|
|
260
|
+
const paths = teamSharingPaths({ profile: flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || DEFAULT_PROFILE, env });
|
|
261
|
+
const registry = await readYamlFile(paths.projectsConfig, { version: 1, projects: {} });
|
|
262
|
+
return { ok: true, projectsConfig: paths.projectsConfig, projects: registry.projects || {} };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function statusTeamSharingProject(flags = {}, env = process.env) {
|
|
266
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || DEFAULT_PROFILE);
|
|
267
|
+
const project = await readTeamSharingProjectConfig({ profile, cwd: flags.cwd || process.cwd(), env });
|
|
268
|
+
const profileState = await readTeamSharingProfileConfig(profile, env);
|
|
269
|
+
return {
|
|
270
|
+
ok: Boolean(project.config),
|
|
271
|
+
projectConfig: project.paths.projectConfig,
|
|
272
|
+
profileConfig: profileState.paths.profileConfig,
|
|
273
|
+
configured: Boolean(project.config),
|
|
274
|
+
loggedIn: Boolean(profileState.config?.token),
|
|
275
|
+
config: project.config || null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function setTeamSharingProjectEnabled(flags = {}, env = process.env, enabled = true) {
|
|
280
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || DEFAULT_PROFILE);
|
|
281
|
+
const project = await readTeamSharingProjectConfig({ profile, cwd: flags.cwd || process.cwd(), env });
|
|
282
|
+
if (!project.config) throw new Error('Run `magclaw team-sharing init --channel <channel>` first.');
|
|
283
|
+
project.config.enabled = Boolean(enabled);
|
|
284
|
+
project.config.updated_at = now();
|
|
285
|
+
if (enabled && !project.config.enabled_since) project.config.enabled_since = now();
|
|
286
|
+
await writeYamlFile(project.paths.projectConfig, project.config);
|
|
287
|
+
return { ok: true, enabled: Boolean(enabled), projectConfig: project.paths.projectConfig };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function unsetTeamSharingProject(flags = {}, env = process.env) {
|
|
291
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || DEFAULT_PROFILE);
|
|
292
|
+
const project = await readTeamSharingProjectConfig({ profile, cwd: flags.cwd || process.cwd(), env });
|
|
293
|
+
await rm(project.paths.projectConfig, { force: true });
|
|
294
|
+
return { ok: true, removed: Boolean(project.config), projectConfig: project.paths.projectConfig };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function teamSharingRequestJson({ serverUrl, token = '', method = 'GET', pathname = '/', body = null } = {}) {
|
|
298
|
+
const response = await fetch(`${normalizeServerUrl(serverUrl)}${pathname}`, {
|
|
299
|
+
method,
|
|
300
|
+
headers: {
|
|
301
|
+
...(body ? { 'content-type': 'application/json' } : {}),
|
|
302
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
303
|
+
},
|
|
304
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
305
|
+
});
|
|
306
|
+
const data = await response.json().catch(() => ({}));
|
|
307
|
+
if (!response.ok) throw new Error(data.error || data.message || `${response.status} ${response.statusText}`);
|
|
308
|
+
return data;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sleep(ms) {
|
|
312
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function maybeOpenUrl(url, flags = {}, env = process.env) {
|
|
316
|
+
if (!url || flags.noOpen || flags.open === false || env.MAGCLAW_TEAM_SHARING_OPEN_BROWSER === '0') return;
|
|
317
|
+
if (process.platform === 'darwin') spawnSync('open', [url], { stdio: 'ignore' });
|
|
318
|
+
else if (process.platform === 'win32') spawnSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
319
|
+
else spawnSync('xdg-open', [url], { stdio: 'ignore' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function loginTeamSharingProfile(flags = {}, env = process.env) {
|
|
323
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
324
|
+
const existing = (await readTeamSharingProfileConfig(profile, env)).config || {};
|
|
325
|
+
const serverUrl = normalizeServerUrl(flags.serverUrl || existing.server_url || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL);
|
|
326
|
+
const workspaceId = String(flags.workspaceId || flags.workspace || existing.workspace_id || env.MAGCLAW_WORKSPACE_ID || 'local').trim();
|
|
327
|
+
const manualToken = String(flags.token || flags.apiKey || flags.memoryToken || env.MAGCLAW_TEAM_SHARING_TOKEN || env.MAGCLAW_MEMORY_TOKEN || '').trim();
|
|
328
|
+
let token = manualToken;
|
|
329
|
+
let user = {};
|
|
330
|
+
if (!token) {
|
|
331
|
+
const started = await teamSharingRequestJson({
|
|
332
|
+
serverUrl,
|
|
333
|
+
method: 'POST',
|
|
334
|
+
pathname: '/api/team-memory/auth/start',
|
|
335
|
+
body: { workspaceId, profile, packageName: TEAM_SHARING_PACKAGE_NAME },
|
|
336
|
+
});
|
|
337
|
+
maybeOpenUrl(started.verificationUri, flags, env);
|
|
338
|
+
const intervalMs = Math.max(1, Math.min(10_000, Number(started.intervalMs || 2000) || 2000));
|
|
339
|
+
const deadline = Date.now() + Math.max(1000, Number(flags.pollTimeoutMs || 10 * 60_000) || 10 * 60_000);
|
|
340
|
+
while (Date.now() < deadline) {
|
|
341
|
+
const status = await teamSharingRequestJson({
|
|
342
|
+
serverUrl,
|
|
343
|
+
method: 'POST',
|
|
344
|
+
pathname: '/api/team-memory/auth/token',
|
|
345
|
+
body: { deviceCode: started.deviceCode },
|
|
346
|
+
});
|
|
347
|
+
if (status.status === 'approved' && status.token) {
|
|
348
|
+
token = status.token;
|
|
349
|
+
user = status.user || {};
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
if (status.status === 'expired') throw new Error(status.error || 'Team Sharing login expired.');
|
|
353
|
+
await sleep(intervalMs);
|
|
354
|
+
}
|
|
355
|
+
if (!token) throw new Error('Team Sharing login timed out.');
|
|
356
|
+
}
|
|
357
|
+
const config = {
|
|
358
|
+
version: 1,
|
|
359
|
+
profile,
|
|
360
|
+
server_url: serverUrl,
|
|
361
|
+
workspace_id: workspaceId,
|
|
362
|
+
token,
|
|
363
|
+
token_scope: 'team_memory:sync,team_memory:search,team_memory:context,team_memory:feedback',
|
|
364
|
+
user_id: user.id || existing.user_id || '',
|
|
365
|
+
user_email: user.email || existing.user_email || '',
|
|
366
|
+
created_at: existing.created_at || now(),
|
|
367
|
+
updated_at: now(),
|
|
368
|
+
};
|
|
369
|
+
const profileConfig = await writeTeamSharingProfileConfig(profile, config, env);
|
|
370
|
+
return { ok: true, profile, serverUrl, workspaceId, hasToken: Boolean(token), profileConfig, user };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function logoutTeamSharingProfile(flags = {}, env = process.env) {
|
|
374
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
375
|
+
const { paths, config } = await readTeamSharingProfileConfig(profile, env);
|
|
376
|
+
const token = String(config?.token || '').trim();
|
|
377
|
+
if (token) {
|
|
378
|
+
await teamSharingRequestJson({
|
|
379
|
+
serverUrl: config.server_url || flags.serverUrl || DEFAULT_SERVER_URL,
|
|
380
|
+
token,
|
|
381
|
+
method: 'POST',
|
|
382
|
+
pathname: '/api/team-memory/auth/revoke',
|
|
383
|
+
body: { profile },
|
|
384
|
+
}).catch(() => null);
|
|
385
|
+
}
|
|
386
|
+
await rm(paths.profileConfig, { force: true });
|
|
387
|
+
return { ok: true, profile, revoked: Boolean(token), profileConfig: paths.profileConfig };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function whoamiTeamSharingProfile(flags = {}, env = process.env) {
|
|
391
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
392
|
+
const { config } = await readTeamSharingProfileConfig(profile, env);
|
|
393
|
+
const token = String(config?.token || env.MAGCLAW_TEAM_SHARING_TOKEN || '').trim();
|
|
394
|
+
if (!token) throw new Error('Run `magclaw team-sharing login` first.');
|
|
395
|
+
return teamSharingRequestJson({
|
|
396
|
+
serverUrl: flags.serverUrl || config.server_url || DEFAULT_SERVER_URL,
|
|
397
|
+
token,
|
|
398
|
+
pathname: '/api/team-memory/auth/whoami',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function selectedTargets(flags = {}, env = process.env) {
|
|
403
|
+
const raw = String(flags.target || flags.runtime || '').trim().toLowerCase();
|
|
404
|
+
const parts = raw ? raw.split(',').map((item) => normalizeRuntime(item)).filter(Boolean) : [];
|
|
405
|
+
const requested = new Set(parts.length ? parts : ['all']);
|
|
406
|
+
if (requested.has('all')) {
|
|
407
|
+
const home = homeDirForEnv(env);
|
|
408
|
+
const detected = [];
|
|
409
|
+
if (env.CODEX_HOME || existsSync(path.join(home, '.codex'))) detected.push('codex');
|
|
410
|
+
if (env.CLAUDE_HOME || existsSync(path.join(home, '.claude'))) detected.push('claude_code');
|
|
411
|
+
return detected.length ? detected : ['codex', 'claude_code'];
|
|
412
|
+
}
|
|
413
|
+
return [...requested].map(normalizeRuntime);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function promptSetupTarget(flags = {}, env = process.env) {
|
|
417
|
+
if (flags.target || flags.runtime || flags.yes || flags.nonInteractive || env.CI) return flags;
|
|
418
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return flags;
|
|
419
|
+
const detected = selectedTargets({ ...flags, target: 'all' }, env);
|
|
420
|
+
if (detected.length < 2) return { ...flags, target: detected[0] || 'all' };
|
|
421
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
422
|
+
try {
|
|
423
|
+
const answer = await rl.question('Install Team Sharing for [a]ll, [c]odex, or c[l]aude? ');
|
|
424
|
+
const clean = String(answer || '').trim().toLowerCase();
|
|
425
|
+
if (clean === 'c' || clean === 'codex') return { ...flags, target: 'codex' };
|
|
426
|
+
if (clean === 'l' || clean === 'claude' || clean === 'claude_code') return { ...flags, target: 'claude_code' };
|
|
427
|
+
return { ...flags, target: 'all' };
|
|
428
|
+
} finally {
|
|
429
|
+
rl.close();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function targetConfigPath(runtime, flags = {}, env = process.env) {
|
|
434
|
+
const home = homeDirForEnv(env);
|
|
435
|
+
if (runtime === 'claude_code') return flags.claudeConfig || path.join(home, '.claude', 'settings.json');
|
|
436
|
+
return flags.codexConfig || path.join(home, '.codex', 'hooks.json');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function readJsonFile(file, fallback = {}) {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
442
|
+
} catch {
|
|
443
|
+
return fallback;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function writeJsonFile(file, value) {
|
|
448
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
449
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function hookEventsForRuntime(runtime) {
|
|
453
|
+
return normalizeRuntime(runtime) === 'claude_code'
|
|
454
|
+
? ['Stop', 'SessionEnd', 'PreCompact', 'SessionStart']
|
|
455
|
+
: ['Stop', 'PreCompact', 'SessionStart'];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export async function installTeamSharingHooks(flags = {}, env = process.env) {
|
|
459
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
460
|
+
const output = { ok: true };
|
|
461
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
462
|
+
const key = runtime === 'claude_code' ? 'claude' : 'codex';
|
|
463
|
+
output[key] = await installTeamMemoryHookConfig({
|
|
464
|
+
runtime,
|
|
465
|
+
configPath: targetConfigPath(runtime, flags, env),
|
|
466
|
+
projectDir: cwd,
|
|
467
|
+
integration: TEAM_SHARING_INTEGRATION,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
output.ok = Object.values(output).every((item) => item === true || item?.ok !== false);
|
|
471
|
+
return output;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function statusTeamSharingHooks(flags = {}, env = process.env) {
|
|
475
|
+
const output = { ok: true };
|
|
476
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
477
|
+
const key = runtime === 'claude_code' ? 'claude' : 'codex';
|
|
478
|
+
const configPath = targetConfigPath(runtime, flags, env);
|
|
479
|
+
const config = await readJsonFile(configPath, {});
|
|
480
|
+
const installed = [];
|
|
481
|
+
for (const eventName of hookEventsForRuntime(runtime)) {
|
|
482
|
+
for (const entry of Array.isArray(config.hooks?.[eventName]) ? config.hooks[eventName] : []) {
|
|
483
|
+
for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
|
|
484
|
+
if (String(hook.command || '').includes('--integration team-sharing')) installed.push(eventName);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
output[key] = { ok: true, runtime, configPath, installed };
|
|
489
|
+
}
|
|
490
|
+
return output;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function removeTeamSharingHooks(flags = {}, env = process.env) {
|
|
494
|
+
const output = { ok: true };
|
|
495
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
496
|
+
const key = runtime === 'claude_code' ? 'claude' : 'codex';
|
|
497
|
+
const configPath = targetConfigPath(runtime, flags, env);
|
|
498
|
+
const config = await readJsonFile(configPath, {});
|
|
499
|
+
const removed = [];
|
|
500
|
+
for (const eventName of hookEventsForRuntime(runtime)) {
|
|
501
|
+
const entries = Array.isArray(config.hooks?.[eventName]) ? config.hooks[eventName] : [];
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
const before = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
504
|
+
entry.hooks = before.filter((hook) => {
|
|
505
|
+
const remove = String(hook.command || '').includes('--integration team-sharing');
|
|
506
|
+
if (remove) removed.push(eventName);
|
|
507
|
+
return !remove;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await writeJsonFile(configPath, config);
|
|
512
|
+
output[key] = { ok: true, runtime, configPath, removed };
|
|
513
|
+
}
|
|
514
|
+
return output;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function teamSharingSkillMarkdown() {
|
|
518
|
+
return [
|
|
519
|
+
'---',
|
|
520
|
+
'name: magclaw-team-memory',
|
|
521
|
+
'description: Search, read, and cite MagClaw Team Sharing memory from Codex and Claude Code sessions.',
|
|
522
|
+
'---',
|
|
523
|
+
'',
|
|
524
|
+
'# MagClaw Team Sharing',
|
|
525
|
+
'',
|
|
526
|
+
'Use this skill when the user asks what teammates discussed, wants to align with another AI session, or needs original MagClaw conversation context.',
|
|
527
|
+
'',
|
|
528
|
+
'## Workflow',
|
|
529
|
+
'',
|
|
530
|
+
'1. Run `magclaw team-sharing search --query "<question>" --limit 5` from the configured project directory.',
|
|
531
|
+
'2. Use returned L0/L1 evidence for rough answers.',
|
|
532
|
+
'3. For deep follow-up, run `magclaw team-sharing context --session-id <sessionId> --anchor-event-id <eventId> --direction around --limit 20`.',
|
|
533
|
+
'4. Cite session titles, source refs, and context URLs.',
|
|
534
|
+
'',
|
|
535
|
+
'## Rules',
|
|
536
|
+
'',
|
|
537
|
+
'- Do not upload local secrets or raw tool output.',
|
|
538
|
+
'- Prefer concise synthesis before pulling original context.',
|
|
539
|
+
'- If confidence is low, ask the user for a narrower date, channel, or topic.',
|
|
540
|
+
'',
|
|
541
|
+
].join('\n');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function skillRootForTarget(runtime, env = process.env) {
|
|
545
|
+
const home = homeDirForEnv(env);
|
|
546
|
+
if (runtime === 'claude_code') return path.resolve(env.CLAUDE_HOME || path.join(home, '.claude'));
|
|
547
|
+
return path.resolve(env.CODEX_HOME || path.join(home, '.codex'));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function writeTeamSharingSkill(rootDir) {
|
|
551
|
+
const skillDir = path.join(rootDir, 'skills', 'magclaw-team-memory');
|
|
552
|
+
await mkdir(skillDir, { recursive: true });
|
|
553
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
554
|
+
await writeFile(skillPath, teamSharingSkillMarkdown());
|
|
555
|
+
return skillPath;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function installTeamSharingSkill(flags = {}, env = process.env) {
|
|
559
|
+
const output = { ok: true, installed: [] };
|
|
560
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
561
|
+
output.installed.push({ target: runtime, path: await writeTeamSharingSkill(skillRootForTarget(runtime, env)) });
|
|
562
|
+
}
|
|
563
|
+
output.ok = output.installed.length > 0;
|
|
564
|
+
return output;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export async function statusTeamSharingSkill(flags = {}, env = process.env) {
|
|
568
|
+
const installed = [];
|
|
569
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
570
|
+
const skillPath = path.join(skillRootForTarget(runtime, env), 'skills', 'magclaw-team-memory', 'SKILL.md');
|
|
571
|
+
if (existsSync(skillPath)) installed.push({ target: runtime, path: skillPath });
|
|
572
|
+
}
|
|
573
|
+
return { ok: true, installed };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export async function removeTeamSharingSkill(flags = {}, env = process.env) {
|
|
577
|
+
const removed = [];
|
|
578
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
579
|
+
const skillDir = path.join(skillRootForTarget(runtime, env), 'skills', 'magclaw-team-memory');
|
|
580
|
+
if (existsSync(skillDir)) {
|
|
581
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
582
|
+
removed.push({ target: runtime, path: skillDir });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return { ok: true, removed };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export async function disableTeamSharingSkill(flags = {}, env = process.env) {
|
|
589
|
+
const disabled = [];
|
|
590
|
+
for (const runtime of selectedTargets(flags, env)) {
|
|
591
|
+
const skillPath = path.join(skillRootForTarget(runtime, env), 'skills', 'magclaw-team-memory', 'SKILL.md');
|
|
592
|
+
const disabledPath = `${skillPath}.disabled`;
|
|
593
|
+
if (existsSync(skillPath)) {
|
|
594
|
+
await rename(skillPath, disabledPath);
|
|
595
|
+
disabled.push({ target: runtime, path: disabledPath });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return { ok: true, disabled };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function setupTeamSharing(flags = {}, env = process.env) {
|
|
602
|
+
flags = await promptSetupTarget(flags, env);
|
|
603
|
+
const profile = safeProfileName(flags.profile || env.MAGCLAW_TEAM_SHARING_PROFILE || env.MAGCLAW_MEMORY_PROFILE || DEFAULT_PROFILE);
|
|
604
|
+
const profileConfig = await readTeamSharingProfileConfig(profile, env);
|
|
605
|
+
if (!flags.noLogin && !profileConfig.config?.token) {
|
|
606
|
+
await loginTeamSharingProfile(flags, env);
|
|
607
|
+
}
|
|
608
|
+
const project = await initTeamSharingProject(flags, env);
|
|
609
|
+
const hooks = await installTeamSharingHooks(flags, env);
|
|
610
|
+
const skill = await installTeamSharingSkill(flags, env);
|
|
611
|
+
return {
|
|
612
|
+
ok: Boolean(project.ok && hooks.ok && skill.ok),
|
|
613
|
+
project,
|
|
614
|
+
hooks,
|
|
615
|
+
skill,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function semverParts(value = '') {
|
|
620
|
+
return String(value || '').replace(/^[^\d]*/, '').split(/[.-]/).slice(0, 3).map((part) => Number(part) || 0);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function semverGreater(left = '', right = '') {
|
|
624
|
+
const a = semverParts(left);
|
|
625
|
+
const b = semverParts(right);
|
|
626
|
+
for (let index = 0; index < 3; index += 1) {
|
|
627
|
+
if (a[index] > b[index]) return true;
|
|
628
|
+
if (a[index] < b[index]) return false;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function checkTeamSharingUpgrade(options = {}, env = process.env) {
|
|
634
|
+
const paths = teamSharingPaths({ env });
|
|
635
|
+
const nowMs = typeof options.nowMs === 'function' ? options.nowMs() : Date.now();
|
|
636
|
+
const ttlMs = Math.max(60_000, Number(options.ttlMs || env.MAGCLAW_TEAM_SHARING_UPGRADE_TTL_MS || 24 * 60 * 60 * 1000) || 24 * 60 * 60 * 1000);
|
|
637
|
+
const currentVersion = String(options.currentVersion || env.MAGCLAW_TEAM_SHARING_VERSION || env.MAGCLAW_ENTRY_PACKAGE_VERSION || '0.0.0');
|
|
638
|
+
const cached = await readJsonFile(paths.versionCache, null);
|
|
639
|
+
if (!options.force && cached?.checkedAtMs && nowMs - Number(cached.checkedAtMs) < ttlMs) {
|
|
640
|
+
return {
|
|
641
|
+
ok: true,
|
|
642
|
+
fromCache: true,
|
|
643
|
+
currentVersion,
|
|
644
|
+
latestVersion: cached.latestVersion || currentVersion,
|
|
645
|
+
upgradeAvailable: semverGreater(cached.latestVersion || currentVersion, currentVersion),
|
|
646
|
+
checkedAtMs: cached.checkedAtMs,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const response = await fetch(`https://registry.npmjs.org/${TEAM_SHARING_PACKAGE_NAME}`);
|
|
650
|
+
const data = await response.json().catch(() => ({}));
|
|
651
|
+
if (!response.ok) throw new Error(data.error || `npm registry returned ${response.status}`);
|
|
652
|
+
const latestVersion = String(data?.['dist-tags']?.latest || currentVersion);
|
|
653
|
+
const result = {
|
|
654
|
+
ok: true,
|
|
655
|
+
fromCache: false,
|
|
656
|
+
currentVersion,
|
|
657
|
+
latestVersion,
|
|
658
|
+
upgradeAvailable: semverGreater(latestVersion, currentVersion),
|
|
659
|
+
checkedAtMs: nowMs,
|
|
660
|
+
};
|
|
661
|
+
await writeJsonFile(paths.versionCache, result);
|
|
662
|
+
return result;
|
|
663
|
+
}
|