stella-timeline-plugin 2.0.2 → 2.0.4
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/dist/index.js +1 -1
- package/openclaw.plugin.json +54 -54
- package/package.json +72 -72
- package/scripts/doctor-openclaw-workspace.mjs +94 -153
- package/scripts/migrate-existing-memory.mjs +153 -153
- package/scripts/release.mjs +55 -55
- package/scripts/run-openclaw-live-e2e.mjs +64 -64
- package/scripts/setup-openclaw-workspace.mjs +119 -196
- package/skills/timeline/SKILL.md +111 -111
- package/templates/AGENTS.fragment.md +29 -29
- package/templates/SOUL.fragment.md +17 -17
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
function parseArgs(argv) {
|
|
5
|
-
const args = { root: 'memory', apply: false };
|
|
6
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
7
|
-
const token = argv[i];
|
|
8
|
-
if (token === '--apply') {
|
|
9
|
-
args.apply = true;
|
|
10
|
-
continue;
|
|
11
|
-
}
|
|
12
|
-
if (token === '--root') {
|
|
13
|
-
args.root = argv[i + 1] || args.root;
|
|
14
|
-
i += 1;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return args;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function escapeRegExp(value) {
|
|
21
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseMemoryFile(content) {
|
|
25
|
-
const episodes = [];
|
|
26
|
-
if (!content || !content.trim()) return episodes;
|
|
27
|
-
|
|
28
|
-
const parts = content.split(/^### \[/m);
|
|
29
|
-
for (const part of parts) {
|
|
30
|
-
if (!part.trim()) continue;
|
|
31
|
-
const timestampMatch = part.match(/[-*]\s*Timestamp:\s*([^\n]+)/i);
|
|
32
|
-
if (!timestampMatch) continue;
|
|
33
|
-
|
|
34
|
-
const locationMatch = part.match(/[-*]\s*Location:\s*([^\n]+)/i);
|
|
35
|
-
const actionMatch = part.match(/[-*]\s*Action:\s*([^\n]+)/i);
|
|
36
|
-
const emotionTagsMatch = part.match(/[-*]\s*Emotion_Tags:\s*\[([^\]]+)\]/i)
|
|
37
|
-
|| part.match(/[-*]\s*Emotion_Tags:\s*([^\n]+)/i);
|
|
38
|
-
const appearanceMatch = part.match(/[-*]\s*Appearance:\s*([^\n]+)/i);
|
|
39
|
-
const monologueMatch = part.match(/[-*]\s*Internal_Monologue:\s*([^\n]+)/i);
|
|
40
|
-
|
|
41
|
-
const emotionTags = emotionTagsMatch
|
|
42
|
-
? emotionTagsMatch[1].split(',').map((tag) => tag.replace(/[\[\]]/g, '').trim()).filter(Boolean)
|
|
43
|
-
: ['neutral'];
|
|
44
|
-
|
|
45
|
-
episodes.push({
|
|
46
|
-
timestamp: timestampMatch[1].trim(),
|
|
47
|
-
location: locationMatch ? locationMatch[1].trim() : 'unknown',
|
|
48
|
-
action: actionMatch ? actionMatch[1].trim() : 'unknown',
|
|
49
|
-
emotionTags: emotionTags.length ? emotionTags : ['neutral'],
|
|
50
|
-
appearance: appearanceMatch ? appearanceMatch[1].trim() : 'unknown',
|
|
51
|
-
internalMonologue: monologueMatch ? monologueMatch[1].trim() : '',
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return episodes;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isSafelyStructured(content) {
|
|
59
|
-
const parts = content.split(/^### \[/m).filter((part) => part.trim());
|
|
60
|
-
if (parts.length === 0) return false;
|
|
61
|
-
return parts.every((part) => /[-*]\s*Timestamp:\s*([^\n]+)/i.test(part));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatHeading(action, timestamp) {
|
|
65
|
-
const timeMatch = timestamp.match(/(\d{2}:\d{2}:\d{2})/);
|
|
66
|
-
const time = timeMatch ? timeMatch[1] : '00:00:00';
|
|
67
|
-
return `### [${time}] ${action.slice(0, 15)}...`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function formatEpisode(episode) {
|
|
71
|
-
const lines = [
|
|
72
|
-
formatHeading(episode.action, episode.timestamp),
|
|
73
|
-
'',
|
|
74
|
-
`- Timestamp: ${episode.timestamp.replace('T', ' ').replace(/([+-]\d{2}:\d{2}|Z)$/, '')}`,
|
|
75
|
-
`- Location: ${episode.location}`,
|
|
76
|
-
`- Action: ${episode.action}`,
|
|
77
|
-
`- Emotion_Tags: [${episode.emotionTags.join(', ')}]`,
|
|
78
|
-
`- Appearance: ${episode.appearance}`,
|
|
79
|
-
];
|
|
80
|
-
if (episode.internalMonologue) {
|
|
81
|
-
lines.push(`- Internal_Monologue: ${episode.internalMonologue}`);
|
|
82
|
-
}
|
|
83
|
-
lines.push('');
|
|
84
|
-
return `${lines.join('\n')}\n`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function collectDailyFiles(rootDir) {
|
|
88
|
-
if (!fs.existsSync(rootDir)) return [];
|
|
89
|
-
return fs.readdirSync(rootDir)
|
|
90
|
-
.filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name))
|
|
91
|
-
.map((name) => path.join(rootDir, name))
|
|
92
|
-
.sort();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function ensureBackup(filePath, content) {
|
|
96
|
-
const backupPath = `${filePath}.bak`;
|
|
97
|
-
if (!fs.existsSync(backupPath)) {
|
|
98
|
-
fs.writeFileSync(backupPath, content, 'utf8');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function migrateFile(filePath, apply) {
|
|
103
|
-
const original = fs.readFileSync(filePath, 'utf8');
|
|
104
|
-
if (!original.trim()) {
|
|
105
|
-
return { file: filePath, status: 'skipped_empty' };
|
|
106
|
-
}
|
|
107
|
-
if (!isSafelyStructured(original)) {
|
|
108
|
-
return { file: filePath, status: 'skipped_unstructured' };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const episodes = parseMemoryFile(original);
|
|
112
|
-
if (episodes.length === 0) {
|
|
113
|
-
return { file: filePath, status: 'skipped_unstructured' };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const normalized = episodes.map((episode) => formatEpisode(episode)).join('');
|
|
117
|
-
if (normalized === original) {
|
|
118
|
-
return { file: filePath, status: 'unchanged', episodes: episodes.length };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (apply) {
|
|
122
|
-
ensureBackup(filePath, original);
|
|
123
|
-
fs.writeFileSync(filePath, normalized, 'utf8');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
file: filePath,
|
|
128
|
-
status: apply ? 'migrated' : 'would_migrate',
|
|
129
|
-
episodes: episodes.length,
|
|
130
|
-
backup: apply ? `${filePath}.bak` : undefined,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function main() {
|
|
135
|
-
const args = parseArgs(process.argv.slice(2));
|
|
136
|
-
const rootDir = path.resolve(process.cwd(), args.root);
|
|
137
|
-
const files = collectDailyFiles(rootDir);
|
|
138
|
-
const results = files.map((filePath) => migrateFile(filePath, args.apply));
|
|
139
|
-
const summary = {
|
|
140
|
-
root: rootDir,
|
|
141
|
-
apply: args.apply,
|
|
142
|
-
scanned: files.length,
|
|
143
|
-
migrated: results.filter((item) => item.status === 'migrated').length,
|
|
144
|
-
would_migrate: results.filter((item) => item.status === 'would_migrate').length,
|
|
145
|
-
unchanged: results.filter((item) => item.status === 'unchanged').length,
|
|
146
|
-
skipped_unstructured: results.filter((item) => item.status === 'skipped_unstructured').length,
|
|
147
|
-
skipped_empty: results.filter((item) => item.status === 'skipped_empty').length,
|
|
148
|
-
results,
|
|
149
|
-
};
|
|
150
|
-
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
main();
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const args = { root: 'memory', apply: false };
|
|
6
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
7
|
+
const token = argv[i];
|
|
8
|
+
if (token === '--apply') {
|
|
9
|
+
args.apply = true;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (token === '--root') {
|
|
13
|
+
args.root = argv[i + 1] || args.root;
|
|
14
|
+
i += 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return args;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function escapeRegExp(value) {
|
|
21
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseMemoryFile(content) {
|
|
25
|
+
const episodes = [];
|
|
26
|
+
if (!content || !content.trim()) return episodes;
|
|
27
|
+
|
|
28
|
+
const parts = content.split(/^### \[/m);
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
if (!part.trim()) continue;
|
|
31
|
+
const timestampMatch = part.match(/[-*]\s*Timestamp:\s*([^\n]+)/i);
|
|
32
|
+
if (!timestampMatch) continue;
|
|
33
|
+
|
|
34
|
+
const locationMatch = part.match(/[-*]\s*Location:\s*([^\n]+)/i);
|
|
35
|
+
const actionMatch = part.match(/[-*]\s*Action:\s*([^\n]+)/i);
|
|
36
|
+
const emotionTagsMatch = part.match(/[-*]\s*Emotion_Tags:\s*\[([^\]]+)\]/i)
|
|
37
|
+
|| part.match(/[-*]\s*Emotion_Tags:\s*([^\n]+)/i);
|
|
38
|
+
const appearanceMatch = part.match(/[-*]\s*Appearance:\s*([^\n]+)/i);
|
|
39
|
+
const monologueMatch = part.match(/[-*]\s*Internal_Monologue:\s*([^\n]+)/i);
|
|
40
|
+
|
|
41
|
+
const emotionTags = emotionTagsMatch
|
|
42
|
+
? emotionTagsMatch[1].split(',').map((tag) => tag.replace(/[\[\]]/g, '').trim()).filter(Boolean)
|
|
43
|
+
: ['neutral'];
|
|
44
|
+
|
|
45
|
+
episodes.push({
|
|
46
|
+
timestamp: timestampMatch[1].trim(),
|
|
47
|
+
location: locationMatch ? locationMatch[1].trim() : 'unknown',
|
|
48
|
+
action: actionMatch ? actionMatch[1].trim() : 'unknown',
|
|
49
|
+
emotionTags: emotionTags.length ? emotionTags : ['neutral'],
|
|
50
|
+
appearance: appearanceMatch ? appearanceMatch[1].trim() : 'unknown',
|
|
51
|
+
internalMonologue: monologueMatch ? monologueMatch[1].trim() : '',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return episodes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isSafelyStructured(content) {
|
|
59
|
+
const parts = content.split(/^### \[/m).filter((part) => part.trim());
|
|
60
|
+
if (parts.length === 0) return false;
|
|
61
|
+
return parts.every((part) => /[-*]\s*Timestamp:\s*([^\n]+)/i.test(part));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatHeading(action, timestamp) {
|
|
65
|
+
const timeMatch = timestamp.match(/(\d{2}:\d{2}:\d{2})/);
|
|
66
|
+
const time = timeMatch ? timeMatch[1] : '00:00:00';
|
|
67
|
+
return `### [${time}] ${action.slice(0, 15)}...`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatEpisode(episode) {
|
|
71
|
+
const lines = [
|
|
72
|
+
formatHeading(episode.action, episode.timestamp),
|
|
73
|
+
'',
|
|
74
|
+
`- Timestamp: ${episode.timestamp.replace('T', ' ').replace(/([+-]\d{2}:\d{2}|Z)$/, '')}`,
|
|
75
|
+
`- Location: ${episode.location}`,
|
|
76
|
+
`- Action: ${episode.action}`,
|
|
77
|
+
`- Emotion_Tags: [${episode.emotionTags.join(', ')}]`,
|
|
78
|
+
`- Appearance: ${episode.appearance}`,
|
|
79
|
+
];
|
|
80
|
+
if (episode.internalMonologue) {
|
|
81
|
+
lines.push(`- Internal_Monologue: ${episode.internalMonologue}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push('');
|
|
84
|
+
return `${lines.join('\n')}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collectDailyFiles(rootDir) {
|
|
88
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
89
|
+
return fs.readdirSync(rootDir)
|
|
90
|
+
.filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name))
|
|
91
|
+
.map((name) => path.join(rootDir, name))
|
|
92
|
+
.sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureBackup(filePath, content) {
|
|
96
|
+
const backupPath = `${filePath}.bak`;
|
|
97
|
+
if (!fs.existsSync(backupPath)) {
|
|
98
|
+
fs.writeFileSync(backupPath, content, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function migrateFile(filePath, apply) {
|
|
103
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
104
|
+
if (!original.trim()) {
|
|
105
|
+
return { file: filePath, status: 'skipped_empty' };
|
|
106
|
+
}
|
|
107
|
+
if (!isSafelyStructured(original)) {
|
|
108
|
+
return { file: filePath, status: 'skipped_unstructured' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const episodes = parseMemoryFile(original);
|
|
112
|
+
if (episodes.length === 0) {
|
|
113
|
+
return { file: filePath, status: 'skipped_unstructured' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalized = episodes.map((episode) => formatEpisode(episode)).join('');
|
|
117
|
+
if (normalized === original) {
|
|
118
|
+
return { file: filePath, status: 'unchanged', episodes: episodes.length };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (apply) {
|
|
122
|
+
ensureBackup(filePath, original);
|
|
123
|
+
fs.writeFileSync(filePath, normalized, 'utf8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
file: filePath,
|
|
128
|
+
status: apply ? 'migrated' : 'would_migrate',
|
|
129
|
+
episodes: episodes.length,
|
|
130
|
+
backup: apply ? `${filePath}.bak` : undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function main() {
|
|
135
|
+
const args = parseArgs(process.argv.slice(2));
|
|
136
|
+
const rootDir = path.resolve(process.cwd(), args.root);
|
|
137
|
+
const files = collectDailyFiles(rootDir);
|
|
138
|
+
const results = files.map((filePath) => migrateFile(filePath, args.apply));
|
|
139
|
+
const summary = {
|
|
140
|
+
root: rootDir,
|
|
141
|
+
apply: args.apply,
|
|
142
|
+
scanned: files.length,
|
|
143
|
+
migrated: results.filter((item) => item.status === 'migrated').length,
|
|
144
|
+
would_migrate: results.filter((item) => item.status === 'would_migrate').length,
|
|
145
|
+
unchanged: results.filter((item) => item.status === 'unchanged').length,
|
|
146
|
+
skipped_unstructured: results.filter((item) => item.status === 'skipped_unstructured').length,
|
|
147
|
+
skipped_empty: results.filter((item) => item.status === 'skipped_empty').length,
|
|
148
|
+
results,
|
|
149
|
+
};
|
|
150
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main();
|
package/scripts/release.mjs
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
-
|
|
8
|
-
function npmCommand() {
|
|
9
|
-
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function run(command, args) {
|
|
13
|
-
const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
14
|
-
const result = spawnSync(command, args, {
|
|
15
|
-
cwd: repoRoot,
|
|
16
|
-
stdio: 'inherit',
|
|
17
|
-
shell: useShell,
|
|
18
|
-
env: { ...process.env },
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (result.status !== 0) {
|
|
22
|
-
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function printHelp() {
|
|
27
|
-
console.log([
|
|
28
|
-
'Usage: npm run release -- [npm publish args]',
|
|
29
|
-
'',
|
|
30
|
-
'Runs verify, then publishes the current package to npm.',
|
|
31
|
-
'Uses the npm credentials from your local npm login (~/.npmrc).',
|
|
32
|
-
'Examples:',
|
|
33
|
-
' npm run release',
|
|
34
|
-
' npm run release -- --tag next',
|
|
35
|
-
' npm run release -- --dry-run',
|
|
36
|
-
].join('\n'));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function main() {
|
|
40
|
-
const publishArgs = process.argv.slice(2);
|
|
41
|
-
if (publishArgs.includes('--help') || publishArgs.includes('-h')) {
|
|
42
|
-
printHelp();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
run(npmCommand(), ['run', 'verify']);
|
|
47
|
-
run(npmCommand(), ['publish', ...publishArgs]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
main();
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
|
|
8
|
+
function npmCommand() {
|
|
9
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function run(command, args) {
|
|
13
|
+
const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
14
|
+
const result = spawnSync(command, args, {
|
|
15
|
+
cwd: repoRoot,
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
shell: useShell,
|
|
18
|
+
env: { ...process.env },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log([
|
|
28
|
+
'Usage: npm run release -- [npm publish args]',
|
|
29
|
+
'',
|
|
30
|
+
'Runs verify, then publishes the current package to npm.',
|
|
31
|
+
'Uses the npm credentials from your local npm login (~/.npmrc).',
|
|
32
|
+
'Examples:',
|
|
33
|
+
' npm run release',
|
|
34
|
+
' npm run release -- --tag next',
|
|
35
|
+
' npm run release -- --dry-run',
|
|
36
|
+
].join('\n'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
const publishArgs = process.argv.slice(2);
|
|
41
|
+
if (publishArgs.includes('--help') || publishArgs.includes('-h')) {
|
|
42
|
+
printHelp();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run(npmCommand(), ['run', 'verify']);
|
|
47
|
+
run(npmCommand(), ['publish', ...publishArgs]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
main();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
7
|
-
|
|
8
|
-
function withRuntimeTemp(env = process.env) {
|
|
9
|
-
return {
|
|
10
|
-
...env,
|
|
11
|
-
TMPDIR: runtimeTempDir,
|
|
12
|
-
TMP: runtimeTempDir,
|
|
13
|
-
TEMP: runtimeTempDir,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveOnPath(binaryName) {
|
|
18
|
-
const command = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
-
const result = spawnSync(command, [binaryName], {
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
env: withRuntimeTemp(),
|
|
22
|
-
});
|
|
23
|
-
if (result.status !== 0) return '';
|
|
24
|
-
const firstLine = result.stdout
|
|
25
|
-
.split(/\r?\n/)
|
|
26
|
-
.map((line) => line.trim())
|
|
27
|
-
.find(Boolean);
|
|
28
|
-
return firstLine || '';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function resolveOpenClawBin() {
|
|
32
|
-
if (process.env.OPENCLAW_BIN?.trim()) return process.env.OPENCLAW_BIN.trim();
|
|
33
|
-
|
|
34
|
-
const resolved = resolveOnPath('openclaw');
|
|
35
|
-
if (resolved) return resolved;
|
|
36
|
-
|
|
37
|
-
const homeCandidate = path.join(os.homedir(), '.nvm', 'versions', 'node', 'v24.9.0', 'bin', 'openclaw');
|
|
38
|
-
if (fs.existsSync(homeCandidate)) return homeCandidate;
|
|
39
|
-
|
|
40
|
-
throw new Error(
|
|
41
|
-
[
|
|
42
|
-
'Could not locate the openclaw executable.',
|
|
43
|
-
'Make sure openclaw is installed, or set OPENCLAW_BIN explicitly before running.',
|
|
44
|
-
'Example: OPENCLAW_BIN=/Users/zangtao/.nvm/versions/node/v24.9.0/bin/openclaw npm run test:live-experience',
|
|
45
|
-
].join('\n'),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const openClawBin = resolveOpenClawBin();
|
|
50
|
-
|
|
51
|
-
const result = spawnSync(
|
|
52
|
-
process.execPath,
|
|
53
|
-
['node_modules/jest/bin/jest.js', '--runInBand', '--runTestsByPath', 'src/integration/openclaw-live-experience.e2e.test.ts'],
|
|
54
|
-
{
|
|
55
|
-
stdio: 'inherit',
|
|
56
|
-
env: {
|
|
57
|
-
...withRuntimeTemp(),
|
|
58
|
-
OPENCLAW_LIVE_E2E: '1',
|
|
59
|
-
OPENCLAW_BIN: openClawBin,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
process.exit(result.status ?? 1);
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
7
|
+
|
|
8
|
+
function withRuntimeTemp(env = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
...env,
|
|
11
|
+
TMPDIR: runtimeTempDir,
|
|
12
|
+
TMP: runtimeTempDir,
|
|
13
|
+
TEMP: runtimeTempDir,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveOnPath(binaryName) {
|
|
18
|
+
const command = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
+
const result = spawnSync(command, [binaryName], {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
env: withRuntimeTemp(),
|
|
22
|
+
});
|
|
23
|
+
if (result.status !== 0) return '';
|
|
24
|
+
const firstLine = result.stdout
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.find(Boolean);
|
|
28
|
+
return firstLine || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveOpenClawBin() {
|
|
32
|
+
if (process.env.OPENCLAW_BIN?.trim()) return process.env.OPENCLAW_BIN.trim();
|
|
33
|
+
|
|
34
|
+
const resolved = resolveOnPath('openclaw');
|
|
35
|
+
if (resolved) return resolved;
|
|
36
|
+
|
|
37
|
+
const homeCandidate = path.join(os.homedir(), '.nvm', 'versions', 'node', 'v24.9.0', 'bin', 'openclaw');
|
|
38
|
+
if (fs.existsSync(homeCandidate)) return homeCandidate;
|
|
39
|
+
|
|
40
|
+
throw new Error(
|
|
41
|
+
[
|
|
42
|
+
'Could not locate the openclaw executable.',
|
|
43
|
+
'Make sure openclaw is installed, or set OPENCLAW_BIN explicitly before running.',
|
|
44
|
+
'Example: OPENCLAW_BIN=/Users/zangtao/.nvm/versions/node/v24.9.0/bin/openclaw npm run test:live-experience',
|
|
45
|
+
].join('\n'),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const openClawBin = resolveOpenClawBin();
|
|
50
|
+
|
|
51
|
+
const result = spawnSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
['node_modules/jest/bin/jest.js', '--runInBand', '--runTestsByPath', 'src/integration/openclaw-live-experience.e2e.test.ts'],
|
|
54
|
+
{
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
env: {
|
|
57
|
+
...withRuntimeTemp(),
|
|
58
|
+
OPENCLAW_LIVE_E2E: '1',
|
|
59
|
+
OPENCLAW_BIN: openClawBin,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
process.exit(result.status ?? 1);
|