codemini-cli 0.4.0 → 0.4.2
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/OPERATIONS.md +4 -2
- package/README.md +89 -11
- package/deployment.md +14 -7
- package/package.json +1 -2
- package/src/cli.js +1 -1
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +18 -311
- package/src/core/chat-runtime.js +389 -53
- package/src/core/command-loader.js +12 -5
- package/src/core/config-store.js +2 -0
- package/src/core/context-compact.js +34 -9
- package/src/core/default-system-prompt.js +5 -5
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/reflect-skill.js +178 -0
- package/src/core/shell-profile.js +8 -8
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +144 -190
- package/src/tui/chat-app.js +270 -28
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function parseInlineRangePath(value) {
|
|
4
|
+
const text = String(value || '').trim();
|
|
5
|
+
if (!text) return null;
|
|
6
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
9
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
10
|
+
const startLine = Number(startRaw);
|
|
11
|
+
const endLine = Number(endRaw || startRaw);
|
|
12
|
+
if (!Number.isFinite(startLine) || startLine <= 0) return null;
|
|
13
|
+
if (!Number.isFinite(endLine) || endLine < startLine) return null;
|
|
14
|
+
return {
|
|
15
|
+
path: maybePath,
|
|
16
|
+
start_line: startLine,
|
|
17
|
+
end_line: endLine
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeReadArgs(rawArgs) {
|
|
22
|
+
const source =
|
|
23
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
24
|
+
? { ...rawArgs }
|
|
25
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
26
|
+
|
|
27
|
+
const normalized = { ...source };
|
|
28
|
+
const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
|
|
29
|
+
if (aliasPath) normalized.path = aliasPath;
|
|
30
|
+
|
|
31
|
+
if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
|
|
32
|
+
normalized.start_line = Number(source.offset);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
|
|
36
|
+
const startLine = Number(normalized.start_line);
|
|
37
|
+
const limit = Number(source.limit);
|
|
38
|
+
if (startLine > 0 && limit > 0) {
|
|
39
|
+
normalized.end_line = startLine + limit - 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const inlineRange = parseInlineRangePath(normalized.path);
|
|
44
|
+
if (inlineRange) {
|
|
45
|
+
normalized.path = inlineRange.path;
|
|
46
|
+
if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
|
|
47
|
+
if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizePathArgs(rawArgs, aliases = []) {
|
|
54
|
+
const source =
|
|
55
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
56
|
+
? { ...rawArgs }
|
|
57
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
58
|
+
const normalized = { ...source };
|
|
59
|
+
const keys = ['path', ...aliases];
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const value = String(source?.[key] || '').trim();
|
|
62
|
+
if (value) {
|
|
63
|
+
normalized.path = value;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
|
|
71
|
+
const source =
|
|
72
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
73
|
+
? { ...rawArgs }
|
|
74
|
+
: { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
75
|
+
const normalized = { ...source };
|
|
76
|
+
for (const key of ['pattern', ...aliases]) {
|
|
77
|
+
const value = String(source?.[key] || '').trim();
|
|
78
|
+
if (value) {
|
|
79
|
+
normalized.pattern = value;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const key of ['path', ...defaultPathAliases]) {
|
|
84
|
+
const value = String(source?.[key] || '').trim();
|
|
85
|
+
if (value) {
|
|
86
|
+
normalized.path = value;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeWriteArgs(rawArgs) {
|
|
94
|
+
const source =
|
|
95
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
96
|
+
? { ...rawArgs }
|
|
97
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
98
|
+
const normalized = { ...source };
|
|
99
|
+
const filePath = String(source.path || source.file_path || source.file || '').trim();
|
|
100
|
+
if (filePath) normalized.path = filePath;
|
|
101
|
+
if (normalized.content == null) {
|
|
102
|
+
if (source.text != null) normalized.content = source.text;
|
|
103
|
+
if (source.new_content != null) normalized.content = source.new_content;
|
|
104
|
+
}
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeWebFetchArgs(rawArgs) {
|
|
109
|
+
const normalized = normalizePathArgs(rawArgs, ['url', 'href', 'link', 'target']);
|
|
110
|
+
const url = String(normalized.url || normalized.path || '').trim();
|
|
111
|
+
return { ...normalized, url };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeWebSearchArgs(rawArgs) {
|
|
115
|
+
const normalized = normalizePatternArgs(rawArgs, ['query', 'q', 'keyword']);
|
|
116
|
+
const query = String(normalized.query || normalized.pattern || '').trim();
|
|
117
|
+
return { ...normalized, query };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildDeleteApprovalDetails(source, rawPath) {
|
|
121
|
+
const existing =
|
|
122
|
+
source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
|
|
123
|
+
? source.approval
|
|
124
|
+
: {};
|
|
125
|
+
const approvalPath = String(existing.path || rawPath || '').trim();
|
|
126
|
+
const approvalName = String(existing.name || (approvalPath ? path.basename(approvalPath) : '') || '').trim();
|
|
127
|
+
const approvalType = String(existing.type || '').trim();
|
|
128
|
+
|
|
129
|
+
const approval = {};
|
|
130
|
+
if (approvalPath) approval.path = approvalPath;
|
|
131
|
+
if (approvalName) approval.name = approvalName;
|
|
132
|
+
if (approvalType) approval.type = approvalType;
|
|
133
|
+
return Object.keys(approval).length > 0 ? approval : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function normalizeToolArguments(toolName, args, rawArguments) {
|
|
137
|
+
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
138
|
+
const primitive =
|
|
139
|
+
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
140
|
+
? args
|
|
141
|
+
: null;
|
|
142
|
+
const source =
|
|
143
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
144
|
+
? { ...args }
|
|
145
|
+
: {};
|
|
146
|
+
|
|
147
|
+
if (primitive != null && typeof primitive !== 'object') {
|
|
148
|
+
source._raw = rawText || String(primitive);
|
|
149
|
+
} else if (!source._raw && rawText && source._invalid_json) {
|
|
150
|
+
source._raw = rawText;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const stringValue =
|
|
154
|
+
typeof primitive === 'string'
|
|
155
|
+
? primitive.trim()
|
|
156
|
+
: String(source._raw || '').trim();
|
|
157
|
+
|
|
158
|
+
if (toolName === 'read') return normalizeReadArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
159
|
+
if (toolName === 'list') return normalizePathArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) }, ['dir', 'directory']);
|
|
160
|
+
if (toolName === 'glob') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['glob', 'query'], ['directory']);
|
|
161
|
+
if (toolName === 'grep') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
|
|
162
|
+
if (toolName === 'write') return normalizeWriteArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
163
|
+
|
|
164
|
+
if (toolName === 'edit') {
|
|
165
|
+
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
166
|
+
if (value && !source.path) source.path = value;
|
|
167
|
+
return source;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === 'delete') {
|
|
171
|
+
const normalized = normalizePathArgs(
|
|
172
|
+
{ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) },
|
|
173
|
+
['file_path', 'file', 'target', 'directory', 'dir']
|
|
174
|
+
);
|
|
175
|
+
const approval = buildDeleteApprovalDetails(normalized, normalized.path);
|
|
176
|
+
if (approval) normalized.approval = approval;
|
|
177
|
+
return normalized;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return source;
|
|
181
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { BoundedCache } from './bounded-cache.js';
|
|
5
|
+
import { trimInline } from './string-utils.js';
|
|
6
|
+
|
|
7
|
+
const TOOL_RESULT_DISK_THRESHOLD = 6000;
|
|
8
|
+
const PREVIEW_SIZE_BYTES = 2000;
|
|
9
|
+
const TOOL_RESULTS_SUBDIR = 'tool-results';
|
|
10
|
+
|
|
11
|
+
let currentResultDir = null;
|
|
12
|
+
let resultDirReady = false;
|
|
13
|
+
|
|
14
|
+
const storedResults = new BoundedCache({
|
|
15
|
+
maxSize: 64,
|
|
16
|
+
ttlMs: 30 * 60 * 1000,
|
|
17
|
+
onEvict(_key, value) {
|
|
18
|
+
if (value?.filePath) {
|
|
19
|
+
fs.unlink(value.filePath).catch(() => {});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 });
|
|
25
|
+
|
|
26
|
+
function generatePreview(content) {
|
|
27
|
+
if (content.length <= PREVIEW_SIZE_BYTES) {
|
|
28
|
+
return { preview: content, hasMore: false };
|
|
29
|
+
}
|
|
30
|
+
const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
|
|
31
|
+
const lastNewline = truncated.lastIndexOf('\n');
|
|
32
|
+
const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
|
|
33
|
+
return { preview: content.slice(0, cutPoint), hasMore: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatFileSize(chars) {
|
|
37
|
+
if (chars < 1024) return `${chars} B`;
|
|
38
|
+
return `${(chars / 1024).toFixed(1)} KB`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setResultDir(dir) {
|
|
42
|
+
currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
|
|
43
|
+
resultDirReady = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ensureResultDir() {
|
|
47
|
+
if (!currentResultDir) return false;
|
|
48
|
+
if (!resultDirReady) {
|
|
49
|
+
await fs.mkdir(currentResultDir, { recursive: true });
|
|
50
|
+
resultDirReady = true;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function storeResultIfNeeded(callId, formattedContent, rawResult) {
|
|
56
|
+
if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
|
|
57
|
+
return formattedContent;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const ready = await ensureResultDir();
|
|
61
|
+
const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
|
|
62
|
+
if (!resultDirReady && dir === currentResultDir) {
|
|
63
|
+
await fs.mkdir(dir, { recursive: true });
|
|
64
|
+
} else if (!resultDirReady) {
|
|
65
|
+
await fs.mkdir(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
const filePath = path.join(dir, `${callId}.txt`);
|
|
68
|
+
const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
69
|
+
await fs.writeFile(filePath, payload, 'utf-8');
|
|
70
|
+
const summary = summarizeToolResult(rawResult);
|
|
71
|
+
const { preview, hasMore } = generatePreview(payload);
|
|
72
|
+
storedResults.set(callId, { filePath, summary });
|
|
73
|
+
|
|
74
|
+
return `<persisted-output>
|
|
75
|
+
Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
|
|
76
|
+
|
|
77
|
+
Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
|
|
78
|
+
${preview}${hasMore ? '\n...' : ''}
|
|
79
|
+
|
|
80
|
+
Summary: ${summary}
|
|
81
|
+
</persisted-output>`;
|
|
82
|
+
} catch {
|
|
83
|
+
return formattedContent;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clearResultStore() {
|
|
88
|
+
const files = [];
|
|
89
|
+
for (const [, val] of storedResults.entries()) {
|
|
90
|
+
files.push(val.filePath);
|
|
91
|
+
}
|
|
92
|
+
storedResults.clear();
|
|
93
|
+
readCache.clear();
|
|
94
|
+
return Promise.allSettled(files.map((filePath) => fs.unlink(filePath).catch(() => {})));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
98
|
+
const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
|
|
99
|
+
if (readCache.has(key)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
readCache.set(key, true);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function summarizeToolResult(result) {
|
|
107
|
+
if (result === null || result === undefined) return 'no output';
|
|
108
|
+
if (typeof result === 'string') {
|
|
109
|
+
const oneLine = result.replace(/\s+/g, ' ').trim();
|
|
110
|
+
return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
|
|
111
|
+
}
|
|
112
|
+
if (typeof result === 'object') {
|
|
113
|
+
const obj = result;
|
|
114
|
+
if (Array.isArray(obj)) return `array(${obj.length})`;
|
|
115
|
+
if ('deleted' in obj && 'path' in obj) {
|
|
116
|
+
const kind = trimInline(obj.type || 'item', 16);
|
|
117
|
+
const target = trimInline(obj.path || '', 96);
|
|
118
|
+
if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
|
|
119
|
+
if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
|
|
120
|
+
}
|
|
121
|
+
if ('path' in obj && 'action' in obj) {
|
|
122
|
+
const p = String(obj.path || '');
|
|
123
|
+
const action = String(obj.action || 'write');
|
|
124
|
+
const line = Number(obj.changed_line || 1);
|
|
125
|
+
const suffix =
|
|
126
|
+
action === 'delete'
|
|
127
|
+
? 'deleted'
|
|
128
|
+
: action === 'create'
|
|
129
|
+
? 'created'
|
|
130
|
+
: action === 'patch'
|
|
131
|
+
? 'patched'
|
|
132
|
+
: action === 'replace_block' || action === 'replace_text'
|
|
133
|
+
? 'edited'
|
|
134
|
+
: action === 'append'
|
|
135
|
+
? 'appended'
|
|
136
|
+
: 'updated';
|
|
137
|
+
return p ? `${suffix} ${p}${line > 0 ? ` @L${line}` : ''}` : suffix;
|
|
138
|
+
}
|
|
139
|
+
if ('path' in obj && 'phase' in obj) {
|
|
140
|
+
const phase = String(obj.phase || '');
|
|
141
|
+
const p = String(obj.path || '');
|
|
142
|
+
const total = Number(obj.total_lines);
|
|
143
|
+
const start =
|
|
144
|
+
Number(obj.suggested_start_line || obj.start_line) > 0
|
|
145
|
+
? Number(obj.suggested_start_line || obj.start_line)
|
|
146
|
+
: 1;
|
|
147
|
+
const end =
|
|
148
|
+
Number(obj.suggested_end_line || obj.end_line) >= start
|
|
149
|
+
? Number(obj.suggested_end_line || obj.end_line)
|
|
150
|
+
: start;
|
|
151
|
+
const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
|
|
152
|
+
const totalText = total > 0 ? ` of ${total}` : '';
|
|
153
|
+
const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
|
|
154
|
+
const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
|
|
155
|
+
const truncatedText = obj.truncated ? ' [truncated]' : '';
|
|
156
|
+
return phase === 'metadata'
|
|
157
|
+
? `metadata for ${p}${rangeText}${totalText}${errorText}`
|
|
158
|
+
: `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
|
|
159
|
+
}
|
|
160
|
+
if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
|
|
161
|
+
const stdout = trimInline(obj.stdout || '', 96);
|
|
162
|
+
const stderr = trimInline(obj.stderr || '', 96);
|
|
163
|
+
const command = trimInline(obj.command || '', 72);
|
|
164
|
+
const lead = command ? `${command} -> ` : '';
|
|
165
|
+
if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
|
|
166
|
+
if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
|
|
167
|
+
return `${lead}exit ${obj.code ?? 0}`;
|
|
168
|
+
}
|
|
169
|
+
if ('task_id' in obj && 'startup_confirmed' in obj) {
|
|
170
|
+
const status = trimInline(obj.status || 'unknown', 32);
|
|
171
|
+
const taskId = trimInline(obj.task_id || '', 24);
|
|
172
|
+
const source = trimInline(obj.startup_source || '', 24);
|
|
173
|
+
const outputFile = trimInline(obj.output_file || '', 72);
|
|
174
|
+
const output = Array.isArray(obj.recent_output) ? trimInline(obj.recent_output.slice(-1)[0] || '', 96) : '';
|
|
175
|
+
return `${taskId || 'task'} ${status}${source ? ` (${source})` : ''}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
|
|
176
|
+
}
|
|
177
|
+
if ('tasks' in obj && Array.isArray(obj.tasks)) {
|
|
178
|
+
const count = obj.tasks.length;
|
|
179
|
+
const first = obj.tasks[0];
|
|
180
|
+
const lead = first?.task_id ? `${trimInline(first.task_id, 24)} ${trimInline(first.status || 'unknown', 24)}` : '';
|
|
181
|
+
return `tasks(${count})${lead ? `\n${lead}` : ''}`;
|
|
182
|
+
}
|
|
183
|
+
if ('files' in obj && Array.isArray(obj.files)) {
|
|
184
|
+
return `patched ${obj.files.length} file(s)`;
|
|
185
|
+
}
|
|
186
|
+
if ('diff' in obj && 'new_hash' in obj && 'path' in obj) {
|
|
187
|
+
const p = String(obj.path || '');
|
|
188
|
+
return p ? `diff preview for ${p}` : 'diff preview';
|
|
189
|
+
}
|
|
190
|
+
if ('created' in obj && Array.isArray(obj.created)) {
|
|
191
|
+
return `created ${obj.created.length} task(s)`;
|
|
192
|
+
}
|
|
193
|
+
if ('tasks' in obj && Array.isArray(obj.tasks)) {
|
|
194
|
+
return `${obj.tasks.length} task(s)`;
|
|
195
|
+
}
|
|
196
|
+
if ('newTodos' in obj && Array.isArray(obj.newTodos)) {
|
|
197
|
+
return obj.newTodos.length > 0 ? `updated ${obj.newTodos.length} todo item(s)` : 'cleared todo list';
|
|
198
|
+
}
|
|
199
|
+
if ('newPlan' in obj) {
|
|
200
|
+
return obj.newPlan ? `updated plan state (${String(obj.newPlan.status || 'draft')})` : 'cleared plan state';
|
|
201
|
+
}
|
|
202
|
+
const keys = Object.keys(obj);
|
|
203
|
+
return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
|
|
204
|
+
}
|
|
205
|
+
return String(result);
|
|
206
|
+
}
|