stella-timeline-plugin 2.0.0
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 +16 -0
- package/README.md +103 -0
- package/README_ZH.md +103 -0
- package/bin/openclaw-timeline-doctor.mjs +2 -0
- package/bin/openclaw-timeline-setup.mjs +2 -0
- package/dist/index.js +26 -0
- package/dist/src/core/build_consumption_view.js +52 -0
- package/dist/src/core/calendar_dates.js +23 -0
- package/dist/src/core/collect_sources.js +39 -0
- package/dist/src/core/collect_timeline_request.js +76 -0
- package/dist/src/core/materialize_generated_candidate.js +87 -0
- package/dist/src/core/resolve_window.js +83 -0
- package/dist/src/core/runtime_guard.js +170 -0
- package/dist/src/core/timeline_reasoner_contract.js +2 -0
- package/dist/src/core/trace.js +22 -0
- package/dist/src/core/world_rhythm.js +258 -0
- package/dist/src/lib/fingerprint.js +46 -0
- package/dist/src/lib/holidays.js +95 -0
- package/dist/src/lib/inherit-appearance.js +46 -0
- package/dist/src/lib/parse-memory.js +171 -0
- package/dist/src/lib/time-utils.js +49 -0
- package/dist/src/lib/timeline_semantics.js +63 -0
- package/dist/src/lib/types.js +2 -0
- package/dist/src/openclaw-sdk-compat.js +39 -0
- package/dist/src/plugin_metadata.js +9 -0
- package/dist/src/runtime/conversation_context.js +128 -0
- package/dist/src/runtime/openclaw_timeline_runtime.js +655 -0
- package/dist/src/storage/daily_log.js +60 -0
- package/dist/src/storage/lock.js +74 -0
- package/dist/src/storage/trace_log.js +70 -0
- package/dist/src/storage/write-episode.js +164 -0
- package/dist/src/tools/timeline_resolve.js +689 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +73 -0
- package/scripts/doctor-openclaw-workspace.mjs +94 -0
- package/scripts/migrate-existing-memory.mjs +153 -0
- package/scripts/release.mjs +99 -0
- package/scripts/run-openclaw-live-e2e.mjs +64 -0
- package/scripts/run-openclaw-smoke.mjs +21 -0
- package/scripts/setup-openclaw-workspace.mjs +119 -0
- package/scripts/workspace-contract.mjs +47 -0
- package/skills/timeline/SKILL.md +111 -0
- package/templates/AGENTS.fragment.md +29 -0
- package/templates/SOUL.fragment.md +17 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "stella-timeline-plugin",
|
|
3
|
+
"name": "Stella Timeline Plugin",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"description": "OpenClaw timeline runtime with canonical timeline_resolve, LLM-based temporal reasoning, and guarded append-only writes.",
|
|
6
|
+
"entry": "dist/index.js",
|
|
7
|
+
"skills": [
|
|
8
|
+
"skills/timeline"
|
|
9
|
+
],
|
|
10
|
+
"configSchema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"enableTrace": {
|
|
14
|
+
"type": "boolean",
|
|
15
|
+
"default": true,
|
|
16
|
+
"description": "Whether Timeline writes trace records for each runtime execution."
|
|
17
|
+
},
|
|
18
|
+
"traceLogPath": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Optional trace log path. Relative paths are resolved from the workspace."
|
|
21
|
+
},
|
|
22
|
+
"canonicalMemoryRoot": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Root directory for canonical timeline daily logs. Relative paths are resolved from the workspace."
|
|
25
|
+
},
|
|
26
|
+
"reasonerTimeoutMs": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"default": 45000,
|
|
29
|
+
"description": "Timeout for the internal timeline reasoner subagent, in milliseconds."
|
|
30
|
+
},
|
|
31
|
+
"reasonerSessionPrefix": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"default": "timeline-reasoner",
|
|
34
|
+
"description": "Session key prefix used for the internal timeline reasoner subagent."
|
|
35
|
+
},
|
|
36
|
+
"reasonerMessageLimit": {
|
|
37
|
+
"type": "integer",
|
|
38
|
+
"default": 24,
|
|
39
|
+
"description": "Maximum number of messages retained when reading the internal reasoner transcript."
|
|
40
|
+
},
|
|
41
|
+
"sessionHistoryLimit": {
|
|
42
|
+
"type": "integer",
|
|
43
|
+
"default": 12,
|
|
44
|
+
"description": "Maximum number of current-session messages retained by the collector."
|
|
45
|
+
},
|
|
46
|
+
"memorySearchMaxResults": {
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"default": 6,
|
|
49
|
+
"description": "Maximum number of results requested when the collector calls OpenClaw memory_search."
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": false
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stella-timeline-plugin",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Native OpenClaw timeline plugin with a canonical timeline_resolve tool, bundled skill routing, and guarded append-only writes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"skill",
|
|
8
|
+
"timeline",
|
|
9
|
+
"memory",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"episode",
|
|
12
|
+
"persona"
|
|
13
|
+
],
|
|
14
|
+
"author": "tao.zang",
|
|
15
|
+
"license": "MIT-0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/tower1229/Her"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/tower1229/Her#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/tower1229/Her/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prebuild": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "node node_modules/jest/bin/jest.js --runInBand",
|
|
32
|
+
"test:unit": "node node_modules/jest/bin/jest.js --runInBand",
|
|
33
|
+
"test:smoke": "node ./scripts/run-openclaw-smoke.mjs",
|
|
34
|
+
"test:live-experience": "node ./scripts/run-openclaw-live-e2e.mjs",
|
|
35
|
+
"migrate:memory": "node ./scripts/migrate-existing-memory.mjs --apply",
|
|
36
|
+
"setup:workspace": "node ./scripts/setup-openclaw-workspace.mjs",
|
|
37
|
+
"doctor:workspace": "node ./scripts/doctor-openclaw-workspace.mjs",
|
|
38
|
+
"verify": "npm run typecheck && npm run build && npm run test:unit",
|
|
39
|
+
"pack:plugin": "npm pack",
|
|
40
|
+
"release": "node ./scripts/release.mjs"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"openclaw-timeline-setup": "./bin/openclaw-timeline-setup.mjs",
|
|
44
|
+
"openclaw-timeline-doctor": "./bin/openclaw-timeline-doctor.mjs"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/jest": "^30.0.0",
|
|
51
|
+
"@types/node": "^20.x",
|
|
52
|
+
"jest": "^29.x",
|
|
53
|
+
"ts-jest": "^29.x",
|
|
54
|
+
"typescript": "^5.x"
|
|
55
|
+
},
|
|
56
|
+
"main": "dist/index.js",
|
|
57
|
+
"files": [
|
|
58
|
+
"dist",
|
|
59
|
+
"bin",
|
|
60
|
+
"scripts",
|
|
61
|
+
"templates",
|
|
62
|
+
"skills",
|
|
63
|
+
"openclaw.plugin.json",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"README.md",
|
|
66
|
+
"README_ZH.md"
|
|
67
|
+
],
|
|
68
|
+
"openclaw": {
|
|
69
|
+
"extensions": [
|
|
70
|
+
"./dist/index.js"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
detectAgentsContract,
|
|
6
|
+
detectSoulContract,
|
|
7
|
+
normalizeRootName,
|
|
8
|
+
resolveCanonicalRootPath,
|
|
9
|
+
} from './workspace-contract.mjs';
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const options = {
|
|
13
|
+
workspace: path.resolve(process.cwd()),
|
|
14
|
+
canonicalRootName: 'memory',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
+
const arg = argv[i];
|
|
19
|
+
if (arg === '--workspace') {
|
|
20
|
+
options.workspace = path.resolve(argv[++i] || '');
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === '--canonical-root-name') {
|
|
24
|
+
options.canonicalRootName = normalizeRootName(argv[++i] || '');
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (arg === '--help' || arg === '-h') {
|
|
28
|
+
printHelp();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printHelp() {
|
|
38
|
+
console.log([
|
|
39
|
+
'Usage: openclaw-timeline-doctor [--workspace <dir>] [--canonical-root-name <name>]',
|
|
40
|
+
'',
|
|
41
|
+
'Checks whether the required Timeline workspace contracts are present.',
|
|
42
|
+
].join('\n'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readText(filePath) {
|
|
46
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function check(label, passed, successDetail, failureDetail) {
|
|
50
|
+
const prefix = passed ? '[ok]' : '[missing]';
|
|
51
|
+
console.log(`${prefix} ${label}: ${passed ? successDetail : failureDetail}`);
|
|
52
|
+
return passed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function main() {
|
|
56
|
+
const options = parseArgs(process.argv.slice(2));
|
|
57
|
+
const agentsPath = path.join(options.workspace, 'AGENTS.md');
|
|
58
|
+
const soulPath = path.join(options.workspace, 'SOUL.md');
|
|
59
|
+
const canonicalRootPath = resolveCanonicalRootPath(options.workspace, options.canonicalRootName);
|
|
60
|
+
|
|
61
|
+
const agentsContent = readText(agentsPath);
|
|
62
|
+
const soulContent = readText(soulPath);
|
|
63
|
+
|
|
64
|
+
let ok = true;
|
|
65
|
+
ok = check(
|
|
66
|
+
'AGENTS contract',
|
|
67
|
+
fs.existsSync(agentsPath) && detectAgentsContract(agentsContent),
|
|
68
|
+
agentsPath,
|
|
69
|
+
`${agentsPath} is missing the Timeline daily-log contract`,
|
|
70
|
+
) && ok;
|
|
71
|
+
ok = check(
|
|
72
|
+
'SOUL contract',
|
|
73
|
+
fs.existsSync(soulPath) && detectSoulContract(soulContent),
|
|
74
|
+
soulPath,
|
|
75
|
+
`${soulPath} is missing the Timeline recall contract`,
|
|
76
|
+
) && ok;
|
|
77
|
+
ok = check(
|
|
78
|
+
'Canonical memory root',
|
|
79
|
+
fs.existsSync(canonicalRootPath),
|
|
80
|
+
canonicalRootPath,
|
|
81
|
+
`${canonicalRootPath} does not exist`,
|
|
82
|
+
) && ok;
|
|
83
|
+
|
|
84
|
+
if (!ok) {
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
main();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
@@ -0,0 +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();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
10
|
+
const npmrcPath = path.join(repoRoot, '.npmrc');
|
|
11
|
+
|
|
12
|
+
function readDotEnv() {
|
|
13
|
+
const envPath = path.join(repoRoot, '.env');
|
|
14
|
+
if (!fs.existsSync(envPath)) return {};
|
|
15
|
+
|
|
16
|
+
const env = {};
|
|
17
|
+
for (const rawLine of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
18
|
+
const line = rawLine.trim();
|
|
19
|
+
if (!line || line.startsWith('#')) continue;
|
|
20
|
+
|
|
21
|
+
const separatorIndex = line.indexOf('=');
|
|
22
|
+
if (separatorIndex === -1) continue;
|
|
23
|
+
|
|
24
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
25
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
26
|
+
if (
|
|
27
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
28
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
29
|
+
) {
|
|
30
|
+
value = value.slice(1, -1);
|
|
31
|
+
}
|
|
32
|
+
if (key) {
|
|
33
|
+
env[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return env;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const dotEnv = readDotEnv();
|
|
41
|
+
|
|
42
|
+
function npmCommand() {
|
|
43
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run(command, args) {
|
|
47
|
+
const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
48
|
+
const result = spawnSync(command, args, {
|
|
49
|
+
cwd: repoRoot,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
shell: useShell,
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
...dotEnv,
|
|
55
|
+
NODE_AUTH_TOKEN: process.env.NODE_AUTH_TOKEN || dotEnv.NODE_AUTH_TOKEN || dotEnv.NPM_TOKEN,
|
|
56
|
+
NPM_TOKEN: process.env.NPM_TOKEN || dotEnv.NPM_TOKEN || dotEnv.NODE_AUTH_TOKEN,
|
|
57
|
+
NPM_CONFIG_USERCONFIG: npmrcPath,
|
|
58
|
+
npm_config_cache: path.join(runtimeTempDir, 'stella-timeline-plugin-npm-cache'),
|
|
59
|
+
TMPDIR: runtimeTempDir,
|
|
60
|
+
TMP: runtimeTempDir,
|
|
61
|
+
TEMP: runtimeTempDir,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printHelp() {
|
|
71
|
+
console.log([
|
|
72
|
+
'Usage: npm run release -- [npm publish args]',
|
|
73
|
+
'',
|
|
74
|
+
'Runs verify, then publishes the current package to npm.',
|
|
75
|
+
'Reads NPM_TOKEN/NODE_AUTH_TOKEN from the project .env when present.',
|
|
76
|
+
'Examples:',
|
|
77
|
+
' npm run release',
|
|
78
|
+
' npm run release -- --tag next',
|
|
79
|
+
' npm run release -- --dry-run',
|
|
80
|
+
].join('\n'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const publishArgs = process.argv.slice(2);
|
|
85
|
+
if (publishArgs.includes('--help') || publishArgs.includes('-h')) {
|
|
86
|
+
printHelp();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
run(npmCommand(), ['run', 'verify']);
|
|
91
|
+
run(npmCommand(), ['publish', ...publishArgs]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
main();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
@@ -0,0 +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
|
+
'无法定位 openclaw 可执行文件。',
|
|
43
|
+
'请先确认 openclaw 已安装,或在执行前显式设置 OPENCLAW_BIN。',
|
|
44
|
+
'例如: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);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
5
|
+
|
|
6
|
+
const result = spawnSync(
|
|
7
|
+
process.execPath,
|
|
8
|
+
['node_modules/jest/bin/jest.js', '--runInBand', '--runTestsByPath', 'src/integration/openclaw-runtime.smoke.test.ts'],
|
|
9
|
+
{
|
|
10
|
+
stdio: 'inherit',
|
|
11
|
+
env: {
|
|
12
|
+
...process.env,
|
|
13
|
+
TMPDIR: runtimeTempDir,
|
|
14
|
+
TMP: runtimeTempDir,
|
|
15
|
+
TEMP: runtimeTempDir,
|
|
16
|
+
OPENCLAW_RUNTIME_SMOKE: '1',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
buildAgentsContract,
|
|
6
|
+
buildSoulContract,
|
|
7
|
+
detectAgentsContract,
|
|
8
|
+
detectSoulContract,
|
|
9
|
+
normalizeRootName,
|
|
10
|
+
resolveCanonicalRootPath,
|
|
11
|
+
} from './workspace-contract.mjs';
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const options = {
|
|
15
|
+
workspace: path.resolve(process.cwd()),
|
|
16
|
+
canonicalRootName: 'memory',
|
|
17
|
+
createMemoryRoot: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg === '--workspace') {
|
|
23
|
+
options.workspace = path.resolve(argv[++i] || '');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === '--canonical-root-name') {
|
|
27
|
+
options.canonicalRootName = normalizeRootName(argv[++i] || '');
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--no-create-memory-root') {
|
|
31
|
+
options.createMemoryRoot = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--help' || arg === '-h') {
|
|
35
|
+
printHelp();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printHelp() {
|
|
45
|
+
console.log([
|
|
46
|
+
'Usage: openclaw-timeline-setup [--workspace <dir>] [--canonical-root-name <name>] [--no-create-memory-root]',
|
|
47
|
+
'',
|
|
48
|
+
'Idempotently appends the required Timeline contract blocks to AGENTS.md and SOUL.md.',
|
|
49
|
+
].join('\n'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readTextFile(filePath) {
|
|
53
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureTrailingNewline(content) {
|
|
57
|
+
if (!content) return '';
|
|
58
|
+
return content.endsWith('\n') ? content : `${content}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeSection(existingContent, sectionContent, predicate) {
|
|
62
|
+
if (predicate(existingContent)) {
|
|
63
|
+
return { changed: false, content: ensureTrailingNewline(existingContent) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prefix = existingContent.trimEnd();
|
|
67
|
+
const merged = prefix
|
|
68
|
+
? `${prefix}\n\n${sectionContent}\n`
|
|
69
|
+
: `${sectionContent}\n`;
|
|
70
|
+
return { changed: true, content: merged };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeFile(filePath, content) {
|
|
74
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main() {
|
|
79
|
+
const options = parseArgs(process.argv.slice(2));
|
|
80
|
+
const agentsPath = path.join(options.workspace, 'AGENTS.md');
|
|
81
|
+
const soulPath = path.join(options.workspace, 'SOUL.md');
|
|
82
|
+
const canonicalRootPath = resolveCanonicalRootPath(options.workspace, options.canonicalRootName);
|
|
83
|
+
|
|
84
|
+
const agentsContent = readTextFile(agentsPath);
|
|
85
|
+
const soulContent = readTextFile(soulPath);
|
|
86
|
+
|
|
87
|
+
const agentsResult = mergeSection(
|
|
88
|
+
agentsContent,
|
|
89
|
+
buildAgentsContract(),
|
|
90
|
+
detectAgentsContract,
|
|
91
|
+
);
|
|
92
|
+
const soulResult = mergeSection(
|
|
93
|
+
soulContent,
|
|
94
|
+
buildSoulContract(),
|
|
95
|
+
detectSoulContract,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
writeFile(agentsPath, agentsResult.content);
|
|
99
|
+
writeFile(soulPath, soulResult.content);
|
|
100
|
+
|
|
101
|
+
if (options.createMemoryRoot) {
|
|
102
|
+
fs.mkdirSync(canonicalRootPath, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const updates = [
|
|
106
|
+
`${agentsResult.changed ? 'updated' : 'kept'} ${agentsPath}`,
|
|
107
|
+
`${soulResult.changed ? 'updated' : 'kept'} ${soulPath}`,
|
|
108
|
+
`${options.createMemoryRoot ? 'ensured' : 'skipped'} ${canonicalRootPath}`,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
console.log(updates.join('\n'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
main();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|