stella-timeline-plugin 2.0.1 → 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 +54 -54
- package/dist/src/tools/timeline_resolve.js +1 -1
- package/package.json +1 -1
- package/scripts/doctor-openclaw-workspace.mjs +61 -2
- package/scripts/run-openclaw-live-e2e.mjs +3 -3
- package/scripts/setup-openclaw-workspace.mjs +79 -2
- package/skills/timeline/SKILL.md +54 -54
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.timelinePluginEntry = (0, openclaw_sdk_compat_1.definePluginEntry)({
|
|
|
9
9
|
name: plugin_metadata_1.TIMELINE_PLUGIN_NAME,
|
|
10
10
|
description: plugin_metadata_1.TIMELINE_PLUGIN_DESCRIPTION,
|
|
11
11
|
register(api) {
|
|
12
|
-
api.registerTool((0, openclaw_sdk_compat_1.makeTimelineToolRegistration)()
|
|
12
|
+
api.registerTool((0, openclaw_sdk_compat_1.makeTimelineToolRegistration)());
|
|
13
13
|
},
|
|
14
14
|
});
|
|
15
15
|
exports.timelinePlugin = (0, openclaw_sdk_compat_1.materializePlugin)(exports.timelinePluginEntry);
|
|
@@ -20,7 +20,7 @@ const openClawTimelinePlugin = {
|
|
|
20
20
|
description: plugin_metadata_1.TIMELINE_PLUGIN_DESCRIPTION,
|
|
21
21
|
register(api) {
|
|
22
22
|
const runtimeApi = api;
|
|
23
|
-
api.registerTool((0, openclaw_timeline_runtime_1.makeOpenClawTimelineResolveToolFactory)(runtimeApi), {
|
|
23
|
+
api.registerTool((0, openclaw_timeline_runtime_1.makeOpenClawTimelineResolveToolFactory)(runtimeApi), { names: [...plugin_metadata_1.TIMELINE_TOOL_NAMES] });
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
exports.default = openClawTimelinePlugin;
|
|
@@ -317,35 +317,35 @@ function makePlannerRequestId() {
|
|
|
317
317
|
}
|
|
318
318
|
function buildTimelineQueryPlannerSystemPrompt() {
|
|
319
319
|
return [
|
|
320
|
-
'
|
|
321
|
-
'
|
|
322
|
-
'
|
|
323
|
-
'
|
|
324
|
-
'1.
|
|
325
|
-
'2.
|
|
326
|
-
'3. now
|
|
327
|
-
'4. past_point
|
|
328
|
-
'5. past_range
|
|
329
|
-
'6.
|
|
330
|
-
'7.
|
|
331
|
-
'8.
|
|
332
|
-
'9.
|
|
333
|
-
'10.
|
|
334
|
-
'11.
|
|
320
|
+
'You are the internal Timeline plugin query normalizer.',
|
|
321
|
+
'Your only task is to normalize a natural-language time request into a structured time plan that Timeline can execute.',
|
|
322
|
+
'Do not call tools. Do not output Markdown, explanations, or extra text. Output strict JSON only.',
|
|
323
|
+
'You must follow these constraints:',
|
|
324
|
+
'1. First classify the request as now, past_point, or past_range.',
|
|
325
|
+
'2. Do not classify mechanically from keywords. Interpret the actual time semantics in the user request.',
|
|
326
|
+
'3. For now, do not output normalized_point / normalized_start / normalized_end.',
|
|
327
|
+
'4. For past_point, normalized_point is required.',
|
|
328
|
+
'5. For past_range, normalized_start and normalized_end are required.',
|
|
329
|
+
'6. For colloquial ranges such as “最近”, normalize into concrete bounds using anchor.now.',
|
|
330
|
+
'7. For expressions such as “昨晚”, “今天”, or “昨天上午”, produce a realistic range that fits ordinary usage.',
|
|
331
|
+
'8. All output times must be ISO-like timestamps with timezone offsets.',
|
|
332
|
+
'9. Classify as past_point only when the user clearly points to a specific moment, for example “昨晚八点”, “昨天上午十点”, or “上周六晚上九点”.',
|
|
333
|
+
'10. If the user is asking about a duration or a whole period, it must be past_range, for example “昨晚在做什么”, “今天都忙了什么”, “最近有什么有趣的事吗”, or “这几天怎么样”.',
|
|
334
|
+
'11. “昨晚” by itself is not a point in time. It is an evening range. Only expressions with an explicit anchor such as “昨晚八点” are past_point.',
|
|
335
335
|
].join('\n');
|
|
336
336
|
}
|
|
337
337
|
function buildTimelineQueryPlannerMessage(input, anchor, requestId) {
|
|
338
338
|
return [
|
|
339
|
-
'
|
|
340
|
-
'
|
|
339
|
+
'Normalize time only from the information below.',
|
|
340
|
+
'Output a JSON object with the following shape:',
|
|
341
341
|
JSON.stringify({
|
|
342
342
|
schema_version: '1.0',
|
|
343
343
|
request_id: requestId,
|
|
344
344
|
target_time_range: 'now | past_point | past_range',
|
|
345
|
-
normalized_point: 'past_point
|
|
346
|
-
normalized_start: 'past_range
|
|
347
|
-
normalized_end: 'past_range
|
|
348
|
-
summary: '
|
|
345
|
+
normalized_point: 'required for past_point, omit otherwise',
|
|
346
|
+
normalized_start: 'required for past_range, omit otherwise',
|
|
347
|
+
normalized_end: 'required for past_range, omit otherwise',
|
|
348
|
+
summary: 'short summary of how you interpreted the user time semantics',
|
|
349
349
|
}, null, 2),
|
|
350
350
|
'',
|
|
351
351
|
'input:',
|
|
@@ -357,36 +357,36 @@ function buildTimelineQueryPlannerMessage(input, anchor, requestId) {
|
|
|
357
357
|
}
|
|
358
358
|
function buildTimelineReasonerSystemPrompt() {
|
|
359
359
|
return [
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
'
|
|
363
|
-
'
|
|
364
|
-
'1.
|
|
365
|
-
'2.
|
|
366
|
-
'3.
|
|
367
|
-
'4.
|
|
368
|
-
'5.
|
|
369
|
-
'6. continuity
|
|
370
|
-
'7. request_type
|
|
371
|
-
'8. continuity
|
|
372
|
-
'9. past_point
|
|
373
|
-
'10. past_range
|
|
374
|
-
'11.
|
|
375
|
-
'12.
|
|
376
|
-
'13. generated_fact
|
|
377
|
-
'14.
|
|
378
|
-
'15.
|
|
379
|
-
'16.
|
|
380
|
-
'17.
|
|
381
|
-
'18. MEMORY
|
|
382
|
-
'19.
|
|
383
|
-
'20.
|
|
384
|
-
'21.
|
|
385
|
-
'22.
|
|
386
|
-
'23.
|
|
387
|
-
'24.
|
|
388
|
-
'25.
|
|
389
|
-
'26.
|
|
360
|
+
'You are the internal Timeline plugin time-semantics reasoner.',
|
|
361
|
+
'Your only task is to use the collector fact bundle and output a JSON object that strictly matches TimelineReasonerOutput.',
|
|
362
|
+
'Do not call tools. Do not introduce pre-existing facts beyond the collector input. Do not output Markdown, explanations, or extra text.',
|
|
363
|
+
'You must follow these constraints:',
|
|
364
|
+
'1. Session hard facts and existing canon facts take priority over generation.',
|
|
365
|
+
'2. If collector.request.mode is read_only, you must never generate_new_fact.',
|
|
366
|
+
'3. If decision.action is reuse_existing_fact, selected_fact_id must come from candidate_facts.',
|
|
367
|
+
'4. If decision.action is generate_new_fact, you must provide a complete generated_fact and set should_write_canon=true.',
|
|
368
|
+
'5. Use return_empty only when the available information is insufficient and neither reuse nor generation is justified.',
|
|
369
|
+
'6. The continuity field must truthfully report whether continuity reasoning was used and why.',
|
|
370
|
+
'7. request_type must be one of now, past_point, or past_range.',
|
|
371
|
+
'8. continuity is not a separate request type. It is a reasoning result inside now or past_point queries.',
|
|
372
|
+
'9. past_point may hit either by exact match or by a prior fact that naturally continues to the target time.',
|
|
373
|
+
'10. For past_range, first understand the normalized range, then choose the most relevant, vivid, and worth-mentioning facts from that range.',
|
|
374
|
+
'11. If the user asks with semantic filters such as “有趣”, “好玩”, or “忙不忙”, interpret that filter first before deciding which fact to reuse or generate.',
|
|
375
|
+
'12. When generating a new fact for past_point or past_range, provide a reasonable timestamp that lands inside the target point or range instead of defaulting to the current moment.',
|
|
376
|
+
'13. generated_fact must contain structured fields only, with no free-form prose, explanation, or extra narration.',
|
|
377
|
+
'14. If collector.persona_context.should_constrain_generation=true, the new fact must explicitly respect the stable persona, tone, interests, habits, and long-term commitments described in SOUL / MEMORY / IDENTITY, and must not conflict with them.',
|
|
378
|
+
'15. When persona_context contains concrete persona signals, rationale.persona_basis must be non-empty and must name the specific persona signals used.',
|
|
379
|
+
'16. When persona_context contains concrete persona signals, rationale.constraint_basis must be non-empty and must name the long-term constraints that limited generation.',
|
|
380
|
+
'17. Do not generate generic, template-like daily scenes that could fit anyone. Let location, action, emotion, appearance, and internalMonologue reflect this persona\'s lived continuity.',
|
|
381
|
+
'18. Long-term preferences, relationships, life rhythm, and standing commitments from MEMORY are important constraints when weaving time memories. They are not time facts themselves, but they constrain what generation is believable.',
|
|
382
|
+
'19. You must also respect the real-world temporal logic in collector.world_context: meals, sleep, work/study, leisure, weekends, weekdays, and holidays should broadly fit ordinary life rhythm.',
|
|
383
|
+
'20. For late-night or pre-dawn generation, prefer sleep, rest, or quiet activities. For breakfast, lunch, or dinner scenes, keep the timestamp inside a plausible meal window. Do not generate scenes that obviously violate ordinary routine.',
|
|
384
|
+
'21. If decision.action is generate_new_fact, generated_fact.sceneSemantics must be fully populated to explain the activity type, how it relates to known same-day state, and why that judgment fits.',
|
|
385
|
+
'22. If decision.action is generate_new_fact, generated_fact.appearanceLogic must be fully populated to explain whether the day\'s outfit continues, whether a change is needed, why, and what outfit class results.',
|
|
386
|
+
'23. Appearance and clothing must depend on the concrete event itself rather than being generated independently. Exercise, bathing, sleep, formal outings, and buying/changing into new clothes are all strong appearance drivers.',
|
|
387
|
+
'24. If there is not enough reason for an outfit change, prefer same-day clothing continuity. Do not change appearance descriptions repeatedly within one day without cause.',
|
|
388
|
+
'25. For now queries, if collector.conversation_context.should_prefer_conversation_continuity_for_now=true, then “still continuing the just-active conversation with the user” should be treated as the highest-priority near-field reality.',
|
|
389
|
+
'26. If the current session is still inside the stickiness window, prefer interpreting the current state as continuing the recent topic, thinking about the last turn, or preparing a response, instead of jumping immediately to an unrelated off-thread life scene.',
|
|
390
390
|
].join('\n');
|
|
391
391
|
}
|
|
392
392
|
function setClock(parts, hour, minute = 0, second = 0) {
|
|
@@ -682,8 +682,8 @@ function createTimelineQueryPlanner(pluginApi, toolContext, runtimeConfig) {
|
|
|
682
682
|
}
|
|
683
683
|
function buildTimelineReasonerMessage(collector) {
|
|
684
684
|
return [
|
|
685
|
-
'
|
|
686
|
-
'
|
|
685
|
+
'Perform structured time reasoning using only the collector JSON below.',
|
|
686
|
+
'Output a JSON object matching TimelineReasonerOutput:',
|
|
687
687
|
JSON.stringify({
|
|
688
688
|
schema_version: '1.0',
|
|
689
689
|
request_id: collector.request_id,
|
|
@@ -698,7 +698,7 @@ function buildTimelineReasonerMessage(collector) {
|
|
|
698
698
|
},
|
|
699
699
|
decision: {
|
|
700
700
|
action: 'reuse_existing_fact | generate_new_fact | return_empty',
|
|
701
|
-
selected_fact_id: 'reuse_existing_fact
|
|
701
|
+
selected_fact_id: 'required when action is reuse_existing_fact',
|
|
702
702
|
should_write_canon: true,
|
|
703
703
|
},
|
|
704
704
|
continuity: {
|
|
@@ -684,7 +684,7 @@ async function timelineResolve(input, dependencyOverrides) {
|
|
|
684
684
|
}
|
|
685
685
|
exports.timelineResolveToolSpec = {
|
|
686
686
|
name: 'timeline_resolve',
|
|
687
|
-
description: '
|
|
687
|
+
description: 'Unified entry point for time-grounded reality and recall questions such as “你在干嘛”, “你现在在哪”, “最近有什么有趣的事吗”, or “昨晚八点你在做什么”. It accepts a natural-language query, interprets the time semantics internally, then retrieves or generates and append-only writes canon facts.',
|
|
688
688
|
inputSchema: {
|
|
689
689
|
type: 'object',
|
|
690
690
|
properties: {
|
package/package.json
CHANGED
|
@@ -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
|
detectAgentsContract,
|
|
@@ -8,10 +9,15 @@ import {
|
|
|
8
9
|
resolveCanonicalRootPath,
|
|
9
10
|
} from './workspace-contract.mjs';
|
|
10
11
|
|
|
12
|
+
const PLUGIN_ID = 'stella-timeline-plugin';
|
|
13
|
+
const DEFAULT_OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
14
|
+
|
|
11
15
|
function parseArgs(argv) {
|
|
12
16
|
const options = {
|
|
13
17
|
workspace: path.resolve(process.cwd()),
|
|
14
18
|
canonicalRootName: 'memory',
|
|
19
|
+
openclawConfig: DEFAULT_OPENCLAW_CONFIG,
|
|
20
|
+
skipOpenclawConfig: false,
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -24,6 +30,14 @@ function parseArgs(argv) {
|
|
|
24
30
|
options.canonicalRootName = normalizeRootName(argv[++i] || '');
|
|
25
31
|
continue;
|
|
26
32
|
}
|
|
33
|
+
if (arg === '--openclaw-config') {
|
|
34
|
+
options.openclawConfig = path.resolve(argv[++i] || '');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (arg === '--skip-openclaw-config') {
|
|
38
|
+
options.skipOpenclawConfig = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
27
41
|
if (arg === '--help' || arg === '-h') {
|
|
28
42
|
printHelp();
|
|
29
43
|
process.exit(0);
|
|
@@ -36,9 +50,15 @@ function parseArgs(argv) {
|
|
|
36
50
|
|
|
37
51
|
function printHelp() {
|
|
38
52
|
console.log([
|
|
39
|
-
'Usage: openclaw-timeline-doctor [
|
|
53
|
+
'Usage: openclaw-timeline-doctor [options]',
|
|
54
|
+
'',
|
|
55
|
+
'Checks whether the required Timeline workspace contracts and openclaw.json config are present.',
|
|
40
56
|
'',
|
|
41
|
-
'
|
|
57
|
+
'Options:',
|
|
58
|
+
' --workspace <dir> Workspace to check AGENTS.md / SOUL.md (default: cwd)',
|
|
59
|
+
' --canonical-root-name <n> Memory root folder name (default: memory)',
|
|
60
|
+
' --openclaw-config <path> Path to openclaw.json (default: ~/.openclaw/openclaw.json)',
|
|
61
|
+
' --skip-openclaw-config Skip checking openclaw.json',
|
|
42
62
|
].join('\n'));
|
|
43
63
|
}
|
|
44
64
|
|
|
@@ -52,6 +72,15 @@ function check(label, passed, successDetail, failureDetail) {
|
|
|
52
72
|
return passed;
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
function readOpenclawConfig(configPath) {
|
|
76
|
+
if (!fs.existsSync(configPath)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
function main() {
|
|
56
85
|
const options = parseArgs(process.argv.slice(2));
|
|
57
86
|
const agentsPath = path.join(options.workspace, 'AGENTS.md');
|
|
@@ -81,6 +110,36 @@ function main() {
|
|
|
81
110
|
`${canonicalRootPath} does not exist`,
|
|
82
111
|
) && ok;
|
|
83
112
|
|
|
113
|
+
if (!options.skipOpenclawConfig) {
|
|
114
|
+
const config = readOpenclawConfig(options.openclawConfig);
|
|
115
|
+
if (!config) {
|
|
116
|
+
ok = check('openclaw.json', false, '', `${options.openclawConfig} not found or not valid JSON`) && ok;
|
|
117
|
+
} else {
|
|
118
|
+
const pluginAllowed = Array.isArray(config.plugins?.allow) && config.plugins.allow.includes(PLUGIN_ID);
|
|
119
|
+
const pluginEnabled = config.plugins?.entries?.[PLUGIN_ID]?.enabled === true;
|
|
120
|
+
const pluginPathSet = Array.isArray(config.plugins?.load?.paths) && config.plugins.load.paths.length > 0;
|
|
121
|
+
|
|
122
|
+
ok = check(
|
|
123
|
+
`plugins.allow contains "${PLUGIN_ID}"`,
|
|
124
|
+
pluginAllowed,
|
|
125
|
+
options.openclawConfig,
|
|
126
|
+
`add "${PLUGIN_ID}" to plugins.allow in ${options.openclawConfig}`,
|
|
127
|
+
) && ok;
|
|
128
|
+
ok = check(
|
|
129
|
+
`plugins.entries.${PLUGIN_ID}.enabled`,
|
|
130
|
+
pluginEnabled,
|
|
131
|
+
options.openclawConfig,
|
|
132
|
+
`set plugins.entries.${PLUGIN_ID}.enabled=true in ${options.openclawConfig}`,
|
|
133
|
+
) && ok;
|
|
134
|
+
ok = check(
|
|
135
|
+
'plugins.load.paths is set',
|
|
136
|
+
pluginPathSet,
|
|
137
|
+
options.openclawConfig,
|
|
138
|
+
`add plugin directory to plugins.load.paths in ${options.openclawConfig}`,
|
|
139
|
+
) && ok;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
84
143
|
if (!ok) {
|
|
85
144
|
process.exitCode = 1;
|
|
86
145
|
}
|
|
@@ -39,9 +39,9 @@ function resolveOpenClawBin() {
|
|
|
39
39
|
|
|
40
40
|
throw new Error(
|
|
41
41
|
[
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
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
45
|
].join('\n'),
|
|
46
46
|
);
|
|
47
47
|
}
|
|
@@ -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,95 +1,95 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: timeline
|
|
3
|
-
description:
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Timeline
|
|
7
7
|
|
|
8
|
-
|
|
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
9
|
|
|
10
|
-
##
|
|
10
|
+
## Scope
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
This skill does only two things:
|
|
13
13
|
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
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
16
|
|
|
17
|
-
|
|
18
|
-
|
|
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
19
|
|
|
20
|
-
##
|
|
20
|
+
## Hard Rules
|
|
21
21
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3. `SOUL` / `IDENTITY` / `MEMORY`
|
|
25
|
-
4.
|
|
26
|
-
5.
|
|
27
|
-
6. `timeline_resolve`
|
|
28
|
-
7.
|
|
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
29
|
|
|
30
|
-
##
|
|
30
|
+
## Call Shape
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Default path:
|
|
33
33
|
|
|
34
34
|
```json
|
|
35
35
|
{
|
|
36
|
-
"query": "
|
|
36
|
+
"query": "the user's exact words"
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
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
41
|
|
|
42
|
-
##
|
|
42
|
+
## Scenarios
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Scenario A: Current State
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Abstract shape:
|
|
47
47
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
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
50
|
|
|
51
|
-
|
|
51
|
+
Examples:
|
|
52
52
|
|
|
53
53
|
- “你在干嘛”
|
|
54
54
|
- “你现在在哪”
|
|
55
55
|
- “你现在什么状态”
|
|
56
56
|
- “你刚才不是在打球吗,现在还在吗”
|
|
57
|
-
-
|
|
57
|
+
- A downstream skill needs the current state, such as a selfie, scene description, or status card
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Handling:
|
|
60
60
|
|
|
61
|
-
-
|
|
62
|
-
- `query`
|
|
63
|
-
-
|
|
61
|
+
- Call `timeline_resolve`
|
|
62
|
+
- Set `query` to the user's exact wording
|
|
63
|
+
- Do not decide `now` yourself before calling
|
|
64
64
|
|
|
65
|
-
###
|
|
65
|
+
### Scenario B: A Specific Past Time
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Abstract shape:
|
|
68
68
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
69
|
+
- The user is asking about a sufficiently specific point in time.
|
|
70
|
+
- The goal is to recall facts around that time anchor.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Examples:
|
|
73
73
|
|
|
74
74
|
- “昨晚八点你在做什么”
|
|
75
75
|
- “你昨天上午十点在哪”
|
|
76
76
|
- “你昨晚八点是不是还在看电视”
|
|
77
77
|
- “上周六晚上九点你在忙什么”
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Handling:
|
|
80
80
|
|
|
81
|
-
-
|
|
82
|
-
- `query`
|
|
83
|
-
-
|
|
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
84
|
|
|
85
|
-
###
|
|
85
|
+
### Scenario C: A Past Range
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Abstract shape:
|
|
88
88
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
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
91
|
|
|
92
|
-
|
|
92
|
+
Examples:
|
|
93
93
|
|
|
94
94
|
- “最近有什么有趣的事吗”
|
|
95
95
|
- “你最近都在忙什么”
|
|
@@ -97,15 +97,15 @@ description: 当问题在询问当前状态、过去某个具体时间点、过
|
|
|
97
97
|
- “你今天都忙了什么”
|
|
98
98
|
- “昨晚在做什么”
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Handling:
|
|
101
101
|
|
|
102
|
-
-
|
|
103
|
-
- `query`
|
|
104
|
-
-
|
|
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
105
|
|
|
106
|
-
##
|
|
106
|
+
## Reply Requirements
|
|
107
107
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
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.
|