@xmemo/skill 1.1.25

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,194 @@
1
+ import {
2
+ EXIT_CODE,
3
+ exitCodeForError,
4
+ } from '../lib/core.mjs';
5
+
6
+ import {
7
+ makeHttpRequest,
8
+ handleRestError,
9
+ safeJson,
10
+ sanitizeTerminalText,
11
+ } from '../lib/api.mjs';
12
+
13
+ export async function handleAccount(ctx) {
14
+ const { command, options, flags, token } = ctx;
15
+
16
+ if (command === 'overview') {
17
+ try {
18
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
19
+ operation: 'overview',
20
+ arguments: {},
21
+ }, {
22
+ 'Authorization': `Bearer ${token}`
23
+ }, options.timeoutMs);
24
+
25
+ const data = handleRestError(res, {
26
+ notFoundMessage: 'Account overview not found.',
27
+ context: 'Get overview request',
28
+ options,
29
+ });
30
+
31
+ const result = (data && typeof data.result === 'object' && data.result !== null) ? data.result : data;
32
+
33
+ if (options.json) {
34
+ console.log(safeJson({
35
+ ok: true,
36
+ ...(data?.operation ? { operation: data.operation } : {}),
37
+ ...result,
38
+ result,
39
+ }));
40
+ process.exit(EXIT_CODE.SUCCESS);
41
+ }
42
+
43
+ console.log('XMemo Account Overview:');
44
+ console.log(`- Memories: ${result.memories_total ?? 0} total (${result.memories_active ?? 0} active, ${result.memories_archived ?? 0} archived, ${result.memories_forgotten ?? 0} forgotten)`);
45
+ console.log(`- Active Agents: ${result.agents_active ?? 0}`);
46
+ console.log(`- Storage: ${result.storage_mb ?? 0} MB`);
47
+ console.log(`- Tokens (30d): ${result.tokens_30d ?? 0}`);
48
+ process.exit(EXIT_CODE.SUCCESS);
49
+ } catch (e) {
50
+ console.error('Get overview failed:', e.message);
51
+ process.exit(exitCodeForError(e));
52
+ }
53
+ }
54
+
55
+ if (command === 'activity') {
56
+ const args = {};
57
+ if (flags.limit !== undefined) {
58
+ const parsed = Number(flags.limit);
59
+ args.limit = Number.isInteger(parsed) ? parsed : flags.limit;
60
+ }
61
+
62
+ try {
63
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
64
+ operation: 'activity',
65
+ arguments: args,
66
+ }, {
67
+ 'Authorization': `Bearer ${token}`
68
+ }, options.timeoutMs);
69
+
70
+ const data = handleRestError(res, {
71
+ notFoundMessage: 'Account activity not found.',
72
+ context: 'Get activity request',
73
+ options,
74
+ });
75
+
76
+ const result = (data && typeof data.result === 'object' && data.result !== null) ? data.result : data;
77
+
78
+ if (options.json) {
79
+ console.log(safeJson({
80
+ ok: true,
81
+ ...(data?.operation ? { operation: data.operation } : {}),
82
+ ...result,
83
+ result,
84
+ }));
85
+ process.exit(EXIT_CODE.SUCCESS);
86
+ }
87
+
88
+ const activityList = Array.isArray(result.activity) ? result.activity : [];
89
+ if (activityList.length === 0) {
90
+ console.log('No recent activity found.');
91
+ process.exit(EXIT_CODE.SUCCESS);
92
+ }
93
+
94
+ const totalInfo = result.total !== undefined ? ` (total: ${result.total})` : '';
95
+ console.log(`XMemo Recent Activity (${activityList.length}${totalInfo}):`);
96
+ activityList.forEach((item, idx) => {
97
+ const ts = item.ts || '(unknown date)';
98
+ const type = item.type || 'unknown';
99
+ const summary = item.summary || '';
100
+ const ref = item.ref_id ? ` [ref: ${item.ref_id}]` : '';
101
+ console.log(`[${idx + 1}] ${sanitizeTerminalText(ts)} | ${type.toUpperCase()} | ${sanitizeTerminalText(summary)}${ref}`);
102
+ });
103
+ process.exit(EXIT_CODE.SUCCESS);
104
+ } catch (e) {
105
+ console.error('Get activity failed:', e.message);
106
+ process.exit(exitCodeForError(e));
107
+ }
108
+ }
109
+
110
+ if (command === 'stats') {
111
+ const scope = flags.scope;
112
+ const path = flags.path;
113
+ const bucket = flags.bucket;
114
+ const memoryType = flags['memory-type'] !== undefined ? flags['memory-type'] : flags.memory_type;
115
+ const status = flags.status;
116
+ const source = flags.source;
117
+ const since = flags.since;
118
+ const until = flags.until;
119
+ const groupBy = flags['group-by'] !== undefined ? flags['group-by'] : flags.group_by;
120
+ const topN = flags['top-n'] !== undefined ? flags['top-n'] : flags.top_n;
121
+ const teamId = flags['team-id'] !== undefined ? flags['team-id'] : flags.team_id;
122
+
123
+ const queryParams = [];
124
+ if (scope) queryParams.push(`scope=${encodeURIComponent(scope)}`);
125
+ if (path) queryParams.push(`path=${encodeURIComponent(path)}`);
126
+ if (bucket) queryParams.push(`bucket=${encodeURIComponent(bucket)}`);
127
+ if (memoryType) queryParams.push(`memory_type=${encodeURIComponent(memoryType)}`);
128
+ if (status) queryParams.push(`status=${encodeURIComponent(status)}`);
129
+ if (source) queryParams.push(`source=${encodeURIComponent(source)}`);
130
+ if (since) queryParams.push(`since=${encodeURIComponent(since)}`);
131
+ if (until) queryParams.push(`until=${encodeURIComponent(until)}`);
132
+ if (groupBy) queryParams.push(`group_by=${encodeURIComponent(groupBy)}`);
133
+ if (topN !== undefined) queryParams.push(`top_n=${encodeURIComponent(topN)}`);
134
+ if (teamId) queryParams.push(`team_id=${encodeURIComponent(teamId)}`);
135
+
136
+ let endpoint = '/v1/memories/stats';
137
+ if (queryParams.length > 0) {
138
+ endpoint += `?${queryParams.join('&')}`;
139
+ }
140
+
141
+ try {
142
+ const res = await makeHttpRequest(options.baseUrl, endpoint, 'GET', null, {
143
+ 'Authorization': `Bearer ${token}`
144
+ }, options.timeoutMs);
145
+
146
+ const data = handleRestError(res, {
147
+ notFoundMessage: 'Memory stats not found.',
148
+ context: 'Get memory stats request',
149
+ options,
150
+ });
151
+
152
+ if (options.json) {
153
+ console.log(safeJson({
154
+ ok: true,
155
+ ...data,
156
+ }));
157
+ process.exit(EXIT_CODE.SUCCESS);
158
+ }
159
+
160
+ if ((data.total_count ?? 0) === 0 && (data.filtered_count ?? 0) === 0) {
161
+ console.log('No memory statistics available.');
162
+ process.exit(EXIT_CODE.SUCCESS);
163
+ }
164
+
165
+ console.log('XMemo Memory Statistics:');
166
+ console.log(`- Total Memories: ${data.total_count ?? 0} (filtered: ${data.filtered_count ?? 0}, scanned: ${data.scanned_count ?? 0})`);
167
+ if (data.latest_at) console.log(`- Latest Memory: ${data.latest_at}`);
168
+ if (data.oldest_at) console.log(`- Oldest Memory: ${data.oldest_at}`);
169
+ if (data.type_counts && Object.keys(data.type_counts).length > 0) {
170
+ const counts = Object.entries(data.type_counts).map(([k, v]) => `${k}: ${v}`).join(', ');
171
+ console.log(`- Types: ${counts}`);
172
+ }
173
+ if (data.status_counts && Object.keys(data.status_counts).length > 0) {
174
+ const counts = Object.entries(data.status_counts).map(([k, v]) => `${k}: ${v}`).join(', ');
175
+ console.log(`- Status: ${counts}`);
176
+ }
177
+ if (data.bucket_counts && Object.keys(data.bucket_counts).length > 0) {
178
+ const counts = Object.entries(data.bucket_counts).map(([k, v]) => `${k}: ${v}`).join(', ');
179
+ console.log(`- Buckets: ${counts}`);
180
+ }
181
+ if (Array.isArray(data.groups) && data.groups.length > 0) {
182
+ console.log(`- Groups (${data.groups.length}):`);
183
+ data.groups.forEach((g) => {
184
+ const dims = g.group_by ? Object.entries(g.group_by).map(([k, v]) => `${k}=${v}`).join(', ') : '';
185
+ console.log(` * [${dims}]: ${g.count}`);
186
+ });
187
+ }
188
+ process.exit(EXIT_CODE.SUCCESS);
189
+ } catch (e) {
190
+ console.error('Get memory stats failed:', e.message);
191
+ process.exit(exitCodeForError(e));
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,234 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import {
4
+ credentialsPath, SCRIPT_COMMAND, EXIT_CODE,
5
+ exitCodeForHttpStatus, exitCodeForErrorCode, exitCodeForError,
6
+ } from '../lib/core.mjs';
7
+
8
+ import {
9
+ requirePlaintextStorageConsent, saveToken, getStoredToken,
10
+ getStoredCredential, getInstallationFingerprint,
11
+ } from '../lib/auth-state.mjs';
12
+
13
+ import {
14
+ makeHttpRequest, parseJsonResponse, extractRequestId, apiErrorMessage, safeJson,
15
+ extractExpiresInSeconds, formatRemainingValidity, sanitizeTerminalText,
16
+ formatDuration, fetchTemporaryLimits,
17
+ } from '../lib/api.mjs';
18
+
19
+ export async function handleAuthLogin(ctx) {
20
+ const { command, options, flags, skillVersion } = ctx;
21
+ if (!skillVersion) {
22
+ throw new Error('handleAuthLogin requires ctx.skillVersion to be provided');
23
+ }
24
+
25
+ // 1. LOGIN
26
+ if (command === 'login') {
27
+ try {
28
+ requirePlaintextStorageConsent(options, 'Device login');
29
+ const res = await makeHttpRequest(options.baseUrl, '/v1/auth/device/start', 'POST', {
30
+ client_id: 'xmemo-skill', surface: 'standalone_skill', token_type: 'skill_token',
31
+ client_version: skillVersion,
32
+ scopes: ['memory:read', 'memory:write', 'memory:restore', 'ledger:write', 'ledger:read', 'knowledge:read']
33
+ }, {}, options.timeoutMs);
34
+ const data = parseJsonResponse(res, 'Device login start');
35
+ if (res.statusCode !== 200) {
36
+ const reqId = extractRequestId(data);
37
+ const reqSuffix = reqId ? ` (request_id: ${reqId})` : '';
38
+ console.error(`Failed to start device login: ${apiErrorMessage(data, safeJson(data))}${reqSuffix}`);
39
+ process.exit(exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode));
40
+ }
41
+ const verificationUrl = data.verification_uri_complete || data.verification_uri;
42
+ if (!data.device_code || !verificationUrl) {
43
+ console.error('Failed to start device login: the service response omitted the device code or verification URL.');
44
+ process.exit(EXIT_CODE.SERVER_ERROR);
45
+ }
46
+ const expiresInSeconds = extractExpiresInSeconds(data);
47
+ const expiresInMs = Math.max(1, expiresInSeconds * 1000);
48
+ const loginDeadline = Date.now() + expiresInMs;
49
+ const countdownText = formatRemainingValidity(expiresInSeconds);
50
+ console.log(`To verify this device, open the following URL in your browser:\n\n ${sanitizeTerminalText(verificationUrl)}\n\nOr enter the code: ${sanitizeTerminalText(data.user_code)}\n\nWaiting for authorization... (valid for ${countdownText})`);
51
+
52
+ const deviceCode = data.device_code;
53
+ const intervalSeconds = Number(data.interval);
54
+ let pollInterval = Number.isFinite(intervalSeconds) && intervalSeconds > 0
55
+ ? Math.max(1, intervalSeconds * 1000)
56
+ : 5000;
57
+
58
+ const poll = async () => {
59
+ if (Date.now() >= loginDeadline) {
60
+ console.error('Login failed: the device authorization code expired before approval.');
61
+ process.exit(EXIT_CODE.AUTH_ERROR);
62
+ }
63
+ try {
64
+ const pollRes = await makeHttpRequest(options.baseUrl, '/v1/auth/device/token', 'POST', {
65
+ device_code: deviceCode,
66
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
67
+ }, {}, options.timeoutMs);
68
+ const pollData = parseJsonResponse(pollRes, 'Device login polling');
69
+ if (pollData.error) {
70
+ if (pollData.error === 'authorization_pending') {
71
+ setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
72
+ } else if (pollData.error === 'slow_down') {
73
+ pollInterval += 5000;
74
+ setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
75
+ } else {
76
+ console.error(`Login failed: ${sanitizeTerminalText(pollData.error_description || pollData.error)}`);
77
+ process.exit(EXIT_CODE.AUTH_ERROR);
78
+ }
79
+ } else if (pollData.access_token) {
80
+ try {
81
+ await saveToken(pollData.access_token, { credential_type: 'formal' }, { allowPlaintext: options.allowPlaintext, warn: true });
82
+ console.log(`✅ Authorization successful. Token stored in the explicitly approved user credential file: ${credentialsPath}\nToken value was not printed. Project files were not modified.`);
83
+ process.exit(EXIT_CODE.SUCCESS);
84
+ } catch (err) {
85
+ console.error('Failed to save credentials file:', err.message);
86
+ process.exit(EXIT_CODE.USER_ERROR);
87
+ }
88
+ } else {
89
+ console.error('Login failed: the token endpoint returned neither an access token nor a recognized pending status.');
90
+ process.exit(EXIT_CODE.SERVER_ERROR);
91
+ }
92
+ } catch (e) {
93
+ if (Date.now() >= loginDeadline) {
94
+ console.error('Login failed: the device authorization window expired after repeated polling errors.');
95
+ process.exit(EXIT_CODE.AUTH_ERROR);
96
+ }
97
+ console.error('Login polling error:', e.message);
98
+ setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
99
+ }
100
+ };
101
+ setTimeout(poll, Math.min(pollInterval, expiresInMs));
102
+ } catch (e) {
103
+ console.error('Login error:', e.message);
104
+ process.exit(exitCodeForError(e));
105
+ }
106
+ return;
107
+ }
108
+
109
+ // 1b. LIMITED NO-ACCOUNT-START REGISTRATION (explicit fallback only)
110
+ if (command === 'register') {
111
+ const reason = flags.reason;
112
+ if (!['unattended', 'declined'].includes(reason)) {
113
+ console.error(`Temporary registration is a conditional fallback. Use "${SCRIPT_COMMAND} register --reason unattended --allow-plaintext" when no human can log in, or "--reason declined --allow-plaintext" after the human explicitly declines formal registration.`);
114
+ process.exit(EXIT_CODE.USER_ERROR);
115
+ }
116
+ try {
117
+ requirePlaintextStorageConsent(options, 'Temporary registration');
118
+ } catch (e) {
119
+ console.error(`Temporary registration refused: ${e.message}`);
120
+ process.exit(EXIT_CODE.USER_ERROR);
121
+ }
122
+ if (await getStoredToken()) {
123
+ console.error(`A credential is already configured. Formal login is the recommended path; use "${SCRIPT_COMMAND} login" to refresh it instead of creating temporary access.`);
124
+ process.exit(EXIT_CODE.USER_ERROR);
125
+ }
126
+ try {
127
+ const limits = await fetchTemporaryLimits(options.baseUrl, options.timeoutMs);
128
+ const installation_fingerprint = await getInstallationFingerprint();
129
+ const res = await makeHttpRequest(options.baseUrl, '/v1/agents/register', 'POST', {
130
+ entry_type: 'skill',
131
+ client_name: 'xmemo-skill',
132
+ client_version: skillVersion,
133
+ installation_fingerprint,
134
+ runtime: `node ${process.version}`,
135
+ skill_package_id: 'xmemo-memory',
136
+ metadata: { registration_reason: reason },
137
+ }, {}, options.timeoutMs);
138
+ const data = parseJsonResponse(res, 'Temporary registration');
139
+ if (res.statusCode < 200 || res.statusCode >= 300 || !data.temporary_token) {
140
+ const err = new Error(apiErrorMessage(data, safeJson(data)));
141
+ err.statusCode = res.statusCode;
142
+ throw err;
143
+ }
144
+ await saveToken(data.temporary_token, {
145
+ credential_type: 'temporary',
146
+ agent_id: data.agent_id,
147
+ bind_url: data.bind_url,
148
+ registration_reason: reason,
149
+ }, { allowPlaintext: options.allowPlaintext, warn: true });
150
+ if (options.json) {
151
+ console.log(safeJson({ agent_id: data.agent_id, bind_url: data.bind_url, status: data.status, limits }));
152
+ } else {
153
+ console.log(`✅ Temporary XMemo memory enabled for this installation.\nThis is a limited sandbox, not a formal account.\nTemporary limits: up to ${limits.max_items} items; expires after ${formatDuration(limits.ttl_seconds)} without successful memory activity; maximum ${formatDuration(limits.max_lifetime_seconds)} from registration.\nComplete formal registration (recommended): ${sanitizeTerminalText(data.bind_url)}\nDo not share this bind URL publicly. After the human claim, run "${SCRIPT_COMMAND} auth claim-confirm" to accept the formal credential.`);
154
+ }
155
+ process.exit(EXIT_CODE.SUCCESS);
156
+ } catch (e) {
157
+ console.error('Temporary registration failed:', e.message);
158
+ process.exit(exitCodeForError(e));
159
+ }
160
+ }
161
+
162
+ // 2. LOGOUT
163
+ if (command === 'logout') {
164
+ const credential = await getStoredCredential();
165
+ const token = credential?.token;
166
+ if (!token) {
167
+ console.log('No active login found.');
168
+ process.exit(EXIT_CODE.SUCCESS);
169
+ }
170
+
171
+ if (['openclaw-secret', 'vault'].includes(credential.storage) || (credential.storage === 'environment' && !options.revokeEnvironmentToken)) {
172
+ if (credential.storage === 'openclaw-secret' && options.revokeEnvironmentToken) {
173
+ console.error('Error: OpenClaw secret sentinels are managed by OpenClaw and cannot be revoked remotely. Use "openclaw secrets delete" or the OpenClaw Control UI to manage secrets.');
174
+ process.exit(EXIT_CODE.USER_ERROR);
175
+ }
176
+ const isOc = credential.storage === 'openclaw-secret';
177
+ const isVault = credential.storage === 'vault';
178
+ const status = isOc ? 'openclaw_secret_unchanged' : isVault ? 'vault_credential_unchanged' : 'environment_credential_unchanged';
179
+ const source = isOc ? 'openclaw-secret' : isVault ? 'muse-vault' : 'XMEMO_KEY';
180
+ if (options.json) {
181
+ console.log(safeJson({ status, credential_source: source, remote_revoked: false, local_file_removed: false }));
182
+ } else if (isOc) {
183
+ console.log('XMemo credential is provided by OpenClaw (openclaw-secret). No remote token was revoked and no local credential file was changed.\nTo disconnect or rotate, use "openclaw secrets delete" or the OpenClaw Control UI.');
184
+ } else if (isVault) {
185
+ console.log('XMemo credential is provided by Meta Muse (muse-vault). No remote token was revoked and no local credential file was changed.\nTo disconnect, remove the credential in Meta Muse.');
186
+ } else {
187
+ console.log('XMEMO_KEY is externally managed. No token was revoked and no local credential file was changed.\nUnset XMEMO_KEY in the launching environment to log out, or pass --revoke-environment-token to explicitly revoke that token.');
188
+ }
189
+ process.exit(EXIT_CODE.SUCCESS);
190
+ }
191
+
192
+ let remoteRevoked = false;
193
+ let revokeError = null;
194
+ try {
195
+ const revokeRes = await makeHttpRequest(options.baseUrl, '/v1/auth/token/revoke-self', 'POST', {}, {
196
+ 'Authorization': `Bearer ${token}`
197
+ }, options.timeoutMs);
198
+ remoteRevoked = revokeRes.statusCode >= 200 && revokeRes.statusCode < 300;
199
+ if (!remoteRevoked) revokeError = `HTTP ${revokeRes.statusCode}`;
200
+ } catch (error) {
201
+ revokeError = sanitizeTerminalText(error.message);
202
+ }
203
+
204
+ let localFileRemoved = false;
205
+ if (credential.storage !== 'environment') {
206
+ try {
207
+ await fs.unlink(credentialsPath);
208
+ localFileRemoved = true;
209
+ } catch (error) {
210
+ if (error?.code !== 'ENOENT') throw error;
211
+ }
212
+ }
213
+
214
+ const result = {
215
+ status: remoteRevoked ? 'logged_out' : 'local_logout_completed',
216
+ credential_source: credential.storage === 'environment' ? 'XMEMO_KEY' : 'user-credential-file',
217
+ remote_revoked: remoteRevoked,
218
+ local_file_removed: localFileRemoved,
219
+ ...(revokeError ? { remote_revoke_error: revokeError } : {}),
220
+ };
221
+ if (options.json) {
222
+ console.log(safeJson(result));
223
+ } else if (credential.storage === 'environment') {
224
+ console.log(remoteRevoked
225
+ ? '✅ The externally managed XMEMO_KEY token was explicitly revoked. Unset XMEMO_KEY in the launching environment.'
226
+ : `The XMEMO_KEY token could not be revoked (${revokeError}). It remains externally managed.`);
227
+ } else if (remoteRevoked) {
228
+ console.log('✅ Logged out successfully. The remote token was revoked and the local credential file was removed.');
229
+ } else {
230
+ console.log(`Local credential file removed. Remote revocation could not be confirmed${revokeError ? ` (${revokeError})` : ''}.`);
231
+ }
232
+ process.exit(EXIT_CODE.SUCCESS);
233
+ }
234
+ }
@@ -0,0 +1,201 @@
1
+ import {
2
+ credentialsPath,
3
+ SCRIPT_COMMAND,
4
+ EXIT_CODE,
5
+ exitCodeForError,
6
+ } from '../lib/core.mjs';
7
+
8
+ import {
9
+ readStdin,
10
+ } from '../lib/cli-input.mjs';
11
+
12
+ import {
13
+ printUsage,
14
+ } from '../lib/help.mjs';
15
+
16
+ import {
17
+ getStoredCredential,
18
+ requirePlaintextStorageConsent,
19
+ plaintextStorageAllowed,
20
+ saveToken,
21
+ claimStatus,
22
+ } from '../lib/auth-state.mjs';
23
+
24
+ import {
25
+ isOpenClawSentinel,
26
+ } from '../lib/openclaw-egress.mjs';
27
+
28
+ import {
29
+ makeHttpRequest,
30
+ parseJsonResponse,
31
+ apiErrorMessage,
32
+ safeJson,
33
+ sanitizeTerminalText,
34
+ } from '../lib/api.mjs';
35
+
36
+ export async function handleAuthManage(ctx) {
37
+ const { subcommand, options, flags } = ctx;
38
+
39
+ if (subcommand === 'status') {
40
+ const credential = await getStoredCredential();
41
+ const token = credential?.token;
42
+ if (!token) {
43
+ if (options.json) {
44
+ console.log(JSON.stringify({ status: 'logged_out' }));
45
+ } else {
46
+ console.log('Status: Logged out.');
47
+ }
48
+ process.exit(EXIT_CODE.SUCCESS);
49
+ }
50
+
51
+ const credentialSource = credential?.storage === 'openclaw-secret'
52
+ ? 'openclaw-secret'
53
+ : credential?.storage === 'vault'
54
+ ? 'muse-vault'
55
+ : credential?.storage === 'environment'
56
+ ? 'XMEMO_KEY'
57
+ : credential?.credential_type === 'temporary'
58
+ ? 'temporary-user-credential-file'
59
+ : 'formal-user-credential-file';
60
+ if (options.verify) {
61
+ try {
62
+ const res = await makeHttpRequest(options.baseUrl, '/v1/auth/token/validate', 'GET', null, {
63
+ 'Authorization': `Bearer ${token}`
64
+ }, options.timeoutMs);
65
+ const data = parseJsonResponse(res, 'Token verification');
66
+ if (res.statusCode === 200) {
67
+ if (options.json) {
68
+ console.log(safeJson({ status: 'valid', credential_source: credentialSource, scopes: data.scopes, setup_state: data.setup_state }));
69
+ } else {
70
+ const scopes = Array.isArray(data.scopes) ? data.scopes : [];
71
+ console.log(`Status: Logged in (verified)\nCredential Source: ${credentialSource}\nScopes: ${scopes.join(', ')}`);
72
+ }
73
+ } else {
74
+ if (options.json) {
75
+ console.log(safeJson({ status: 'invalid', credential_source: credentialSource }));
76
+ } else {
77
+ console.error(`Status: Invalid or expired token.${data ? ` ${apiErrorMessage(data, '')}` : ''}`);
78
+ }
79
+ const exitCode = res.statusCode >= 500 ? EXIT_CODE.SERVER_ERROR : EXIT_CODE.AUTH_ERROR;
80
+ process.exit(exitCode);
81
+ }
82
+ } catch (e) {
83
+ console.error('Verification error:', e.message);
84
+ process.exit(exitCodeForError(e));
85
+ }
86
+ } else {
87
+ if (options.json) {
88
+ console.log(safeJson({ status: 'logged_in', credential_source: credentialSource }));
89
+ } else {
90
+ const kind = credential?.credential_type === 'temporary' ? 'Temporary access' : 'Logged in';
91
+ console.log(`Status: ${kind}\nCredential Source: ${credentialSource}`);
92
+ }
93
+ }
94
+ process.exit(EXIT_CODE.SUCCESS);
95
+ }
96
+
97
+ if (subcommand === 'add') {
98
+ if (flags['from-stdin'] !== undefined || process.argv.includes('--from-stdin')) {
99
+ try {
100
+ requirePlaintextStorageConsent(options, 'auth add');
101
+ } catch (e) {
102
+ console.error(`Credential storage refused: ${e.message}`);
103
+ process.exit(EXIT_CODE.USER_ERROR);
104
+ }
105
+ const token = await readStdin();
106
+ if (!token) {
107
+ console.error('Error: Stdin did not provide a token.');
108
+ process.exit(EXIT_CODE.USER_ERROR);
109
+ }
110
+ if (typeof token === 'string' && token.startsWith('hsurr:')) {
111
+ console.error('Error: Refusing to store Meta Muse surrogate token. Surrogate tokens are dynamic and managed by Meta Muse.');
112
+ process.exit(EXIT_CODE.USER_ERROR);
113
+ }
114
+ if (isOpenClawSentinel(token)) {
115
+ console.error('Error: Refusing to store OpenClaw sentinel token. Sentinels are dynamic and managed by OpenClaw.');
116
+ process.exit(EXIT_CODE.USER_ERROR);
117
+ }
118
+ try {
119
+ await saveToken(token, { credential_type: 'formal' }, { allowPlaintext: options.allowPlaintext, warn: true });
120
+ console.log(`✅ Credential stored in the explicitly approved user credential file: ${credentialsPath}`);
121
+ console.log('Token value was not printed. Project files were not modified.');
122
+ process.exit(EXIT_CODE.SUCCESS);
123
+ } catch (err) {
124
+ console.error('Failed to save credentials file:', err.message);
125
+ process.exit(EXIT_CODE.USER_ERROR);
126
+ }
127
+ } else {
128
+ console.error(`Error: Run "${SCRIPT_COMMAND} auth add --from-stdin --allow-plaintext" to supply and explicitly store a token.`);
129
+ process.exit(EXIT_CODE.USER_ERROR);
130
+ }
131
+ }
132
+
133
+ if (subcommand === 'claim-status' || subcommand === 'claim-confirm' || subcommand === 'claim-deny') {
134
+ const credential = await getStoredCredential();
135
+ if (!credential?.token || credential.credential_type !== 'temporary') {
136
+ console.error('Error: Claim commands require a locally stored temporary credential from "register".');
137
+ process.exit(EXIT_CODE.USER_ERROR);
138
+ }
139
+ try {
140
+ if (subcommand === 'claim-deny') {
141
+ const denyRes = await makeHttpRequest(options.baseUrl, '/v1/agents/bind/deny-current-user', 'POST', {}, {
142
+ Authorization: `Bearer ${credential.token}`,
143
+ }, options.timeoutMs);
144
+ const denyData = parseJsonResponse(denyRes, 'Claim denial');
145
+ if (denyRes.statusCode < 200 || denyRes.statusCode >= 300) {
146
+ const err = new Error(apiErrorMessage(denyData, safeJson(denyData)));
147
+ err.statusCode = denyRes.statusCode;
148
+ throw err;
149
+ }
150
+ const allowPlaintext = plaintextStorageAllowed(options, credential);
151
+ await saveToken(credential.token, {
152
+ credential_type: 'temporary',
153
+ agent_id: credential.agent_id,
154
+ bind_url: credential.bind_url,
155
+ registration_reason: credential.registration_reason,
156
+ }, { allowPlaintext, warn: options.allowPlaintext && !credential.plaintext_storage_consent });
157
+ if (options.json) {
158
+ console.log(safeJson(denyData));
159
+ } else {
160
+ console.log('Pending account binding declined. The credential remains limited to isolated temporary memory; formal account login is still recommended.');
161
+ }
162
+ process.exit(EXIT_CODE.SUCCESS);
163
+ }
164
+ const status = await claimStatus(options.baseUrl, credential, options);
165
+ if (subcommand === 'claim-confirm' && !status.formal_token) {
166
+ const confirmation_token = status.confirmation_token || credential.pending_confirmation_token;
167
+ if (!confirmation_token) {
168
+ console.error(`No pending human claim confirmation is available. Current status: ${sanitizeTerminalText(status.status || 'unknown')}. Open the stored bind URL first: ${sanitizeTerminalText(credential.bind_url || '(unavailable)')}`);
169
+ process.exit(EXIT_CODE.USER_ERROR);
170
+ }
171
+ const confirmRes = await makeHttpRequest(options.baseUrl, '/v1/agents/bind/confirm-current-user', 'POST', { confirmation_token }, {
172
+ Authorization: `Bearer ${credential.token}`,
173
+ }, options.timeoutMs);
174
+ const confirmData = parseJsonResponse(confirmRes, 'Claim confirmation');
175
+ if (confirmRes.statusCode < 200 || confirmRes.statusCode >= 300) {
176
+ const err = new Error(apiErrorMessage(confirmData, safeJson(confirmData)));
177
+ err.statusCode = confirmRes.statusCode;
178
+ throw err;
179
+ }
180
+ if (credential.pending_confirmation_token) {
181
+ const allowPlaintext = plaintextStorageAllowed(options, credential);
182
+ await saveToken(credential.token, {
183
+ credential_type: 'temporary',
184
+ agent_id: credential.agent_id,
185
+ bind_url: credential.bind_url,
186
+ registration_reason: credential.registration_reason,
187
+ }, { allowPlaintext, warn: options.allowPlaintext && !credential.plaintext_storage_consent });
188
+ }
189
+ await claimStatus(options.baseUrl, credential, options);
190
+ }
191
+ process.exit(EXIT_CODE.SUCCESS);
192
+ } catch (e) {
193
+ console.error('Claim flow failed:', e.message);
194
+ process.exit(exitCodeForError(e));
195
+ }
196
+ }
197
+
198
+ console.error(`Unknown auth subcommand: ${subcommand || '(missing)'}`);
199
+ printUsage('auth');
200
+ process.exit(EXIT_CODE.USER_ERROR);
201
+ }