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 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)(), { optional: true });
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), { optional: true });
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
- '你是 Timeline 插件内部的时间查询归一化器。',
321
- '你的唯一任务,是把自然语言时间请求归一化为 Timeline 内部可执行的结构化时间计划。',
322
- '禁止调用任何工具,禁止输出 Markdown、解释或多余文本,只输出严格 JSON',
323
- '必须遵守这些约束:',
324
- '1. 你必须先判断请求属于 nowpast_pointpast_range 中的哪一类。',
325
- '2. 不能靠关键词机械枚举,而要真正理解用户语言中的时间语义。',
326
- '3. now 不输出 normalized_point / normalized_start / normalized_end',
327
- '4. past_point 必须输出 normalized_point。',
328
- '5. past_range 必须输出 normalized_start normalized_end',
329
- '6. 对“最近”这类口语范围,要结合 anchor.now 归一化成具体起止时间。',
330
- '7. 对“昨晚”“今天”“昨天上午”这类表达,要给出符合现实习惯的合理时间范围。',
331
- '8. 输出时间必须是带时区偏移的 ISO-like 时间戳。',
332
- '9. 只有用户明确指向某个时刻时,才允许判为 past_point;例如“昨晚八点”“昨天上午十点”“上周六晚上九点”。',
333
- '10. 只要用户问的是一个时间段或一整个时段,就必须判为 past_range;例如“昨晚在做什么”“今天都忙了什么”“最近有什么有趣的事吗”“这几天怎么样”。',
334
- '11. “昨晚”本身不是时间点,而是一个晚间范围;只有“昨晚八点”这类带明确时点锚点的表达才是 past_point',
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
- '输出 JSON 对象,字段必须满足下列结构:',
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
- '你是 Timeline 插件内部的时间语义推理器。',
361
- '你的唯一任务,是基于 collector 提供的事实包,输出一个严格符合 TimelineReasonerOutput 结构的 JSON 对象。',
362
- '禁止调用任何工具,禁止引用 collector 之外的新既有事实,禁止输出 Markdown、解释或多余文本。',
363
- '必须遵守这些约束:',
364
- '1. 会话硬事实和已存在 canon 优先于生成。',
365
- '2. 如果 collector.request.mode read_only,则绝不能 generate_new_fact',
366
- '3. 如果 decision.action reuse_existing_factselected_fact_id 必须来自 candidate_facts',
367
- '4. 如果 decision.action generate_new_fact,必须给出完整 generated_fact,并且 should_write_canon=true',
368
- '5. 如果当前信息不足且不应复用或生成,才允许 return_empty',
369
- '6. continuity 字段必须如实表达是否做了延续性判断,以及判断理由。',
370
- '7. request_type 只能是 nowpast_pointpast_range',
371
- '8. continuity 不是独立请求类型;它只是 now past_point 查询中的推理结果。',
372
- '9. past_point 可以通过精确命中,或通过“较早事实自然持续到目标时间点”的方式命中。',
373
- '10. past_range 需要先理解已经归一化的时间范围,再从该范围内挑选最相关、最鲜活、最值得提的事实。',
374
- '11. 如果用户在问“有趣”“好玩”“忙不忙”这类语义筛选词,必须先理解筛选语义,再决定复用什么事实或生成什么事实。',
375
- '12. 如果为 past_point past_range 生成新事实,generated_fact 应尽量提供一个合理的 timestamp,并保证它落在目标时间点或目标时间范围内,而不是默认落在当前时刻。',
376
- '13. generated_fact 只输出结构化字段,不要输出自然正文、解释或额外叙述。',
377
- '14. 如果 collector.persona_context.should_constrain_generation=true,则生成的新事实必须显式参考 SOUL / MEMORY / IDENTITY 中的稳定人格、语气、兴趣、生活习惯或长期约定,不能生成与这些内容冲突的生活片段。',
378
- '15. persona_context 中存在明确人格线索时,rationale.persona_basis 不能为空,且必须指出本次生成具体参考了哪些 persona 线索。',
379
- '16. persona_context 中存在明确人格线索时,rationale.constraint_basis 不能为空,且必须指出哪些长期约束限制了生成结果。',
380
- '17. 不要生成通用、模板化、任何人格都能成立的空泛日常;应尽量让 locationactionemotionappearanceinternalMonologue 都体现该 persona 的生活连续性。',
381
- '18. MEMORY 中的长期偏好、关系、生活节奏和与用户的长期约定,都是编织时间记忆时的重要约束;它们不是时间事实本身,但会限制什么样的生成是可信的。',
382
- '19. 还必须遵守 collector.world_context 提供的现实时间逻辑:一日三餐、睡眠、工作/学习、休闲、周末、工作日、节假日的安排都应尽量符合普通现实生活节奏。',
383
- '20. 如果生成的是凌晨或深夜时段,优先考虑睡眠、休息、安静活动;如果生成的是早餐/午餐/晚餐,则时间应落在合理餐段;不要生成明显违背现实作息的片段。',
384
- '21. 如果 decision.action generate_new_factgenerated_fact.sceneSemantics 必须完整输出,用来说明本次编织的事件属于什么活动类型、与当天已知状态是什么连续关系,以及为什么这样判断。',
385
- '22. 如果 decision.action generate_new_factgenerated_fact.appearanceLogic 必须完整输出,用来说明这次事件是否延续当天穿着、是否需要换装、换装原因是什么、最终服装类型属于哪一类。',
386
- '23. 外貌与穿着必须依赖具体事件本身,而不是脱离事件单独生成;例如运动、洗澡、入睡、正式出门、买到并换上新衣物,都会显著影响 appearanceLogic。',
387
- '24. 如果没有足够理由触发换装,优先认为当天穿着具有连续性;不要无缘无故在同一天内频繁改变外貌描述。',
388
- '25. now 查询,如果 collector.conversation_context.should_prefer_conversation_continuity_for_now=true,则“刚刚还在和用户继续这段对话”应被视为最高优先级的近场现实。',
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
- '请只根据下面的 collector JSON 做结构化时间推理。',
686
- '输出一个 JSON 对象,字段必须满足 TimelineReasonerOutput',
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: '处理“你在干嘛”“你现在在哪”“最近有什么有趣的事吗”“昨晚八点你在做什么”这类时间现实与回忆问题的统一入口;直接接收自然语言 query,内部会先理解时间语义,再检索或生成并 append-only 写入 canon',
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,6 +1,6 @@
1
1
  {
2
2
  "name": "stella-timeline-plugin",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Native OpenClaw timeline plugin with a canonical timeline_resolve tool, bundled skill routing, and guarded append-only writes.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -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 [--workspace <dir>] [--canonical-root-name <name>]',
53
+ 'Usage: openclaw-timeline-doctor [options]',
54
+ '',
55
+ 'Checks whether the required Timeline workspace contracts and openclaw.json config are present.',
40
56
  '',
41
- 'Checks whether the required Timeline workspace contracts are present.',
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
- '无法定位 openclaw 可执行文件。',
43
- '请先确认 openclaw 已安装,或在执行前显式设置 OPENCLAW_BIN',
44
- '例如:OPENCLAW_BIN=/Users/zangtao/.nvm/versions/node/v24.9.0/bin/openclaw npm run test:live-experience',
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 [--workspace <dir>] [--canonical-root-name <name>] [--no-create-memory-root]',
65
+ 'Usage: openclaw-timeline-setup [options]',
47
66
  '',
48
- 'Idempotently appends the required Timeline contract blocks to AGENTS.md and SOUL.md.',
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
 
@@ -1,95 +1,95 @@
1
1
  ---
2
2
  name: timeline
3
- description: 当问题在询问当前状态、过去某个具体时间点、过去一段时间,或下游技能需要时间状态事实时,先进入 timeline skill,再把原始自然语言 query 交给 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
4
  ---
5
5
 
6
6
  # Timeline
7
7
 
8
- 遇到和时间状态或回忆有关的请求时,先调用 Timeline,再组织回答。不要先按人设猜,也不要先直接读 `memory/` 自己下结论。
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
- ## 这个 skill 负责什么
10
+ ## Scope
11
11
 
12
- 这个 skill 只负责两件事:
12
+ This skill does only two things:
13
13
 
14
- 1. 判断这次请求是否属于 Timeline 应接管的时间现实问题。
15
- 2. 把用户原话或下游技能的自然语言问题,原样交给 `timeline_resolve`。
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
- 它不直接生成事实,也不负责自己归一化时间,更不负责绕过 Timeline 直接回答。
18
- 当目标时间缺少既有事实时,生成与写入的决定交给 Timeline 内部处理。
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. 涉及时间现实的问题,先调用 `timeline_resolve`,后回答。
23
- 2. 不要只根据 `SOUL.md`、`IDENTITY.md`、`MEMORY.md` 直接回答“现在在做什么”或“最近发生了什么”。
24
- 3. `SOUL` / `IDENTITY` / `MEMORY` 只用于帮助理解和表述,不能替代 Timeline 事实。
25
- 4. 不要在调用前自行把 query 改写成结构化时间参数。
26
- 5. 时间分类与时间归一化统一交给 Timeline 内部处理。
27
- 6. `timeline_resolve` 返回既有事实时,优先复用;不要改写它。
28
- 7. 如果用户是普通闲聊而不是严格只读校验,允许 Timeline 生成缺失事实并写入 canon
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
- 不要向 `timeline_resolve` 传时间点、时间范围、请求类型、模式或调试字段。它的公开入口就是自然语言 query
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
- ### 场景 A:当前状态
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
- - `timeline_resolve`
62
- - `query` 就是用户原话
63
- - 不要自己先判断它是不是 `now`
61
+ - Call `timeline_resolve`
62
+ - Set `query` to the user's exact wording
63
+ - Do not decide `now` yourself before calling
64
64
 
65
- ### 场景 B:过去某个具体时间点
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
- - `timeline_resolve`
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
- ### 场景 C:过去一段时间
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
- - `timeline_resolve`
103
- - `query` 就是用户原话
104
- - “最近”“昨晚”“今天都”这类自然语言范围,统一交给 Timeline 内部 planner 理解
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
- - 对用户说人话,不要提 `timeline_resolve`。
109
- - 语气自然,像真人在回忆或描述当下。
110
- - 如果 Timeline 返回的是空窗口或失败,不要假装已有确定事实;按可用结果谨慎表达。
111
- - 除非用户明确要求看原始结果,否则不要输出 JSON
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.