@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,175 @@
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 handleLedger(ctx) {
14
+ const { command, options, flags, token } = ctx;
15
+
16
+ if (command === 'ledger-list') {
17
+ let dateFrom = flags.from;
18
+ let dateTo = flags.to;
19
+ if (flags.month) {
20
+ const [y, m] = flags.month.split('-').map(Number);
21
+ const lastDayNum = new Date(Date.UTC(y, m, 0)).getUTCDate();
22
+ if (!dateFrom) dateFrom = `${flags.month}-01`;
23
+ if (!dateTo) dateTo = `${flags.month}-${String(lastDayNum).padStart(2, '0')}`;
24
+ }
25
+
26
+ const args = {};
27
+ if (flags.limit !== undefined) {
28
+ const parsed = Number(flags.limit);
29
+ args.limit = Number.isInteger(parsed) ? parsed : flags.limit;
30
+ }
31
+ if (flags.offset !== undefined) {
32
+ const parsed = Number(flags.offset);
33
+ args.offset = Number.isInteger(parsed) ? parsed : flags.offset;
34
+ }
35
+ if (flags.currency) args.currency = String(flags.currency);
36
+ if (dateFrom) args.date_from = String(dateFrom);
37
+ if (dateTo) args.date_to = String(dateTo);
38
+ if (flags.category) args.category = String(flags.category);
39
+ if (flags['min-amount'] !== undefined) {
40
+ const parsed = Number(flags['min-amount']);
41
+ args.min_amount = !isNaN(parsed) ? parsed : flags['min-amount'];
42
+ }
43
+ if (flags['max-amount'] !== undefined) {
44
+ const parsed = Number(flags['max-amount']);
45
+ args.max_amount = !isNaN(parsed) ? parsed : flags['max-amount'];
46
+ }
47
+ if (flags.type) args.transaction_type = String(flags.type);
48
+
49
+ try {
50
+ // Dispatches query to XMemo API over HTTPS with Bearer authorization.
51
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
52
+ operation: 'ledger-list',
53
+ arguments: args,
54
+ }, {
55
+ 'Authorization': `Bearer ${token}`
56
+ }, options.timeoutMs);
57
+
58
+ const data = handleRestError(res, {
59
+ notFoundMessage: 'Ledger transactions not found.',
60
+ context: 'List ledger transactions request',
61
+ options,
62
+ });
63
+
64
+ const result = (data && typeof data.result === 'object' && data.result !== null) ? data.result : data;
65
+
66
+ if (options.json) {
67
+ console.log(safeJson({
68
+ ok: true,
69
+ ...(data?.operation ? { operation: data.operation } : {}),
70
+ ...result,
71
+ result,
72
+ }));
73
+ process.exit(EXIT_CODE.SUCCESS);
74
+ }
75
+
76
+ const transactions = Array.isArray(result.transactions)
77
+ ? result.transactions
78
+ : (Array.isArray(result) ? result : []);
79
+
80
+ if (transactions.length === 0) {
81
+ console.log('No ledger transactions found.');
82
+ process.exit(EXIT_CODE.SUCCESS);
83
+ }
84
+
85
+ const totalInfo = result.total !== undefined ? ` (total: ${result.total})` : '';
86
+ console.log(`XMemo Ledger Transactions (${transactions.length}${totalInfo}):`);
87
+ transactions.forEach((tx, idx) => {
88
+ const date = tx.transaction_date || tx.date || tx.created_at || '(unknown date)';
89
+ const amount = (tx.amount !== undefined && tx.amount !== null) ? tx.amount : '(unknown)';
90
+ const curr = tx.currency || 'UNKNOWN';
91
+ const type = tx.transaction_type || tx.type || 'expense';
92
+ const cat = tx.category ? ` [${tx.category}]` : '';
93
+ const desc = tx.description || tx.item || tx.note || '';
94
+ console.log(`[${idx + 1}] ${sanitizeTerminalText(date)} | ${type.toUpperCase()} | ${amount} ${curr}${sanitizeTerminalText(cat)}${desc ? ` | ${sanitizeTerminalText(desc)}` : ''}`);
95
+ });
96
+ process.exit(EXIT_CODE.SUCCESS);
97
+ } catch (e) {
98
+ console.error('List ledger transactions failed:', e.message);
99
+ process.exit(exitCodeForError(e));
100
+ }
101
+ }
102
+
103
+ if (command === 'ledger-summary') {
104
+ const args = {};
105
+ if (flags.months !== undefined) {
106
+ const parsed = Number(flags.months);
107
+ args.months = Number.isInteger(parsed) ? parsed : flags.months;
108
+ }
109
+ if (flags.currency) args.currency = String(flags.currency);
110
+ if (flags.type) args.transaction_type = String(flags.type);
111
+
112
+ try {
113
+ // Dispatches query to XMemo API over HTTPS with Bearer authorization.
114
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
115
+ operation: 'ledger-summary',
116
+ arguments: args,
117
+ }, {
118
+ 'Authorization': `Bearer ${token}`
119
+ }, options.timeoutMs);
120
+
121
+ const data = handleRestError(res, {
122
+ notFoundMessage: 'Ledger monthly summary not found.',
123
+ context: 'Get ledger monthly summary request',
124
+ options,
125
+ });
126
+
127
+ const result = (data && typeof data.result === 'object' && data.result !== null) ? data.result : data;
128
+
129
+ if (options.json) {
130
+ console.log(safeJson({
131
+ ok: true,
132
+ ...(data?.operation ? { operation: data.operation } : {}),
133
+ ...result,
134
+ result,
135
+ }));
136
+ process.exit(EXIT_CODE.SUCCESS);
137
+ }
138
+
139
+ const summaryList = Array.isArray(result.summary) ? result.summary : [];
140
+ if (summaryList.length === 0 && result.total === undefined && result.count === undefined) {
141
+ console.log('No ledger monthly summary available.');
142
+ process.exit(EXIT_CODE.SUCCESS);
143
+ }
144
+
145
+ if (summaryList.length > 0) {
146
+ console.log(`XMemo Ledger Monthly Summary (${summaryList.length} month${summaryList.length === 1 ? '' : 's'}):`);
147
+ summaryList.forEach((item) => {
148
+ const month = item.month || '(unknown month)';
149
+ const curr = item.currency || 'UNKNOWN';
150
+ const expense = item.expense_total !== undefined ? `${item.expense_total} ${curr}` : null;
151
+ const income = item.income_total !== undefined ? `${item.income_total} ${curr}` : null;
152
+ const net = item.net_total !== undefined ? `${item.net_total} ${curr}` : null;
153
+ const count = item.transaction_count !== undefined ? `${item.transaction_count} tx` : '';
154
+ const parts = [];
155
+ if (expense !== null) parts.push(`Expense: ${expense}`);
156
+ if (income !== null) parts.push(`Income: ${income}`);
157
+ if (net !== null) parts.push(`Net: ${net}`);
158
+ if (count) parts.push(count);
159
+ console.log(`- ${month} (${curr}): ${parts.join(' | ')}`);
160
+ });
161
+ process.exit(EXIT_CODE.SUCCESS);
162
+ }
163
+
164
+ const month = result.month || '(unknown month)';
165
+ const curr = result.currency || 'UNKNOWN';
166
+ const total = result.total !== undefined ? result.total : 0;
167
+ const count = result.count !== undefined ? result.count : 0;
168
+ console.log(`XMemo ledger summary for ${sanitizeTerminalText(month)}: ${total} ${curr} across ${count} transaction${count === 1 ? '' : 's'}.`);
169
+ process.exit(EXIT_CODE.SUCCESS);
170
+ } catch (e) {
171
+ console.error('Get ledger monthly summary failed:', e.message);
172
+ process.exit(exitCodeForError(e));
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,306 @@
1
+ import {
2
+ EXIT_CODE,
3
+ exitCodeForHttpStatus,
4
+ exitCodeForErrorCode,
5
+ exitCodeForError,
6
+ } from '../lib/core.mjs';
7
+
8
+ import {
9
+ printMemoryResults,
10
+ } from '../lib/auth-state.mjs';
11
+
12
+ import {
13
+ makeHttpRequest,
14
+ parseJsonResponse,
15
+ extractRequestId,
16
+ extractId,
17
+ apiErrorMessage,
18
+ outputRestError,
19
+ handleRestError,
20
+ outputJsonFailure,
21
+ safeJson,
22
+ sanitizeTerminalText,
23
+ formatMemoryContent,
24
+ } from '../lib/api.mjs';
25
+
26
+ function reqSuffix(data) {
27
+ const reqId = extractRequestId(data);
28
+ return reqId ? ` (request_id: ${reqId})` : '';
29
+ }
30
+
31
+ function failRequest(data, statusCode, prefix) {
32
+ console.error(`${prefix}${reqSuffix(data)}`);
33
+ process.exit(exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(statusCode));
34
+ }
35
+
36
+ function extractRecord(data) {
37
+ if (data && typeof data === 'object') {
38
+ if (data.result && typeof data.result === 'object') return data.result;
39
+ if (data.memory && typeof data.memory === 'object') return data.memory;
40
+ }
41
+ return data;
42
+ }
43
+
44
+ export async function handleMemory(ctx) {
45
+ const { command, options, flags, token } = ctx;
46
+
47
+ if (command === 'restart-snapshot' || command === 'restart-restore') {
48
+ const endpoint = command === 'restart-snapshot' ? '/v1/restart/snapshot' : '/v1/restart/restore';
49
+ const label = command === 'restart-snapshot' ? 'Restart snapshot' : 'Restart restore';
50
+ try {
51
+ const res = await makeHttpRequest(options.baseUrl, endpoint, 'POST', flags, {
52
+ 'Authorization': `Bearer ${token}`
53
+ }, options.timeoutMs);
54
+ const data = parseJsonResponse(res, `${label} request`);
55
+ const succeeded = res.statusCode >= 200 && res.statusCode < 300;
56
+ if (options.json) {
57
+ if (succeeded) {
58
+ const payload = (data && typeof data === 'object') ? data : { result: data };
59
+ console.log(safeJson({ ok: true, ...payload }));
60
+ process.exit(EXIT_CODE.SUCCESS);
61
+ }
62
+ outputJsonFailure(data, res.statusCode);
63
+ }
64
+ if (!succeeded) {
65
+ failRequest(data, res.statusCode, `${label} failed: ${apiErrorMessage(data)} (HTTP ${res.statusCode})`);
66
+ }
67
+ if (command === 'restart-snapshot') {
68
+ console.log(`✅ Restart snapshot saved.\nID: ${sanitizeTerminalText(extractId(data))}${data.expires_at ? `\nExpires: ${sanitizeTerminalText(data.expires_at)}` : ''}`);
69
+ } else {
70
+ const isNotRestored = data.restored === false || data.status === 'not_found' || (!extractId(data) && !data.restored_at);
71
+ if (isNotRestored) {
72
+ console.log('ℹ️ No active restart snapshot found to restore.');
73
+ } else {
74
+ console.log(`✅ Restart snapshot restored.\nID: ${sanitizeTerminalText(extractId(data))}${data.restored_at ? `\nRestored: ${sanitizeTerminalText(data.restored_at)}` : ''}`);
75
+ }
76
+ }
77
+ process.exit(EXIT_CODE.SUCCESS);
78
+ } catch (e) {
79
+ console.error(`${label} failed:`, e.message);
80
+ process.exit(exitCodeForError(e));
81
+ }
82
+ }
83
+
84
+ if (command === 'recall-context') {
85
+ const body = Object.fromEntries(
86
+ Object.entries({
87
+ query: flags.query,
88
+ path: flags.path || '%',
89
+ bucket: flags.bucket || '%',
90
+ scope: flags.scope,
91
+ team_id: flags.team_id,
92
+ memory_type: flags.memory_type || 'auto',
93
+ status: flags.status || 'active',
94
+ threshold: flags.threshold === undefined ? undefined : Number(flags.threshold),
95
+ max_items: flags.max_items,
96
+ max_tokens: flags.max_tokens,
97
+ limit: flags.limit,
98
+ prefer_working: flags.prefer_working === undefined ? true : flags.prefer_working,
99
+ include_knowledge: flags.include_knowledge,
100
+ }).filter(([, v]) => v !== undefined)
101
+ );
102
+ try {
103
+ const res = await makeHttpRequest(options.baseUrl, '/v1/recall/context', 'POST', body, {
104
+ 'Authorization': `Bearer ${token}`
105
+ }, options.timeoutMs);
106
+ const data = parseJsonResponse(res, 'Recall context request');
107
+ const succeeded = res.statusCode >= 200 && res.statusCode < 300 && data.ok !== false;
108
+ if (options.json) {
109
+ console.log(safeJson(data));
110
+ process.exit(succeeded ? EXIT_CODE.SUCCESS : (exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode)));
111
+ }
112
+ if (!succeeded) {
113
+ failRequest(data, res.statusCode, `Error: ${apiErrorMessage(data)} (Code: ${data.error?.code || `HTTP ${res.statusCode}`})`);
114
+ }
115
+ const items = Array.isArray(data.items) ? data.items.length : 0;
116
+ const contextText = sanitizeTerminalText(data.context_text || '');
117
+ console.log(`XMemo Context: ${items} item${items === 1 ? '' : 's'}\n${contextText || 'No matching memories found.'}`);
118
+ process.exit(EXIT_CODE.SUCCESS);
119
+ } catch (e) {
120
+ console.error('Recall context failed:', e.message);
121
+ process.exit(exitCodeForError(e));
122
+ }
123
+ }
124
+
125
+ if (command === 'read') {
126
+ const queryParams = [
127
+ flags.bucket ? `bucket=${encodeURIComponent(flags.bucket)}` : null,
128
+ flags.scope ? `scope=${encodeURIComponent(flags.scope)}` : null,
129
+ ].filter(Boolean);
130
+ const endpoint = `/v1/memories/${encodeURIComponent(flags.id)}/explain?include_embedding=false${queryParams.length ? `&${queryParams.join('&')}` : ''}`;
131
+ try {
132
+ const res = await makeHttpRequest(options.baseUrl, endpoint, 'GET', null, {
133
+ 'Authorization': `Bearer ${token}`
134
+ }, options.timeoutMs);
135
+
136
+ const data = handleRestError(res, {
137
+ notFoundMessage: `Memory '${flags.id}' not found.`,
138
+ context: 'Read memory request',
139
+ options,
140
+ });
141
+
142
+ const record = extractRecord(data);
143
+ if (record && record.status && String(record.status).toLowerCase() === 'deleted') {
144
+ outputRestError('not_found', `Memory '${flags.id}' not found or deleted.`, options);
145
+ }
146
+
147
+ if (!record || typeof record.content !== 'string') {
148
+ outputRestError('not_found', `Memory '${flags.id}' not found.`, options);
149
+ }
150
+
151
+ const fullContent = record.content;
152
+ const totalLength = fullContent.length;
153
+ const offset = flags.offset !== undefined ? Number(flags.offset) : 0;
154
+ const hasLimit = flags.limit !== undefined && flags.limit !== null;
155
+ const limit = hasLimit ? Number(flags.limit) : totalLength;
156
+ const slicedContent = fullContent.slice(offset, offset + limit);
157
+ const truncated = offset > 0 || (offset + slicedContent.length < totalLength);
158
+
159
+ const projected = {
160
+ id: record.id || record.memory_id || flags.id,
161
+ path: record.path || record.canonical_path || '',
162
+ content: slicedContent,
163
+ version: record.version || record.updated_at || record.created_at || null,
164
+ truncated,
165
+ };
166
+
167
+ if (options.json) {
168
+ console.log(safeJson({ ok: true, ...projected }));
169
+ process.exit(EXIT_CODE.SUCCESS);
170
+ }
171
+
172
+ console.log(`Memory: ${sanitizeTerminalText(projected.id)} | Path: ${sanitizeTerminalText(projected.path || '(unknown)')} | Version: ${sanitizeTerminalText(projected.version || '(unknown)')}${projected.truncated ? ' [truncated]' : ''}`);
173
+ console.log(`Content: ${formatMemoryContent(projected.content, options.compact)}`);
174
+ process.exit(EXIT_CODE.SUCCESS);
175
+ } catch (e) {
176
+ console.error('Read memory failed:', e.message);
177
+ process.exit(exitCodeForError(e));
178
+ }
179
+ }
180
+
181
+ if (command === 'update') {
182
+ const endpoint = `/v1/memories/${encodeURIComponent(flags.id)}`;
183
+ const body = {};
184
+ if (flags.content !== undefined) body.content = flags.content;
185
+ if (flags.path !== undefined) body.path = flags.path;
186
+ if (flags.metadata !== undefined) body.metadata = flags.metadata;
187
+ if (flags.bucket !== undefined) body.bucket = flags.bucket;
188
+ if (flags.scope !== undefined) body.scope = flags.scope;
189
+
190
+ try {
191
+ const res = await makeHttpRequest(options.baseUrl, endpoint, 'PATCH', body, {
192
+ 'Authorization': `Bearer ${token}`
193
+ }, options.timeoutMs);
194
+
195
+ if (res.statusCode === 400) {
196
+ let errData = null;
197
+ try { errData = parseJsonResponse(res, 'Update memory request'); } catch {}
198
+ const code = errData?.error?.code || 'invalid_request';
199
+ const fallbackMsg = code === 'invalid_memory_id'
200
+ ? `Invalid memory ID: '${flags.id}'.`
201
+ : 'Invalid update request.';
202
+ const msg = apiErrorMessage(errData, fallbackMsg);
203
+ outputRestError(code, msg, options, errData, EXIT_CODE.USER_ERROR);
204
+ }
205
+
206
+ const data = handleRestError(res, {
207
+ notFoundMessage: `Memory '${flags.id}' not found.`,
208
+ context: 'Update memory request',
209
+ options,
210
+ });
211
+
212
+ const record = extractRecord(data);
213
+ const memoryId = record?.id || record?.memory_id || flags.id;
214
+ if (options.json) {
215
+ console.log(safeJson({
216
+ ok: true,
217
+ id: memoryId,
218
+ path: record?.path || flags.path || '',
219
+ updated: true,
220
+ ...(typeof record === 'object' ? record : {}),
221
+ }));
222
+ process.exit(EXIT_CODE.SUCCESS);
223
+ }
224
+
225
+ console.log(`✅ Memory updated.\nID: ${sanitizeTerminalText(memoryId)}${flags.path ? `\nPath: ${sanitizeTerminalText(flags.path)}` : ''}`);
226
+ process.exit(EXIT_CODE.SUCCESS);
227
+ } catch (e) {
228
+ console.error('Update memory failed:', e.message);
229
+ process.exit(exitCodeForError(e));
230
+ }
231
+ }
232
+
233
+ if (command === 'forget') {
234
+ if (!flags.confirm) {
235
+ const msg = `Confirmation required to forget memory or ledger record '${flags.id}'. Pass --confirm to proceed.`;
236
+ if (options.json) {
237
+ console.log(safeJson({ ok: false, error: { code: 'confirmation_required', message: msg, target_id: flags.id } }));
238
+ } else {
239
+ console.error(`Error: ${msg}\nTarget: ${sanitizeTerminalText(flags.id)}`);
240
+ }
241
+ process.exit(EXIT_CODE.USER_ERROR);
242
+ }
243
+
244
+ const endpoint = `/v1/memories/${encodeURIComponent(flags.id)}/forget`;
245
+ const body = { mode: 'soft_delete' };
246
+ if (flags.reason !== undefined && String(flags.reason).trim() !== '') {
247
+ body.reason = String(flags.reason);
248
+ }
249
+
250
+ try {
251
+ const res = await makeHttpRequest(options.baseUrl, endpoint, 'POST', body, {
252
+ 'Authorization': `Bearer ${token}`
253
+ }, options.timeoutMs);
254
+
255
+ handleRestError(res, {
256
+ notFoundMessage: `Record '${flags.id}' not found.`,
257
+ context: 'Forget request',
258
+ options,
259
+ });
260
+
261
+ if (options.json) {
262
+ console.log(safeJson({ ok: true, id: flags.id, mode: 'soft_delete', forgotten: true }));
263
+ process.exit(EXIT_CODE.SUCCESS);
264
+ }
265
+
266
+ console.log(`✅ Record forgotten (soft-deleted).\nID: ${sanitizeTerminalText(flags.id)}`);
267
+ process.exit(EXIT_CODE.SUCCESS);
268
+ } catch (e) {
269
+ console.error('Forget failed:', e.message);
270
+ process.exit(exitCodeForError(e));
271
+ }
272
+ }
273
+
274
+ if (command === 'remember' || command === 'recall' || command === 'search') {
275
+ try {
276
+ const res = await makeHttpRequest(options.baseUrl, '/v1/skill/operations', 'POST', {
277
+ operation: command,
278
+ arguments: flags,
279
+ }, {
280
+ 'Authorization': `Bearer ${token}`
281
+ }, options.timeoutMs);
282
+
283
+ const data = parseJsonResponse(res, `${command} request`);
284
+ const succeeded = res.statusCode >= 200 && res.statusCode < 300 && data.ok !== false;
285
+ if (options.json) {
286
+ console.log(safeJson(data));
287
+ process.exit(succeeded ? EXIT_CODE.SUCCESS : (exitCodeForErrorCode(data?.error?.code) ?? exitCodeForHttpStatus(res.statusCode)));
288
+ }
289
+
290
+ if (!succeeded) {
291
+ failRequest(data, res.statusCode, `Error: ${apiErrorMessage(data)} (Code: ${data.error?.code || `HTTP ${res.statusCode}`})`);
292
+ }
293
+
294
+ if (command === 'recall' || command === 'search') {
295
+ printMemoryResults(data.result, options.compact);
296
+ process.exit(EXIT_CODE.SUCCESS);
297
+ } else if (command === 'remember') {
298
+ console.log(`✅ Saved to XMemo.\nID: ${sanitizeTerminalText(extractId(data.result))}`);
299
+ process.exit(EXIT_CODE.SUCCESS);
300
+ }
301
+ } catch (e) {
302
+ console.error('Request failed:', e.message);
303
+ process.exit(exitCodeForError(e));
304
+ }
305
+ }
306
+ }