stella-timeline-plugin 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/src/runtime/openclaw_timeline_runtime.js +429 -146
- package/dist/src/tools/timeline_resolve.js +9 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +35 -35
- package/scripts/doctor-openclaw-workspace.mjs +61 -2
- package/scripts/migrate-existing-memory.mjs +153 -153
- package/scripts/release.mjs +2 -46
- package/scripts/run-openclaw-live-e2e.mjs +16 -16
- package/scripts/run-openclaw-smoke.mjs +285 -10
- package/scripts/setup-openclaw-workspace.mjs +79 -2
- package/skills/timeline/SKILL.md +111 -111
- package/templates/AGENTS.fragment.md +29 -29
- package/templates/SOUL.fragment.md +17 -17
|
@@ -1,21 +1,296 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
|
|
7
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
4
8
|
const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
function runCapture(command) {
|
|
11
|
+
try {
|
|
12
|
+
return execSync(command, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
TMPDIR: runtimeTempDir,
|
|
18
|
+
TMP: runtimeTempDir,
|
|
19
|
+
TEMP: runtimeTempDir,
|
|
20
|
+
},
|
|
21
|
+
}).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveOnPath(binaryName) {
|
|
28
|
+
const command = process.platform === 'win32' ? 'where' : 'which';
|
|
29
|
+
const result = spawnSync(command, [binaryName], {
|
|
30
|
+
encoding: 'utf8',
|
|
11
31
|
env: {
|
|
12
32
|
...process.env,
|
|
13
33
|
TMPDIR: runtimeTempDir,
|
|
14
34
|
TMP: runtimeTempDir,
|
|
15
35
|
TEMP: runtimeTempDir,
|
|
16
|
-
OPENCLAW_RUNTIME_SMOKE: '1',
|
|
17
36
|
},
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) return '';
|
|
39
|
+
return result.stdout
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.find(Boolean) || '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listJsFiles(dirPath) {
|
|
46
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
47
|
+
const files = [];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
files.push(...listJsFiles(fullPath));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
55
|
+
files.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function candidateDistDirs() {
|
|
62
|
+
const dirs = new Set();
|
|
63
|
+
const explicit = process.env.OPENCLAW_RUNTIME_MODULE?.trim();
|
|
64
|
+
if (explicit && fs.existsSync(explicit)) {
|
|
65
|
+
dirs.add(path.dirname(explicit));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const npmRoot = runCapture(process.platform === 'win32' ? 'npm.cmd root -g' : 'npm root -g');
|
|
69
|
+
if (npmRoot) {
|
|
70
|
+
dirs.add(path.join(npmRoot, 'openclaw', 'dist'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const npmPrefix = runCapture(process.platform === 'win32' ? 'npm.cmd prefix -g' : 'npm prefix -g');
|
|
74
|
+
if (npmPrefix) {
|
|
75
|
+
dirs.add(path.join(npmPrefix, 'node_modules', 'openclaw', 'dist'));
|
|
76
|
+
dirs.add(path.join(npmPrefix, 'lib', 'node_modules', 'openclaw', 'dist'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const openClawBin = resolveOnPath('openclaw');
|
|
80
|
+
if (openClawBin) {
|
|
81
|
+
const binDir = path.dirname(openClawBin);
|
|
82
|
+
dirs.add(path.join(binDir, 'node_modules', 'openclaw', 'dist'));
|
|
83
|
+
dirs.add(path.join(binDir, '..', 'node_modules', 'openclaw', 'dist'));
|
|
84
|
+
dirs.add(path.join(binDir, '..', 'lib', 'node_modules', 'openclaw', 'dist'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.platform === 'win32') {
|
|
88
|
+
const appData = process.env.APPDATA?.trim();
|
|
89
|
+
if (appData) {
|
|
90
|
+
dirs.add(path.join(appData, 'npm', 'node_modules', 'openclaw', 'dist'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [...dirs];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findRuntimeModuleInDist(distDir) {
|
|
98
|
+
if (!fs.existsSync(distDir)) return '';
|
|
99
|
+
const candidates = listJsFiles(distDir);
|
|
100
|
+
const aliasAware = candidates.find((filePath) => {
|
|
101
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
102
|
+
return content.includes('loadOpenClawPlugins as') && content.includes('resolvePluginTools as');
|
|
103
|
+
});
|
|
104
|
+
if (aliasAware) return aliasAware;
|
|
105
|
+
|
|
106
|
+
const direct = candidates.find((filePath) => {
|
|
107
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
108
|
+
return content.includes('function loadOpenClawPlugins') && content.includes('function resolvePluginTools');
|
|
109
|
+
});
|
|
110
|
+
if (direct) return direct;
|
|
111
|
+
|
|
112
|
+
return candidates.find((filePath) => /^reply-.*\.js$/.test(path.basename(filePath))) || '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findOpenClawRuntimeModule() {
|
|
116
|
+
const explicit = process.env.OPENCLAW_RUNTIME_MODULE?.trim();
|
|
117
|
+
if (explicit && fs.existsSync(explicit)) return explicit;
|
|
118
|
+
|
|
119
|
+
for (const distDir of candidateDistDirs()) {
|
|
120
|
+
const runtimeModule = findRuntimeModuleInDist(distDir);
|
|
121
|
+
if (runtimeModule) return runtimeModule;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nvmNodeDir = path.join(os.homedir(), '.nvm', 'versions', 'node');
|
|
125
|
+
if (fs.existsSync(nvmNodeDir)) {
|
|
126
|
+
for (const entry of fs.readdirSync(nvmNodeDir, { withFileTypes: true })) {
|
|
127
|
+
if (!entry.isDirectory()) continue;
|
|
128
|
+
const distDir = path.join(nvmNodeDir, entry.name, 'lib', 'node_modules', 'openclaw', 'dist');
|
|
129
|
+
const runtimeModule = findRuntimeModuleInDist(distDir);
|
|
130
|
+
if (runtimeModule) return runtimeModule;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findCompatibleNodeBin(runtimeModulePath) {
|
|
138
|
+
const explicit = process.env.OPENCLAW_NODE_BIN?.trim();
|
|
139
|
+
if (explicit && fs.existsSync(explicit)) return explicit;
|
|
140
|
+
|
|
141
|
+
const normalized = path.normalize(runtimeModulePath);
|
|
142
|
+
const unixMarker = `${path.sep}lib${path.sep}node_modules${path.sep}openclaw${path.sep}dist${path.sep}`;
|
|
143
|
+
const unixIndex = normalized.lastIndexOf(unixMarker);
|
|
144
|
+
if (unixIndex !== -1) {
|
|
145
|
+
const prefixDir = normalized.slice(0, unixIndex);
|
|
146
|
+
const unixNode = path.join(prefixDir, 'bin', 'node');
|
|
147
|
+
if (fs.existsSync(unixNode)) return unixNode;
|
|
148
|
+
const winNode = path.join(prefixDir, 'node.exe');
|
|
149
|
+
if (fs.existsSync(winNode)) return winNode;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return process.execPath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseLastJsonObject(raw) {
|
|
156
|
+
const jsonLine = raw
|
|
157
|
+
.split(/\r?\n/)
|
|
158
|
+
.map((line) => line.trim())
|
|
159
|
+
.filter((line) => line.startsWith('{') && line.endsWith('}'))
|
|
160
|
+
.slice(-1)[0];
|
|
161
|
+
if (!jsonLine) {
|
|
162
|
+
throw new Error(`OpenClaw smoke script did not emit a JSON payload.\n${raw}`);
|
|
163
|
+
}
|
|
164
|
+
return JSON.parse(jsonLine);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function copyRecursive(sourcePath, targetPath) {
|
|
168
|
+
const stat = fs.statSync(sourcePath);
|
|
169
|
+
if (stat.isDirectory()) {
|
|
170
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
171
|
+
for (const entry of fs.readdirSync(sourcePath)) {
|
|
172
|
+
copyRecursive(path.join(sourcePath, entry), path.join(targetPath, entry));
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
177
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setSafePermissions(targetPath) {
|
|
181
|
+
if (process.platform === 'win32' || !fs.existsSync(targetPath)) return;
|
|
182
|
+
const stat = fs.statSync(targetPath);
|
|
183
|
+
fs.chmodSync(targetPath, stat.isDirectory() ? 0o755 : 0o644);
|
|
184
|
+
if (stat.isDirectory()) {
|
|
185
|
+
for (const entry of fs.readdirSync(targetPath)) {
|
|
186
|
+
setSafePermissions(path.join(targetPath, entry));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stagePluginForSmoke() {
|
|
192
|
+
const stagedRoot = fs.mkdtempSync(path.join(runtimeTempDir, 'stella-timeline-plugin-smoke-'));
|
|
193
|
+
for (const entry of ['package.json', 'openclaw.plugin.json', 'dist']) {
|
|
194
|
+
const sourcePath = path.join(repoRoot, entry);
|
|
195
|
+
if (!fs.existsSync(sourcePath)) {
|
|
196
|
+
throw new Error(`OpenClaw smoke staging missing required path: ${sourcePath}`);
|
|
197
|
+
}
|
|
198
|
+
copyRecursive(sourcePath, path.join(stagedRoot, entry));
|
|
199
|
+
}
|
|
200
|
+
setSafePermissions(stagedRoot);
|
|
201
|
+
return stagedRoot;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const runtimeModule = findOpenClawRuntimeModule();
|
|
205
|
+
if (!runtimeModule) {
|
|
206
|
+
console.log('Skipping OpenClaw smoke: OpenClaw runtime not found on this machine.');
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const openClawNodeBin = findCompatibleNodeBin(runtimeModule);
|
|
211
|
+
const stagedPluginRoot = stagePluginForSmoke();
|
|
212
|
+
const script = `
|
|
213
|
+
import fs from 'node:fs';
|
|
214
|
+
import { pathToFileURL } from 'node:url';
|
|
215
|
+
|
|
216
|
+
const runtimeModulePath = ${JSON.stringify(runtimeModule)};
|
|
217
|
+
const repoRoot = ${JSON.stringify(repoRoot)};
|
|
218
|
+
const pluginRoot = ${JSON.stringify(stagedPluginRoot)};
|
|
219
|
+
const runtimeSource = fs.readFileSync(runtimeModulePath, 'utf8');
|
|
220
|
+
const alias = (symbolName) => runtimeSource.match(new RegExp('\\\\b' + symbolName + ' as ([\\\\w$]+)'))?.[1] || symbolName;
|
|
221
|
+
const runtime = await import(pathToFileURL(runtimeModulePath).href);
|
|
222
|
+
const loadOpenClawPlugins = runtime[alias('loadOpenClawPlugins')]
|
|
223
|
+
|| runtime.loadOpenClawPlugins
|
|
224
|
+
|| runtime.default?.[alias('loadOpenClawPlugins')]
|
|
225
|
+
|| runtime.default?.loadOpenClawPlugins;
|
|
226
|
+
const resolvePluginTools = runtime[alias('resolvePluginTools')]
|
|
227
|
+
|| runtime.resolvePluginTools
|
|
228
|
+
|| runtime.default?.[alias('resolvePluginTools')]
|
|
229
|
+
|| runtime.default?.resolvePluginTools;
|
|
230
|
+
|
|
231
|
+
if (typeof loadOpenClawPlugins !== 'function' || typeof resolvePluginTools !== 'function') {
|
|
232
|
+
throw new Error('Unable to resolve OpenClaw runtime exports from ' + runtimeModulePath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const config = {
|
|
236
|
+
plugins: {
|
|
237
|
+
allow: ['stella-timeline-plugin'],
|
|
238
|
+
load: { paths: [pluginRoot] },
|
|
239
|
+
entries: { 'stella-timeline-plugin': { enabled: true } },
|
|
240
|
+
},
|
|
241
|
+
tools: {
|
|
242
|
+
profile: 'coding',
|
|
243
|
+
alsoAllow: ['stella-timeline-plugin'],
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const registry = loadOpenClawPlugins({ config, workspaceDir: repoRoot });
|
|
248
|
+
const plugin = registry.plugins.find((entry) => entry.id === 'stella-timeline-plugin');
|
|
249
|
+
const resolvedTools = resolvePluginTools({
|
|
250
|
+
context: { config, workspaceDir: repoRoot, sandboxed: true },
|
|
251
|
+
existingToolNames: new Set(),
|
|
252
|
+
toolAllowlist: ['stella-timeline-plugin'],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log(JSON.stringify({
|
|
256
|
+
runtimeModulePath,
|
|
257
|
+
plugin: plugin ? {
|
|
258
|
+
status: plugin.status,
|
|
259
|
+
toolNames: plugin.toolNames,
|
|
260
|
+
} : null,
|
|
261
|
+
resolvedToolNames: resolvedTools.map((tool) => tool.name),
|
|
262
|
+
}));
|
|
263
|
+
`;
|
|
264
|
+
|
|
265
|
+
const result = spawnSync(openClawNodeBin, ['--input-type=module', '-e', script], {
|
|
266
|
+
cwd: repoRoot,
|
|
267
|
+
encoding: 'utf8',
|
|
268
|
+
env: {
|
|
269
|
+
...process.env,
|
|
270
|
+
TMPDIR: runtimeTempDir,
|
|
271
|
+
TMP: runtimeTempDir,
|
|
272
|
+
TEMP: runtimeTempDir,
|
|
18
273
|
},
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result.status !== 0) {
|
|
277
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
278
|
+
console.error(output || `OpenClaw smoke failed with exit code ${result.status}`);
|
|
279
|
+
process.exit(result.status ?? 1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const payload = parseLastJsonObject(result.stdout || '');
|
|
283
|
+
if (!payload.plugin) {
|
|
284
|
+
console.error(`OpenClaw smoke did not load stella-timeline-plugin.\n${result.stdout || ''}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
if (payload.plugin.status !== 'loaded') {
|
|
288
|
+
console.error(`OpenClaw smoke loaded stella-timeline-plugin with unexpected status: ${payload.plugin.status}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(payload.resolvedToolNames) || !payload.resolvedToolNames.includes('timeline_resolve')) {
|
|
292
|
+
console.error(`OpenClaw smoke did not resolve timeline_resolve into runtime tools.\n${JSON.stringify(payload)}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(`OpenClaw smoke passed using ${runtimeModule}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import {
|
|
5
6
|
buildAgentsContract,
|
|
@@ -10,11 +11,17 @@ import {
|
|
|
10
11
|
resolveCanonicalRootPath,
|
|
11
12
|
} from './workspace-contract.mjs';
|
|
12
13
|
|
|
14
|
+
const PLUGIN_ID = 'stella-timeline-plugin';
|
|
15
|
+
const DEFAULT_OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
16
|
+
|
|
13
17
|
function parseArgs(argv) {
|
|
14
18
|
const options = {
|
|
15
19
|
workspace: path.resolve(process.cwd()),
|
|
16
20
|
canonicalRootName: 'memory',
|
|
17
21
|
createMemoryRoot: true,
|
|
22
|
+
pluginDir: path.resolve(process.cwd()),
|
|
23
|
+
openclawConfig: DEFAULT_OPENCLAW_CONFIG,
|
|
24
|
+
skipOpenclawConfig: false,
|
|
18
25
|
};
|
|
19
26
|
|
|
20
27
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -31,6 +38,18 @@ function parseArgs(argv) {
|
|
|
31
38
|
options.createMemoryRoot = false;
|
|
32
39
|
continue;
|
|
33
40
|
}
|
|
41
|
+
if (arg === '--plugin-dir') {
|
|
42
|
+
options.pluginDir = path.resolve(argv[++i] || '');
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg === '--openclaw-config') {
|
|
46
|
+
options.openclawConfig = path.resolve(argv[++i] || '');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === '--skip-openclaw-config') {
|
|
50
|
+
options.skipOpenclawConfig = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
34
53
|
if (arg === '--help' || arg === '-h') {
|
|
35
54
|
printHelp();
|
|
36
55
|
process.exit(0);
|
|
@@ -43,9 +62,17 @@ function parseArgs(argv) {
|
|
|
43
62
|
|
|
44
63
|
function printHelp() {
|
|
45
64
|
console.log([
|
|
46
|
-
'Usage: openclaw-timeline-setup [
|
|
65
|
+
'Usage: openclaw-timeline-setup [options]',
|
|
47
66
|
'',
|
|
48
|
-
'Idempotently
|
|
67
|
+
'Idempotently sets up the Timeline plugin: patches AGENTS.md, SOUL.md, and openclaw.json.',
|
|
68
|
+
'',
|
|
69
|
+
'Options:',
|
|
70
|
+
' --workspace <dir> Workspace to patch AGENTS.md / SOUL.md (default: cwd)',
|
|
71
|
+
' --canonical-root-name <name> Memory root folder name (default: memory)',
|
|
72
|
+
' --no-create-memory-root Skip creating the memory root directory',
|
|
73
|
+
' --plugin-dir <dir> Plugin source directory to register in openclaw.json (default: cwd)',
|
|
74
|
+
' --openclaw-config <path> Path to openclaw.json (default: ~/.openclaw/openclaw.json)',
|
|
75
|
+
' --skip-openclaw-config Skip patching openclaw.json',
|
|
49
76
|
].join('\n'));
|
|
50
77
|
}
|
|
51
78
|
|
|
@@ -75,6 +102,47 @@ function writeFile(filePath, content) {
|
|
|
75
102
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
76
103
|
}
|
|
77
104
|
|
|
105
|
+
function mergeUniqueArray(existing, value) {
|
|
106
|
+
if (!Array.isArray(existing)) return [value];
|
|
107
|
+
return existing.includes(value) ? existing : [...existing, value];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function patchOpenclawConfig(configPath, pluginDir) {
|
|
111
|
+
if (!fs.existsSync(configPath)) {
|
|
112
|
+
return { changed: false, skipped: true, reason: `${configPath} not found` };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let config;
|
|
116
|
+
try {
|
|
117
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
118
|
+
} catch {
|
|
119
|
+
return { changed: false, skipped: true, reason: `${configPath} is not valid JSON` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const original = JSON.stringify(config);
|
|
123
|
+
|
|
124
|
+
// plugins.allow
|
|
125
|
+
config.plugins = config.plugins || {};
|
|
126
|
+
config.plugins.allow = mergeUniqueArray(config.plugins.allow, PLUGIN_ID);
|
|
127
|
+
|
|
128
|
+
// plugins.load.paths
|
|
129
|
+
config.plugins.load = config.plugins.load || {};
|
|
130
|
+
config.plugins.load.paths = mergeUniqueArray(config.plugins.load.paths, pluginDir);
|
|
131
|
+
|
|
132
|
+
// plugins.entries.<id>.enabled
|
|
133
|
+
config.plugins.entries = config.plugins.entries || {};
|
|
134
|
+
config.plugins.entries[PLUGIN_ID] = config.plugins.entries[PLUGIN_ID] || {};
|
|
135
|
+
config.plugins.entries[PLUGIN_ID].enabled = true;
|
|
136
|
+
|
|
137
|
+
const updated = JSON.stringify(config);
|
|
138
|
+
if (original === updated) {
|
|
139
|
+
return { changed: false, skipped: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
143
|
+
return { changed: true, skipped: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
78
146
|
function main() {
|
|
79
147
|
const options = parseArgs(process.argv.slice(2));
|
|
80
148
|
const agentsPath = path.join(options.workspace, 'AGENTS.md');
|
|
@@ -108,6 +176,15 @@ function main() {
|
|
|
108
176
|
`${options.createMemoryRoot ? 'ensured' : 'skipped'} ${canonicalRootPath}`,
|
|
109
177
|
];
|
|
110
178
|
|
|
179
|
+
if (!options.skipOpenclawConfig) {
|
|
180
|
+
const configResult = patchOpenclawConfig(options.openclawConfig, options.pluginDir);
|
|
181
|
+
if (configResult.skipped) {
|
|
182
|
+
updates.push(`skipped ${options.openclawConfig} (${configResult.reason})`);
|
|
183
|
+
} else {
|
|
184
|
+
updates.push(`${configResult.changed ? 'updated' : 'kept'} ${options.openclawConfig}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
111
188
|
console.log(updates.join('\n'));
|
|
112
189
|
}
|
|
113
190
|
|
package/skills/timeline/SKILL.md
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: timeline
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Timeline
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3. `SOUL` / `IDENTITY` / `MEMORY`
|
|
25
|
-
4.
|
|
26
|
-
5.
|
|
27
|
-
6. `timeline_resolve`
|
|
28
|
-
7.
|
|
29
|
-
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"query": "
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
###
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- “你在干嘛”
|
|
54
|
-
- “你现在在哪”
|
|
55
|
-
- “你现在什么状态”
|
|
56
|
-
- “你刚才不是在打球吗,现在还在吗”
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
- `query`
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
###
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- “昨晚八点你在做什么”
|
|
75
|
-
- “你昨天上午十点在哪”
|
|
76
|
-
- “你昨晚八点是不是还在看电视”
|
|
77
|
-
- “上周六晚上九点你在忙什么”
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
- `query`
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
###
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- “最近有什么有趣的事吗”
|
|
95
|
-
- “你最近都在忙什么”
|
|
96
|
-
- “这几天怎么样”
|
|
97
|
-
- “你今天都忙了什么”
|
|
98
|
-
- “昨晚在做什么”
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
- `query`
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
##
|
|
107
|
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
1
|
+
---
|
|
2
|
+
name: timeline
|
|
3
|
+
description: When a request asks about the current state, a specific past time, a past range, or a downstream skill needs time-state facts, enter the timeline skill first and pass the raw natural-language query to Timeline. Never answer from persona alone.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Timeline
|
|
7
|
+
|
|
8
|
+
When a request is about time-state facts or recall, call Timeline first and then compose the reply. Do not guess from persona first, and do not read `memory/` directly and make your own conclusion.
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
This skill does only two things:
|
|
13
|
+
|
|
14
|
+
1. Decide whether the request is a time-grounded reality question that Timeline should own.
|
|
15
|
+
2. Pass the user's exact wording, or the downstream skill's natural-language question, straight into `timeline_resolve`.
|
|
16
|
+
|
|
17
|
+
It does not generate facts directly, normalize time on its own, or bypass Timeline and answer directly.
|
|
18
|
+
When the target time lacks an existing fact, Timeline decides whether to generate and write.
|
|
19
|
+
|
|
20
|
+
## Hard Rules
|
|
21
|
+
|
|
22
|
+
1. For time-reality questions, call `timeline_resolve` first and answer second.
|
|
23
|
+
2. Do not answer "what are you doing now" or "what happened recently" from `SOUL.md`, `IDENTITY.md`, or `MEMORY.md` alone.
|
|
24
|
+
3. `SOUL` / `IDENTITY` / `MEMORY` help with interpretation and phrasing, but they cannot replace Timeline facts.
|
|
25
|
+
4. Do not rewrite the query into structured time parameters before the call.
|
|
26
|
+
5. Let Timeline own time classification and time normalization.
|
|
27
|
+
6. If `timeline_resolve` returns an existing fact, reuse it as-is.
|
|
28
|
+
7. If the user is doing ordinary conversation rather than strict read-only verification, Timeline may generate missing facts and write canon.
|
|
29
|
+
|
|
30
|
+
## Call Shape
|
|
31
|
+
|
|
32
|
+
Default path:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"query": "the user's exact words"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Do not pass time points, time ranges, request types, modes, or debug fields into `timeline_resolve`. Its public interface is the natural-language query.
|
|
41
|
+
|
|
42
|
+
## Scenarios
|
|
43
|
+
|
|
44
|
+
### Scenario A: Current State
|
|
45
|
+
|
|
46
|
+
Abstract shape:
|
|
47
|
+
|
|
48
|
+
- The user is asking about "right now" or the current short stretch of time.
|
|
49
|
+
- The goal is to answer where she is, what she is doing, or what state she is in.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
|
|
53
|
+
- “你在干嘛”
|
|
54
|
+
- “你现在在哪”
|
|
55
|
+
- “你现在什么状态”
|
|
56
|
+
- “你刚才不是在打球吗,现在还在吗”
|
|
57
|
+
- A downstream skill needs the current state, such as a selfie, scene description, or status card
|
|
58
|
+
|
|
59
|
+
Handling:
|
|
60
|
+
|
|
61
|
+
- Call `timeline_resolve`
|
|
62
|
+
- Set `query` to the user's exact wording
|
|
63
|
+
- Do not decide `now` yourself before calling
|
|
64
|
+
|
|
65
|
+
### Scenario B: A Specific Past Time
|
|
66
|
+
|
|
67
|
+
Abstract shape:
|
|
68
|
+
|
|
69
|
+
- The user is asking about a sufficiently specific point in time.
|
|
70
|
+
- The goal is to recall facts around that time anchor.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
|
|
74
|
+
- “昨晚八点你在做什么”
|
|
75
|
+
- “你昨天上午十点在哪”
|
|
76
|
+
- “你昨晚八点是不是还在看电视”
|
|
77
|
+
- “上周六晚上九点你在忙什么”
|
|
78
|
+
|
|
79
|
+
Handling:
|
|
80
|
+
|
|
81
|
+
- Call `timeline_resolve`
|
|
82
|
+
- Set `query` to the user's exact wording
|
|
83
|
+
- Do not normalize the time point yourself before passing it through
|
|
84
|
+
|
|
85
|
+
### Scenario C: A Past Range
|
|
86
|
+
|
|
87
|
+
Abstract shape:
|
|
88
|
+
|
|
89
|
+
- The user is asking about the overall state of a time range rather than a single point.
|
|
90
|
+
- The goal is to organize recall, an activity overview, or recent updates from that range.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
|
|
94
|
+
- “最近有什么有趣的事吗”
|
|
95
|
+
- “你最近都在忙什么”
|
|
96
|
+
- “这几天怎么样”
|
|
97
|
+
- “你今天都忙了什么”
|
|
98
|
+
- “昨晚在做什么”
|
|
99
|
+
|
|
100
|
+
Handling:
|
|
101
|
+
|
|
102
|
+
- Call `timeline_resolve`
|
|
103
|
+
- Set `query` to the user's exact wording
|
|
104
|
+
- Let Timeline's internal planner interpret natural-language ranges such as “最近”, “昨晚”, or “今天都”
|
|
105
|
+
|
|
106
|
+
## Reply Requirements
|
|
107
|
+
|
|
108
|
+
- Speak naturally to the user and do not mention `timeline_resolve`.
|
|
109
|
+
- Keep the tone human, like natural recall or present-moment description.
|
|
110
|
+
- If Timeline returns an empty window or a failure, do not pretend you have certain facts; phrase the answer cautiously from what is available.
|
|
111
|
+
- Do not output JSON unless the user explicitly asks for raw results.
|