@xmemo/skill 1.1.25 → 1.1.26
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/LICENSE +21 -21
- package/README.md +1 -1
- package/bin/install.mjs +251 -251
- package/package.json +1 -1
- package/skill/CHANGELOG.md +12 -0
- package/skill/SKILL.md +10 -4
- package/skill/references/ledger-operations.md +1 -1
- package/skill/scripts/lib/bounded-read.mjs +162 -0
- package/skill/scripts/lib/cli-input.mjs +13 -91
- package/skill/scripts/lib/core.mjs +1 -2
- package/skill/scripts/lib/openclaw-egress.mjs +7 -3
- package/skill/scripts/xmemo-skill.mjs +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 Yonro
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Yonro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx @xmemo/skill install
|
|
|
20
20
|
## Commands
|
|
21
21
|
|
|
22
22
|
- `xmemo-skill install`: Install the bundled skill files
|
|
23
|
-
- `xmemo-skill version`: Print the skill version (`1.1.
|
|
23
|
+
- `xmemo-skill version`: Print the skill version (`1.1.26`)
|
|
24
24
|
- `xmemo-skill help`: Display command usage and options
|
|
25
25
|
|
|
26
26
|
## Package Integrity
|
package/bin/install.mjs
CHANGED
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import { realpathSync } from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import process from 'node:process';
|
|
9
|
-
|
|
10
|
-
const PACKAGE_NAME = '@xmemo/skill';
|
|
11
|
-
const DEFAULT_INSTALL_DIR = 'xmemo-skill';
|
|
12
|
-
|
|
13
|
-
export async function locateSkillSource(metaUrl = import.meta.url) {
|
|
14
|
-
const currentDir = path.dirname(fileURLToPath(metaUrl));
|
|
15
|
-
const candidateStaged = path.resolve(currentDir, '..', 'skill');
|
|
16
|
-
|
|
17
|
-
const stagedStat = await fs.stat(candidateStaged).catch(() => null);
|
|
18
|
-
if (stagedStat?.isDirectory()) return candidateStaged;
|
|
19
|
-
|
|
20
|
-
throw new Error(`Could not locate bundled skill directory: ${candidateStaged}`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function readSkillVersion(source) {
|
|
24
|
-
for (const required of ['SKILL.md', path.join('scripts', 'xmemo-skill.mjs')]) {
|
|
25
|
-
const stat = await fs.stat(path.join(source, required)).catch(() => null);
|
|
26
|
-
if (!stat?.isFile()) throw new Error(`Missing required Skill file: ${required}`);
|
|
27
|
-
}
|
|
28
|
-
const runtime = await fs.readFile(path.join(source, 'scripts', 'xmemo-skill.mjs'), 'utf8');
|
|
29
|
-
const match = runtime.match(/const SKILL_VERSION = '([^']+)';/);
|
|
30
|
-
if (!match?.[1]) throw new Error('The XMemo Skill version could not be determined from scripts/xmemo-skill.mjs.');
|
|
31
|
-
return match[1];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function resolveRealPath(p) {
|
|
35
|
-
try {
|
|
36
|
-
return realpathSync(p);
|
|
37
|
-
} catch {
|
|
38
|
-
const parent = path.dirname(p);
|
|
39
|
-
try {
|
|
40
|
-
return path.join(realpathSync(parent), path.basename(p));
|
|
41
|
-
} catch {
|
|
42
|
-
return path.resolve(p);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function assertSafeTarget(packageRoot, source, target) {
|
|
48
|
-
if (target === path.parse(target).root) {
|
|
49
|
-
throw new Error('Refusing to install a Skill into a filesystem root.');
|
|
50
|
-
}
|
|
51
|
-
const realPkg = resolveRealPath(packageRoot);
|
|
52
|
-
const realSource = resolveRealPath(source);
|
|
53
|
-
const realTarget = resolveRealPath(target);
|
|
54
|
-
|
|
55
|
-
const relPkg = path.relative(realPkg, realTarget);
|
|
56
|
-
if (!relPkg || (!relPkg.startsWith('..') && !path.isAbsolute(relPkg))) {
|
|
57
|
-
throw new Error('Skill destination cannot be the package root or a directory inside it.');
|
|
58
|
-
}
|
|
59
|
-
const relSource = path.relative(realSource, realTarget);
|
|
60
|
-
if (!relSource || (!relSource.startsWith('..') && !path.isAbsolute(relSource))) {
|
|
61
|
-
throw new Error('Skill destination cannot be the skill source or a directory inside it.');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function pathExists(target) {
|
|
66
|
-
try {
|
|
67
|
-
await fs.access(target);
|
|
68
|
-
return true;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
if (error?.code === 'ENOENT') return false;
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function atomicInstall(source, target, replace, { rename = fs.rename } = {}) {
|
|
76
|
-
const parent = path.dirname(target);
|
|
77
|
-
const base = path.basename(target);
|
|
78
|
-
const nonce = `${process.pid}-${randomUUID()}`;
|
|
79
|
-
const staging = path.join(parent, `.${base}.xmemo-staging-${nonce}`);
|
|
80
|
-
const backup = path.join(parent, `.${base}.xmemo-backup-${nonce}`);
|
|
81
|
-
let movedExisting = false;
|
|
82
|
-
|
|
83
|
-
await fs.mkdir(parent, { recursive: true });
|
|
84
|
-
try {
|
|
85
|
-
await fs.cp(source, staging, { recursive: true, errorOnExist: true, force: false });
|
|
86
|
-
if (replace) {
|
|
87
|
-
await rename(target, backup);
|
|
88
|
-
movedExisting = true;
|
|
89
|
-
}
|
|
90
|
-
await rename(staging, target);
|
|
91
|
-
if (movedExisting) {
|
|
92
|
-
await fs.rm(backup, { recursive: true, force: true });
|
|
93
|
-
movedExisting = false;
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (movedExisting && !(await pathExists(target))) {
|
|
97
|
-
await rename(backup, target).catch(() => {});
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
} finally {
|
|
101
|
-
await fs.rm(staging, { recursive: true, force: true }).catch(() => {});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function parseInstallOptions(args) {
|
|
106
|
-
const flags = new Set(['--dry-run', '--force', '--json']);
|
|
107
|
-
const seen = new Set();
|
|
108
|
-
let target = undefined;
|
|
109
|
-
let dryRun = false;
|
|
110
|
-
let force = false;
|
|
111
|
-
let json = false;
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
114
|
-
const token = args[i];
|
|
115
|
-
if (token === '--help' || token === '-h') {
|
|
116
|
-
return { help: true };
|
|
117
|
-
}
|
|
118
|
-
if (token === '--target') {
|
|
119
|
-
if (seen.has(token)) throw new Error('Duplicate option: --target.');
|
|
120
|
-
const val = args[i + 1];
|
|
121
|
-
if (!val || val.startsWith('-')) throw new Error('Option --target requires a value.');
|
|
122
|
-
target = val;
|
|
123
|
-
seen.add(token);
|
|
124
|
-
i += 1;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (!flags.has(token)) {
|
|
128
|
-
throw new Error(`Unknown skill install option: ${token}`);
|
|
129
|
-
}
|
|
130
|
-
if (seen.has(token)) {
|
|
131
|
-
throw new Error(`Duplicate option: ${token}.`);
|
|
132
|
-
}
|
|
133
|
-
seen.add(token);
|
|
134
|
-
if (token === '--dry-run') dryRun = true;
|
|
135
|
-
if (token === '--force') force = true;
|
|
136
|
-
if (token === '--json') json = true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { help: false, target, dryRun, force, json };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function printHelp(stdout = console.log) {
|
|
143
|
-
stdout('XMemo Skill installer (@xmemo/skill)');
|
|
144
|
-
stdout('');
|
|
145
|
-
stdout('Usage:');
|
|
146
|
-
stdout(' xmemo-skill install [--target <dir>] [--dry-run] [--force] [--json]');
|
|
147
|
-
stdout(' xmemo-skill version [--json]');
|
|
148
|
-
stdout(' xmemo-skill help');
|
|
149
|
-
stdout('');
|
|
150
|
-
stdout('Commands:');
|
|
151
|
-
stdout(' install Install the XMemo Skill locally (offline, zero-network)');
|
|
152
|
-
stdout(' version Print the skill version');
|
|
153
|
-
stdout(' help Print this help message');
|
|
154
|
-
stdout('');
|
|
155
|
-
stdout('Options:');
|
|
156
|
-
stdout(' --target <dir> Installation directory (default: xmemo-skill, or $XMEMO_SKILL_DIR)');
|
|
157
|
-
stdout(' --dry-run Simulate installation without writing files');
|
|
158
|
-
stdout(' --force Overwrite destination directory if it already exists');
|
|
159
|
-
stdout(' --json Output results as JSON');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function runCli(argv = process.argv.slice(2), { cwd = process.cwd(), env = process.env, metaUrl = import.meta.url } = {}) {
|
|
163
|
-
const subcommand = argv[0] ?? 'help';
|
|
164
|
-
|
|
165
|
-
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
166
|
-
printHelp();
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const source = await locateSkillSource(metaUrl);
|
|
171
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(metaUrl)), '..');
|
|
172
|
-
const skillVersion = await readSkillVersion(source);
|
|
173
|
-
|
|
174
|
-
if (subcommand === 'version' || subcommand === '--version' || subcommand === '-v') {
|
|
175
|
-
const isJson = argv.includes('--json');
|
|
176
|
-
if (isJson) {
|
|
177
|
-
console.log(JSON.stringify({ package: PACKAGE_NAME, version: skillVersion, skillVersion }, null, 2));
|
|
178
|
-
} else {
|
|
179
|
-
console.log(skillVersion);
|
|
180
|
-
}
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (subcommand !== 'install') {
|
|
185
|
-
throw new Error(`Unknown command: ${subcommand}. Run "xmemo-skill help" for usage.`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const optionArgs = argv.slice(1);
|
|
189
|
-
const options = parseInstallOptions(optionArgs);
|
|
190
|
-
if (options.help) {
|
|
191
|
-
printHelp();
|
|
192
|
-
return 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const targetDir = options.target ?? env.XMEMO_SKILL_DIR ?? DEFAULT_INSTALL_DIR;
|
|
196
|
-
const target = path.resolve(cwd, targetDir);
|
|
197
|
-
|
|
198
|
-
assertSafeTarget(packageRoot, source, target);
|
|
199
|
-
|
|
200
|
-
const exists = await pathExists(target);
|
|
201
|
-
if (exists && !options.force) {
|
|
202
|
-
throw new Error(`Skill destination already exists: ${target}. Use --force to replace it.`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const report = {
|
|
206
|
-
package: PACKAGE_NAME,
|
|
207
|
-
skillVersion,
|
|
208
|
-
source,
|
|
209
|
-
target,
|
|
210
|
-
dryRun: options.dryRun,
|
|
211
|
-
force: options.force,
|
|
212
|
-
replaced: exists && !options.dryRun,
|
|
213
|
-
installed: false,
|
|
214
|
-
networkUsed: false,
|
|
215
|
-
tokenSent: false,
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
if (!options.dryRun) {
|
|
219
|
-
await atomicInstall(source, target, exists);
|
|
220
|
-
report.installed = true;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (options.json) {
|
|
224
|
-
console.log(JSON.stringify(report, null, 2));
|
|
225
|
-
} else {
|
|
226
|
-
console.log(`${options.dryRun ? 'Would install' : 'Installed'} XMemo Skill ${skillVersion} to ${target}`);
|
|
227
|
-
console.log(`Source: ${PACKAGE_NAME} ${skillVersion} (offline; no credential used)`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isDirectExecution() {
|
|
234
|
-
if (!process.argv[1]) return false;
|
|
235
|
-
const currentPath = fileURLToPath(import.meta.url);
|
|
236
|
-
if (process.argv[1] === currentPath || path.resolve(process.argv[1]) === currentPath) {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
return realpathSync(process.argv[1]) === realpathSync(currentPath);
|
|
241
|
-
} catch {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (isDirectExecution()) {
|
|
247
|
-
runCli().catch((err) => {
|
|
248
|
-
console.error(err.message || String(err));
|
|
249
|
-
process.exit(1);
|
|
250
|
-
});
|
|
251
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = '@xmemo/skill';
|
|
11
|
+
const DEFAULT_INSTALL_DIR = 'xmemo-skill';
|
|
12
|
+
|
|
13
|
+
export async function locateSkillSource(metaUrl = import.meta.url) {
|
|
14
|
+
const currentDir = path.dirname(fileURLToPath(metaUrl));
|
|
15
|
+
const candidateStaged = path.resolve(currentDir, '..', 'skill');
|
|
16
|
+
|
|
17
|
+
const stagedStat = await fs.stat(candidateStaged).catch(() => null);
|
|
18
|
+
if (stagedStat?.isDirectory()) return candidateStaged;
|
|
19
|
+
|
|
20
|
+
throw new Error(`Could not locate bundled skill directory: ${candidateStaged}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readSkillVersion(source) {
|
|
24
|
+
for (const required of ['SKILL.md', path.join('scripts', 'xmemo-skill.mjs')]) {
|
|
25
|
+
const stat = await fs.stat(path.join(source, required)).catch(() => null);
|
|
26
|
+
if (!stat?.isFile()) throw new Error(`Missing required Skill file: ${required}`);
|
|
27
|
+
}
|
|
28
|
+
const runtime = await fs.readFile(path.join(source, 'scripts', 'xmemo-skill.mjs'), 'utf8');
|
|
29
|
+
const match = runtime.match(/const SKILL_VERSION = '([^']+)';/);
|
|
30
|
+
if (!match?.[1]) throw new Error('The XMemo Skill version could not be determined from scripts/xmemo-skill.mjs.');
|
|
31
|
+
return match[1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveRealPath(p) {
|
|
35
|
+
try {
|
|
36
|
+
return realpathSync(p);
|
|
37
|
+
} catch {
|
|
38
|
+
const parent = path.dirname(p);
|
|
39
|
+
try {
|
|
40
|
+
return path.join(realpathSync(parent), path.basename(p));
|
|
41
|
+
} catch {
|
|
42
|
+
return path.resolve(p);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertSafeTarget(packageRoot, source, target) {
|
|
48
|
+
if (target === path.parse(target).root) {
|
|
49
|
+
throw new Error('Refusing to install a Skill into a filesystem root.');
|
|
50
|
+
}
|
|
51
|
+
const realPkg = resolveRealPath(packageRoot);
|
|
52
|
+
const realSource = resolveRealPath(source);
|
|
53
|
+
const realTarget = resolveRealPath(target);
|
|
54
|
+
|
|
55
|
+
const relPkg = path.relative(realPkg, realTarget);
|
|
56
|
+
if (!relPkg || (!relPkg.startsWith('..') && !path.isAbsolute(relPkg))) {
|
|
57
|
+
throw new Error('Skill destination cannot be the package root or a directory inside it.');
|
|
58
|
+
}
|
|
59
|
+
const relSource = path.relative(realSource, realTarget);
|
|
60
|
+
if (!relSource || (!relSource.startsWith('..') && !path.isAbsolute(relSource))) {
|
|
61
|
+
throw new Error('Skill destination cannot be the skill source or a directory inside it.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function pathExists(target) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(target);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error?.code === 'ENOENT') return false;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function atomicInstall(source, target, replace, { rename = fs.rename } = {}) {
|
|
76
|
+
const parent = path.dirname(target);
|
|
77
|
+
const base = path.basename(target);
|
|
78
|
+
const nonce = `${process.pid}-${randomUUID()}`;
|
|
79
|
+
const staging = path.join(parent, `.${base}.xmemo-staging-${nonce}`);
|
|
80
|
+
const backup = path.join(parent, `.${base}.xmemo-backup-${nonce}`);
|
|
81
|
+
let movedExisting = false;
|
|
82
|
+
|
|
83
|
+
await fs.mkdir(parent, { recursive: true });
|
|
84
|
+
try {
|
|
85
|
+
await fs.cp(source, staging, { recursive: true, errorOnExist: true, force: false });
|
|
86
|
+
if (replace) {
|
|
87
|
+
await rename(target, backup);
|
|
88
|
+
movedExisting = true;
|
|
89
|
+
}
|
|
90
|
+
await rename(staging, target);
|
|
91
|
+
if (movedExisting) {
|
|
92
|
+
await fs.rm(backup, { recursive: true, force: true });
|
|
93
|
+
movedExisting = false;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (movedExisting && !(await pathExists(target))) {
|
|
97
|
+
await rename(backup, target).catch(() => {});
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
await fs.rm(staging, { recursive: true, force: true }).catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseInstallOptions(args) {
|
|
106
|
+
const flags = new Set(['--dry-run', '--force', '--json']);
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
let target = undefined;
|
|
109
|
+
let dryRun = false;
|
|
110
|
+
let force = false;
|
|
111
|
+
let json = false;
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
114
|
+
const token = args[i];
|
|
115
|
+
if (token === '--help' || token === '-h') {
|
|
116
|
+
return { help: true };
|
|
117
|
+
}
|
|
118
|
+
if (token === '--target') {
|
|
119
|
+
if (seen.has(token)) throw new Error('Duplicate option: --target.');
|
|
120
|
+
const val = args[i + 1];
|
|
121
|
+
if (!val || val.startsWith('-')) throw new Error('Option --target requires a value.');
|
|
122
|
+
target = val;
|
|
123
|
+
seen.add(token);
|
|
124
|
+
i += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!flags.has(token)) {
|
|
128
|
+
throw new Error(`Unknown skill install option: ${token}`);
|
|
129
|
+
}
|
|
130
|
+
if (seen.has(token)) {
|
|
131
|
+
throw new Error(`Duplicate option: ${token}.`);
|
|
132
|
+
}
|
|
133
|
+
seen.add(token);
|
|
134
|
+
if (token === '--dry-run') dryRun = true;
|
|
135
|
+
if (token === '--force') force = true;
|
|
136
|
+
if (token === '--json') json = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { help: false, target, dryRun, force, json };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function printHelp(stdout = console.log) {
|
|
143
|
+
stdout('XMemo Skill installer (@xmemo/skill)');
|
|
144
|
+
stdout('');
|
|
145
|
+
stdout('Usage:');
|
|
146
|
+
stdout(' xmemo-skill install [--target <dir>] [--dry-run] [--force] [--json]');
|
|
147
|
+
stdout(' xmemo-skill version [--json]');
|
|
148
|
+
stdout(' xmemo-skill help');
|
|
149
|
+
stdout('');
|
|
150
|
+
stdout('Commands:');
|
|
151
|
+
stdout(' install Install the XMemo Skill locally (offline, zero-network)');
|
|
152
|
+
stdout(' version Print the skill version');
|
|
153
|
+
stdout(' help Print this help message');
|
|
154
|
+
stdout('');
|
|
155
|
+
stdout('Options:');
|
|
156
|
+
stdout(' --target <dir> Installation directory (default: xmemo-skill, or $XMEMO_SKILL_DIR)');
|
|
157
|
+
stdout(' --dry-run Simulate installation without writing files');
|
|
158
|
+
stdout(' --force Overwrite destination directory if it already exists');
|
|
159
|
+
stdout(' --json Output results as JSON');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function runCli(argv = process.argv.slice(2), { cwd = process.cwd(), env = process.env, metaUrl = import.meta.url } = {}) {
|
|
163
|
+
const subcommand = argv[0] ?? 'help';
|
|
164
|
+
|
|
165
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
166
|
+
printHelp();
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const source = await locateSkillSource(metaUrl);
|
|
171
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(metaUrl)), '..');
|
|
172
|
+
const skillVersion = await readSkillVersion(source);
|
|
173
|
+
|
|
174
|
+
if (subcommand === 'version' || subcommand === '--version' || subcommand === '-v') {
|
|
175
|
+
const isJson = argv.includes('--json');
|
|
176
|
+
if (isJson) {
|
|
177
|
+
console.log(JSON.stringify({ package: PACKAGE_NAME, version: skillVersion, skillVersion }, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(skillVersion);
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (subcommand !== 'install') {
|
|
185
|
+
throw new Error(`Unknown command: ${subcommand}. Run "xmemo-skill help" for usage.`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const optionArgs = argv.slice(1);
|
|
189
|
+
const options = parseInstallOptions(optionArgs);
|
|
190
|
+
if (options.help) {
|
|
191
|
+
printHelp();
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const targetDir = options.target ?? env.XMEMO_SKILL_DIR ?? DEFAULT_INSTALL_DIR;
|
|
196
|
+
const target = path.resolve(cwd, targetDir);
|
|
197
|
+
|
|
198
|
+
assertSafeTarget(packageRoot, source, target);
|
|
199
|
+
|
|
200
|
+
const exists = await pathExists(target);
|
|
201
|
+
if (exists && !options.force) {
|
|
202
|
+
throw new Error(`Skill destination already exists: ${target}. Use --force to replace it.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const report = {
|
|
206
|
+
package: PACKAGE_NAME,
|
|
207
|
+
skillVersion,
|
|
208
|
+
source,
|
|
209
|
+
target,
|
|
210
|
+
dryRun: options.dryRun,
|
|
211
|
+
force: options.force,
|
|
212
|
+
replaced: exists && !options.dryRun,
|
|
213
|
+
installed: false,
|
|
214
|
+
networkUsed: false,
|
|
215
|
+
tokenSent: false,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (!options.dryRun) {
|
|
219
|
+
await atomicInstall(source, target, exists);
|
|
220
|
+
report.installed = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (options.json) {
|
|
224
|
+
console.log(JSON.stringify(report, null, 2));
|
|
225
|
+
} else {
|
|
226
|
+
console.log(`${options.dryRun ? 'Would install' : 'Installed'} XMemo Skill ${skillVersion} to ${target}`);
|
|
227
|
+
console.log(`Source: ${PACKAGE_NAME} ${skillVersion} (offline; no credential used)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDirectExecution() {
|
|
234
|
+
if (!process.argv[1]) return false;
|
|
235
|
+
const currentPath = fileURLToPath(import.meta.url);
|
|
236
|
+
if (process.argv[1] === currentPath || path.resolve(process.argv[1]) === currentPath) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return realpathSync(process.argv[1]) === realpathSync(currentPath);
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isDirectExecution()) {
|
|
247
|
+
runCli().catch((err) => {
|
|
248
|
+
console.error(err.message || String(err));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
251
|
+
}
|
package/package.json
CHANGED
package/skill/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 1.1.26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Classify HTTP 401 and 403 status codes using whole-number word boundaries, avoiding false-positive authentication error classifications on port numbers and identifiers.
|
|
10
|
+
- Clarified documentation for `ledger-list` in `references/ledger-operations.md` and `SKILL.md` to consistently describe it as a strictly read-only query command, explicitly noting that deleting or voiding a ledger entry is a separate operation requiring explicit confirmation (`forget --id <id> --confirm`) and a delete-capable scope (for example `memory:delete`; see the forget section for the full list).
|
|
11
|
+
- Structured reference links in `SKILL.md` as direct Markdown links with one-line descriptions.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Modularized stdin and file input reading into a dedicated helper module (`scripts/lib/bounded-read.mjs`) with explicit streaming byte counting: capped at `MAX_MEMORY_CONTENT_BYTES` (512 KiB) for memory content and `MAX_STDIN_INPUT_BYTES` (64 KiB) for single-value stdin inputs (`auth add`), with early rejection on stream overflow.
|
|
16
|
+
|
|
5
17
|
## 1.1.25
|
|
6
18
|
|
|
7
19
|
### Added
|
package/skill/SKILL.md
CHANGED
|
@@ -289,9 +289,11 @@ remain limited to `remember`, `recall`, and `search`.
|
|
|
289
289
|
- `ledger-list` is a strictly read-only query backed by
|
|
290
290
|
`POST /v1/skill/operations` (`operation: "ledger-list"`, requiring
|
|
291
291
|
`ledger:read` scope). It retrieves financial and expense transactions without
|
|
292
|
-
any write or delete capabilities
|
|
293
|
-
|
|
294
|
-
`forget --id <
|
|
292
|
+
any write or delete capabilities; `ledger-list` only reads and lists records.
|
|
293
|
+
Deleting or voiding a transaction is a separate operation that requires
|
|
294
|
+
explicit confirmation (`forget --id <id> --confirm`) and a delete-capable
|
|
295
|
+
scope (for example `memory:delete`; see the forget section for the full list).
|
|
296
|
+
It accepts `--limit <n>`, `--offset <n>`,
|
|
295
297
|
`--currency <code>`, `--from <date>` (`date_from`), `--to <date>` (`date_to`),
|
|
296
298
|
`--category <name>`, `--min-amount <n>`, `--max-amount <n>`, and `--type <type>`
|
|
297
299
|
(`transaction_type`). As a convenience, `--month <YYYY-MM>` can be specified to
|
|
@@ -416,7 +418,11 @@ items, verify the credential scopes first. A valid `memory:read` token alone is
|
|
|
416
418
|
not proof of Knowledge authorization; do not fall back to a broader token or
|
|
417
419
|
attempt to inspect another user's Knowledge space.
|
|
418
420
|
|
|
419
|
-
For
|
|
421
|
+
For detailed guides and operational references, see:
|
|
422
|
+
- [memory-operations.md](references/memory-operations.md): Core memory, knowledge context, and continuity workflows.
|
|
423
|
+
- [ledger-operations.md](references/ledger-operations.md): Ledger accounting, financial transactions, and account diagnostics.
|
|
424
|
+
- [runtime-operations.md](references/runtime-operations.md): Command matrix, output safety, JSON envelopes, and exit codes.
|
|
425
|
+
- [troubleshooting.md](references/troubleshooting.md): Auth, network, and service diagnosis and recovery.
|
|
420
426
|
|
|
421
427
|
## Exit Codes
|
|
422
428
|
|
|
@@ -28,7 +28,7 @@ node scripts/xmemo-skill.mjs ledger-list --month 2026-09 --json
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`ledger-list` queries personal financial transactions via `POST /v1/skill/operations` (`operation: "ledger-list"`, requiring `ledger:read` scope).
|
|
31
|
-
This command is strictly read-only and possesses zero write or deletion capabilities.
|
|
31
|
+
This command is strictly read-only and possesses zero write or deletion capabilities; `ledger-list` only reads and lists recorded transactions. Deleting or voiding a ledger entry is a separate operation that requires explicit confirmation (`forget --id <id> --confirm`) and a delete-capable scope (for example `memory:delete`; see the forget section for the full list).
|
|
32
32
|
Allowed server arguments:
|
|
33
33
|
- `--limit <n>`: Page limit (default 30, max 100).
|
|
34
34
|
- `--offset <n>`: Pagination offset (default 0).
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { MAX_MEMORY_CONTENT_BYTES, EXIT_CODE } from './core.mjs';
|
|
3
|
+
import { outputContentTooLarge } from './api.mjs';
|
|
4
|
+
|
|
5
|
+
// Maximum bounded payload for single-value stdin inputs: 65536 bytes (64 KiB).
|
|
6
|
+
export const MAX_STDIN_INPUT_BYTES = 65536;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read stdin up to a bounded byte limit (default 64 KiB).
|
|
10
|
+
* Reading halts and rejects immediately if stdin exceeds maxBytes.
|
|
11
|
+
*
|
|
12
|
+
* @param {number} maxBytes - Maximum permitted bytes (default 65536)
|
|
13
|
+
* @returns {Promise<string>}
|
|
14
|
+
*/
|
|
15
|
+
export function readStdin(maxBytes = MAX_STDIN_INPUT_BYTES) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let totalBytes = 0;
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let done = false;
|
|
20
|
+
|
|
21
|
+
const cleanup = () => {
|
|
22
|
+
process.stdin.removeListener('data', onData);
|
|
23
|
+
process.stdin.removeListener('end', onEnd);
|
|
24
|
+
process.stdin.removeListener('error', onError);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const onData = (chunk) => {
|
|
28
|
+
if (done) return;
|
|
29
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
30
|
+
totalBytes += buf.length;
|
|
31
|
+
// Explicit byte counter: stop and reject immediately when exceeding maxBytes
|
|
32
|
+
if (totalBytes > maxBytes) {
|
|
33
|
+
done = true;
|
|
34
|
+
cleanup();
|
|
35
|
+
try { process.stdin.pause(); } catch {}
|
|
36
|
+
const err = new Error(`Input exceeds maximum limit of ${maxBytes} bytes.`);
|
|
37
|
+
err.exitCode = EXIT_CODE.USER_ERROR;
|
|
38
|
+
reject(err);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
chunks.push(buf);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onEnd = () => {
|
|
45
|
+
if (done) return;
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve(Buffer.concat(chunks).toString('utf8').trim());
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onError = (err) => {
|
|
51
|
+
if (done) return;
|
|
52
|
+
cleanup();
|
|
53
|
+
reject(err);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
process.stdin.on('data', onData).on('end', onEnd).on('error', onError).resume();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read memory content from stdin with explicit streaming byte counter.
|
|
62
|
+
* Reading halts and rejects immediately if stdin exceeds MAX_MEMORY_CONTENT_BYTES (524288 bytes).
|
|
63
|
+
*
|
|
64
|
+
* @param {object} options - Command options (json / terminal)
|
|
65
|
+
* @returns {Promise<string>}
|
|
66
|
+
*/
|
|
67
|
+
export function readStdinContent(options = {}) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
// Explicit byte counter: bounded to MAX_MEMORY_CONTENT_BYTES (524288 bytes / 512 KiB)
|
|
70
|
+
let totalBytes = 0;
|
|
71
|
+
const chunks = [];
|
|
72
|
+
let done = false;
|
|
73
|
+
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
process.stdin.removeListener('data', onData);
|
|
76
|
+
process.stdin.removeListener('end', onEnd);
|
|
77
|
+
process.stdin.removeListener('error', onError);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onData = (chunk) => {
|
|
81
|
+
if (done) return;
|
|
82
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
83
|
+
totalBytes += buf.length;
|
|
84
|
+
// Explicit byte counter check: stop and reject immediately if stream exceeds 524288 bytes
|
|
85
|
+
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) {
|
|
86
|
+
done = true;
|
|
87
|
+
cleanup();
|
|
88
|
+
try { process.stdin.pause(); } catch {}
|
|
89
|
+
try { process.stdin.destroy(); } catch {}
|
|
90
|
+
outputContentTooLarge(`Memory content exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
chunks.push(buf);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onEnd = () => {
|
|
97
|
+
if (done) return;
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onError = (err) => {
|
|
103
|
+
if (done) return;
|
|
104
|
+
cleanup();
|
|
105
|
+
reject(err);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
process.stdin.on('data', onData).on('end', onEnd).on('error', onError).resume();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read memory content from a regular file with bounded size verification.
|
|
114
|
+
* Rejects if file exceeds MAX_MEMORY_CONTENT_BYTES (524288 bytes / 512 KiB).
|
|
115
|
+
*
|
|
116
|
+
* @param {string} filePath - Path to file
|
|
117
|
+
* @param {object} options - Command options
|
|
118
|
+
* @returns {Promise<string|null>}
|
|
119
|
+
*/
|
|
120
|
+
export async function readBoundedFile(filePath, options = {}) {
|
|
121
|
+
let stats;
|
|
122
|
+
try {
|
|
123
|
+
stats = await fs.stat(filePath);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`Failed to read file '${filePath}': ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
if (!stats.isFile()) {
|
|
128
|
+
throw new Error(`Failed to read file '${filePath}': --file must be a regular file.`);
|
|
129
|
+
}
|
|
130
|
+
if (stats.size > MAX_MEMORY_CONTENT_BYTES) {
|
|
131
|
+
outputContentTooLarge(`File '${filePath}' exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
let handle;
|
|
135
|
+
try {
|
|
136
|
+
handle = await fs.open(filePath, 'r');
|
|
137
|
+
} catch (err) {
|
|
138
|
+
throw new Error(`Failed to read file '${filePath}': ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let totalBytes = 0;
|
|
142
|
+
const chunkBuf = Buffer.alloc(65536);
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
const toRead = Math.min(65536, (MAX_MEMORY_CONTENT_BYTES + 1) - totalBytes);
|
|
146
|
+
const { bytesRead } = await handle.read(chunkBuf, 0, toRead, null);
|
|
147
|
+
if (bytesRead === 0) break;
|
|
148
|
+
totalBytes += bytesRead;
|
|
149
|
+
chunks.push(Buffer.from(chunkBuf.subarray(0, bytesRead)));
|
|
150
|
+
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) break;
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(`Failed to read file '${filePath}': ${err.message}`);
|
|
154
|
+
} finally {
|
|
155
|
+
await handle.close();
|
|
156
|
+
}
|
|
157
|
+
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) {
|
|
158
|
+
outputContentTooLarge(`File '${filePath}' exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
162
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import {
|
|
3
2
|
DEFAULT_BASE_URL,
|
|
4
3
|
DEFAULT_TIMEOUT_MS,
|
|
5
4
|
MAX_STATE_TTL_SECONDS,
|
|
6
|
-
MAX_MEMORY_CONTENT_BYTES,
|
|
7
5
|
COMMAND_FLAGS,
|
|
8
6
|
AUTH_FLAGS,
|
|
9
7
|
parseStrictBoolean,
|
|
@@ -11,7 +9,11 @@ import {
|
|
|
11
9
|
parseIntegerInRange,
|
|
12
10
|
parseJsonObject,
|
|
13
11
|
} from './core.mjs';
|
|
14
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
readStdin,
|
|
14
|
+
readStdinContent,
|
|
15
|
+
readBoundedFile,
|
|
16
|
+
} from './bounded-read.mjs';
|
|
15
17
|
|
|
16
18
|
export function isStdoutTty() {
|
|
17
19
|
const env = process.env;
|
|
@@ -185,98 +187,18 @@ export function validateCommandInput(command, subcommand, positionals, options,
|
|
|
185
187
|
}
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
process.stdin.on('end', () => { resolve(data.trim()); });
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Read full stdin content helper (exact UTF-8 content without trimming) with byte limit
|
|
198
|
-
export function readStdinContent(options = {}) {
|
|
199
|
-
return new Promise((resolve, reject) => {
|
|
200
|
-
let totalBytes = 0;
|
|
201
|
-
const chunks = [];
|
|
202
|
-
let done = false;
|
|
203
|
-
const cleanup = () => {
|
|
204
|
-
process.stdin.removeListener('data', onData);
|
|
205
|
-
process.stdin.removeListener('end', onEnd);
|
|
206
|
-
process.stdin.removeListener('error', onError);
|
|
207
|
-
};
|
|
208
|
-
const onData = (chunk) => {
|
|
209
|
-
if (done) return;
|
|
210
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
211
|
-
totalBytes += buf.length;
|
|
212
|
-
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) {
|
|
213
|
-
done = true;
|
|
214
|
-
cleanup();
|
|
215
|
-
try { process.stdin.pause(); } catch {}
|
|
216
|
-
try { process.stdin.destroy(); } catch {}
|
|
217
|
-
outputContentTooLarge(`Memory content exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
chunks.push(buf);
|
|
221
|
-
};
|
|
222
|
-
const onEnd = () => {
|
|
223
|
-
if (done) return;
|
|
224
|
-
cleanup();
|
|
225
|
-
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
226
|
-
};
|
|
227
|
-
const onError = (err) => {
|
|
228
|
-
if (done) return;
|
|
229
|
-
cleanup();
|
|
230
|
-
reject(err);
|
|
231
|
-
};
|
|
232
|
-
process.stdin.on('data', onData).on('end', onEnd).on('error', onError).resume();
|
|
233
|
-
});
|
|
234
|
-
}
|
|
190
|
+
export {
|
|
191
|
+
readStdin,
|
|
192
|
+
readStdinContent,
|
|
193
|
+
readBoundedFile,
|
|
194
|
+
};
|
|
235
195
|
|
|
236
196
|
export async function resolveCommandInputs(command, flags, options = {}) {
|
|
237
197
|
if (command === 'remember') {
|
|
238
198
|
if (flags.file !== undefined) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
} catch (err) {
|
|
243
|
-
throw new Error(`Failed to read file '${flags.file}': ${err.message}`);
|
|
244
|
-
}
|
|
245
|
-
if (!stats.isFile()) {
|
|
246
|
-
throw new Error(`Failed to read file '${flags.file}': --file must be a regular file.`);
|
|
247
|
-
}
|
|
248
|
-
if (stats.size > MAX_MEMORY_CONTENT_BYTES) {
|
|
249
|
-
outputContentTooLarge(`File '${flags.file}' exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
let handle;
|
|
253
|
-
try {
|
|
254
|
-
handle = await fs.open(flags.file, 'r');
|
|
255
|
-
} catch (err) {
|
|
256
|
-
throw new Error(`Failed to read file '${flags.file}': ${err.message}`);
|
|
257
|
-
}
|
|
258
|
-
const chunks = [];
|
|
259
|
-
let totalBytes = 0;
|
|
260
|
-
const chunkBuf = Buffer.alloc(65536);
|
|
261
|
-
try {
|
|
262
|
-
while (true) {
|
|
263
|
-
const toRead = Math.min(65536, (MAX_MEMORY_CONTENT_BYTES + 1) - totalBytes);
|
|
264
|
-
const { bytesRead } = await handle.read(chunkBuf, 0, toRead, null);
|
|
265
|
-
if (bytesRead === 0) break;
|
|
266
|
-
totalBytes += bytesRead;
|
|
267
|
-
chunks.push(Buffer.from(chunkBuf.subarray(0, bytesRead)));
|
|
268
|
-
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) break;
|
|
269
|
-
}
|
|
270
|
-
} catch (err) {
|
|
271
|
-
throw new Error(`Failed to read file '${flags.file}': ${err.message}`);
|
|
272
|
-
} finally {
|
|
273
|
-
await handle.close();
|
|
274
|
-
}
|
|
275
|
-
if (totalBytes > MAX_MEMORY_CONTENT_BYTES) {
|
|
276
|
-
outputContentTooLarge(`File '${flags.file}' exceeds maximum limit of ${MAX_MEMORY_CONTENT_BYTES} bytes.`, options);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
flags.content = Buffer.concat(chunks).toString('utf8');
|
|
199
|
+
const content = await readBoundedFile(flags.file, options);
|
|
200
|
+
if (content === null) return;
|
|
201
|
+
flags.content = content;
|
|
280
202
|
delete flags.file;
|
|
281
203
|
} else if (flags.content === '-') {
|
|
282
204
|
flags.content = await readStdinContent(options);
|
|
@@ -120,9 +120,8 @@ export function exitCodeForError(err) {
|
|
|
120
120
|
return EXIT_CODE.SERVER_ERROR;
|
|
121
121
|
}
|
|
122
122
|
if (
|
|
123
|
-
msg
|
|
123
|
+
/\b40[13]\b/.test(msg) ||
|
|
124
124
|
msg.includes('unauthorized') ||
|
|
125
|
-
msg.includes('403') ||
|
|
126
125
|
msg.includes('forbidden') ||
|
|
127
126
|
msg.includes('invalid or expired token') ||
|
|
128
127
|
msg.includes('no xmemo credential found') ||
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_BASE_URL } from './core.mjs';
|
|
1
|
+
import { DEFAULT_BASE_URL, EXIT_CODE } from './core.mjs';
|
|
2
2
|
import { isSurrogateToken, assertSurrogateOrigin } from './muse-vault.mjs';
|
|
3
3
|
|
|
4
4
|
export const OPENCLAW_SENTINEL_REGEX = /^oc-sent-v2\.[A-Za-z0-9_-]+\.end$/;
|
|
@@ -32,16 +32,20 @@ export function assertOpenClawEgress(token, targetUrl, env = process.env) {
|
|
|
32
32
|
if (!isOpenClawSentinel(token)) return;
|
|
33
33
|
|
|
34
34
|
if (!isProxyEnvActive(env)) {
|
|
35
|
-
|
|
35
|
+
const error = new Error(
|
|
36
36
|
'OpenClaw egress proxy is required when using OpenClaw secrets. Enable secrets.egressProxy.enabled and ensure execution runs in Gateway-hosted exec (HTTPS_PROXY and NODE_USE_ENV_PROXY=1 must be set).'
|
|
37
37
|
);
|
|
38
|
+
error.exitCode = EXIT_CODE.USER_ERROR;
|
|
39
|
+
throw error;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const urlObj = typeof targetUrl === 'string' ? new URL(targetUrl) : targetUrl;
|
|
41
43
|
if (urlObj.origin !== ALLOWED_EGRESS_ORIGIN) {
|
|
42
|
-
|
|
44
|
+
const error = new Error(
|
|
43
45
|
`OpenClaw secret sentinels are restricted to ${ALLOWED_EGRESS_ORIGIN} and cannot be sent to ${urlObj.origin}. Unset XMEMO_BASE_URL or use a standard credential.`
|
|
44
46
|
);
|
|
47
|
+
error.exitCode = EXIT_CODE.USER_ERROR;
|
|
48
|
+
throw error;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -58,7 +58,7 @@ import { handleLedger } from './commands/ledger.mjs';
|
|
|
58
58
|
import { handleAccount } from './commands/account.mjs';
|
|
59
59
|
import { handleOps } from './commands/ops.mjs';
|
|
60
60
|
|
|
61
|
-
const SKILL_VERSION = '1.1.
|
|
61
|
+
const SKILL_VERSION = '1.1.26';
|
|
62
62
|
|
|
63
63
|
async function main() {
|
|
64
64
|
let { command, subcommand, positionals, options, flags } = parseArgs(process.argv.slice(2));
|