@haaaiawd/second-nature 0.2.5 → 0.2.6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -1,5 +1,7 @@
1
1
  import type { ActionBridge } from "../action-bridge.js";
2
2
  import type { OpsRouter } from "../ops/ops-router.js";
3
+ import type { StateDatabase } from "../../storage/db/index.js";
4
+ import type { ObservabilityDatabase } from "../../observability/db/index.js";
3
5
  import type { CliReadModels } from "../read-models/index.js";
4
6
  export interface CliCommandDefinition {
5
7
  name: string;
@@ -10,5 +12,7 @@ export interface CliCommandDeps {
10
12
  readModels: CliReadModels;
11
13
  actionBridge: ActionBridge;
12
14
  opsRouter: OpsRouter;
15
+ stateDb?: StateDatabase;
16
+ observabilityDb?: ObservabilityDatabase;
13
17
  }
14
18
  export declare function createCliCommands(deps: CliCommandDeps): CliCommandDefinition[];
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { credentialVerify } from "./credential.js";
2
4
  import { connectorInit } from "./connector-init.js";
3
5
  import { formatExplanation } from "../explain/format-explanation.js";
@@ -5,6 +7,139 @@ import { explainSurfaceSubject } from "../explain/explain-surface-subject.js";
5
7
  import { showOperatorFallback, OperatorFallbackNotFoundError, } from "../ops/show-operator-fallback.js";
6
8
  import { runStorageModeSmoke } from "../../storage/bootstrap/storage-mode-smoke.js";
7
9
  import { policySet } from "./policy.js";
10
+ const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
11
+ function safeShortText(value, maxLen) {
12
+ if (typeof value !== "string")
13
+ return undefined;
14
+ const trimmed = value.trim();
15
+ if (trimmed.length === 0)
16
+ return undefined;
17
+ return trimmed.slice(0, maxLen);
18
+ }
19
+ function resolveWorkspaceRoot(input) {
20
+ if (typeof input?.workspaceRoot === "string" && input.workspaceRoot.trim().length > 0) {
21
+ return path.resolve(input.workspaceRoot);
22
+ }
23
+ if (typeof process.env.SECOND_NATURE_WORKSPACE_ROOT === "string" && process.env.SECOND_NATURE_WORKSPACE_ROOT.trim().length > 0) {
24
+ return path.resolve(process.env.SECOND_NATURE_WORKSPACE_ROOT);
25
+ }
26
+ return process.cwd();
27
+ }
28
+ function readSetupText(fileName) {
29
+ const candidates = fileName === "SKILL.md"
30
+ ? [path.resolve(process.cwd(), "SKILL.md"), path.resolve(process.cwd(), "..", "SKILL.md")]
31
+ : [path.resolve(process.cwd(), "plugin", "agent-inner-guide.md"), path.resolve(process.cwd(), "..", "plugin", "agent-inner-guide.md")];
32
+ for (const candidate of candidates) {
33
+ try {
34
+ const content = fs.readFileSync(candidate, "utf-8");
35
+ return { ok: true, path: candidate, content };
36
+ }
37
+ catch {
38
+ // try next candidate
39
+ }
40
+ }
41
+ return { ok: false, path: candidates[0] ?? fileName, error: `Could not read ${fileName}` };
42
+ }
43
+ function summarizeSetupText(content) {
44
+ const lines = content.split("\n");
45
+ const nonEmpty = lines.filter((line) => line.trim().length > 0);
46
+ const first = nonEmpty.slice(0, 3).join("\n");
47
+ const marker = content.length > first.length ? "\n\n[...]" : "";
48
+ return `${first}${marker}`;
49
+ }
50
+ function readSetupAckMarker(workspaceRoot) {
51
+ const markerPath = path.join(workspaceRoot, SETUP_MARKER_RELATIVE_PATH);
52
+ try {
53
+ const raw = fs.readFileSync(markerPath, "utf-8");
54
+ const marker = JSON.parse(raw);
55
+ if (marker.status === "acknowledged") {
56
+ return {
57
+ status: "acknowledged",
58
+ markerPath,
59
+ acknowledgedAt: typeof marker.acknowledgedAt === "string" ? marker.acknowledgedAt : undefined,
60
+ placedIn: typeof marker.placedIn === "string" ? marker.placedIn : undefined,
61
+ };
62
+ }
63
+ }
64
+ catch {
65
+ // marker missing or unreadable
66
+ }
67
+ return { status: "pending", markerPath };
68
+ }
69
+ async function buildSetupHintPayload(input) {
70
+ const format = input?.format === "full" ? "full" : "summary";
71
+ const includeSkill = input?.includeSkill !== false;
72
+ const includeGuide = input?.includeGuide !== false;
73
+ const workspaceRoot = resolveWorkspaceRoot(input);
74
+ const ack = readSetupAckMarker(workspaceRoot);
75
+ const data = {
76
+ status: ack.status,
77
+ workspaceRoot,
78
+ markerPath: ack.markerPath,
79
+ acknowledgedAt: ack.acknowledgedAt,
80
+ placedIn: ack.placedIn,
81
+ recommendedPlacement: [
82
+ "agent prompt",
83
+ "workspace/IDENTITY.md",
84
+ "workspace/USER.md",
85
+ ],
86
+ nextStep: ack.status === "acknowledged"
87
+ ? "setup_already_acknowledged"
88
+ : "read_returned_guidance_then_run_setup_ack",
89
+ };
90
+ if (includeSkill) {
91
+ const skill = readSetupText("SKILL.md");
92
+ data.skill = skill.ok
93
+ ? {
94
+ path: skill.path,
95
+ content: format === "full" ? skill.content : summarizeSetupText(skill.content),
96
+ }
97
+ : skill;
98
+ }
99
+ if (includeGuide) {
100
+ const guide = readSetupText("agent-inner-guide.md");
101
+ data.guide = guide.ok
102
+ ? {
103
+ path: guide.path,
104
+ content: format === "full" ? guide.content : summarizeSetupText(guide.content),
105
+ }
106
+ : guide;
107
+ }
108
+ return {
109
+ ok: true,
110
+ command: "setup_hint",
111
+ surfaceMode: "workspace_full_runtime",
112
+ message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
113
+ data,
114
+ };
115
+ }
116
+ async function buildSetupAckPayload(input) {
117
+ const workspaceRoot = resolveWorkspaceRoot(input);
118
+ const markerPath = path.join(workspaceRoot, SETUP_MARKER_RELATIVE_PATH);
119
+ const marker = {
120
+ acknowledgedAt: new Date().toISOString(),
121
+ acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
122
+ placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
123
+ note: safeShortText(input?.note, 240),
124
+ guideVersion: "0.2.5",
125
+ source: "second-nature-cli",
126
+ skillPath: "SKILL.md",
127
+ guidePath: "plugin/agent-inner-guide.md",
128
+ status: "acknowledged",
129
+ };
130
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
131
+ fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
132
+ return {
133
+ ok: true,
134
+ command: "setup_ack",
135
+ surfaceMode: "workspace_full_runtime",
136
+ message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace.",
137
+ data: {
138
+ markerPath,
139
+ ...marker,
140
+ },
141
+ };
142
+ }
8
143
  const notImplemented = async (command) => ({
9
144
  ok: false,
10
145
  command,
@@ -22,16 +157,45 @@ function explainSubjectError(code, message) {
22
157
  };
23
158
  }
24
159
  export function createCliCommands(deps) {
25
- const { readModels, actionBridge, opsRouter } = deps;
160
+ const { readModels, actionBridge, opsRouter, stateDb, observabilityDb } = deps;
161
+ const flush = () => {
162
+ try {
163
+ stateDb?.flush();
164
+ }
165
+ catch {
166
+ // ignore flush errors to avoid masking command results
167
+ }
168
+ try {
169
+ observabilityDb?.flush();
170
+ }
171
+ catch {
172
+ // ignore flush errors to avoid masking command results
173
+ }
174
+ };
26
175
  const opsCommand = (name, description) => ({
27
176
  name,
28
177
  description,
29
178
  execute: async (input) => {
30
179
  const surface = await Promise.resolve(opsRouter.dispatch(name, input));
180
+ flush();
31
181
  return surface;
32
182
  },
33
183
  });
34
184
  return [
185
+ {
186
+ name: "setup_hint",
187
+ description: "Return the packaged setup SKILL and agent inner guide for first-run onboarding",
188
+ execute: async (input) => buildSetupHintPayload(input),
189
+ },
190
+ {
191
+ name: "setup_ack",
192
+ description: "Persist that the packaged setup guide was read and placed into working anchors",
193
+ execute: async (input) => {
194
+ const result = await buildSetupAckPayload(input);
195
+ flush();
196
+ return result;
197
+ },
198
+ },
35
199
  {
36
200
  name: "status",
37
201
  description: "T1.2.6 — Show v6 aggregated Second Nature status (narrative + dream + cycles + runtime)",
@@ -47,7 +211,9 @@ export function createCliCommands(deps) {
47
211
  execute: async (input) => {
48
212
  const action = typeof input?.action === "string" ? input.action : "show";
49
213
  if (action === "set") {
50
- return policySet(actionBridge, input);
214
+ const result = await policySet(actionBridge, input);
215
+ flush();
216
+ return result;
51
217
  }
52
218
  // T1.2.6 (SN-CODE-01): `policy show` (default) returns the current rhythm policy
53
219
  // snapshot. Returns workspace defaults when no policy row has been persisted yet.
@@ -159,6 +325,7 @@ export function createCliCommands(deps) {
159
325
  description: "Workspace heartbeat_check ops surface (v5 HeartbeatSurfaceResult)",
160
326
  execute: async (input) => {
161
327
  const surface = await Promise.resolve(opsRouter.dispatch("heartbeat_check", input));
328
+ flush();
162
329
  return surface;
163
330
  },
164
331
  },
@@ -174,6 +341,7 @@ export function createCliCommands(deps) {
174
341
  runRepairFixture,
175
342
  workspaceRoot,
176
343
  });
344
+ flush();
177
345
  return { ok: true, data };
178
346
  },
179
347
  },
@@ -218,6 +386,7 @@ export function createCliCommands(deps) {
218
386
  description: "T1.2.8 — probe host capabilities and persist report (static unknown adapter in CLI context)",
219
387
  execute: async (input) => {
220
388
  const surface = await Promise.resolve(opsRouter.dispatch("capability_probe", input));
389
+ flush();
221
390
  return surface;
222
391
  },
223
392
  },
@@ -226,6 +395,7 @@ export function createCliCommands(deps) {
226
395
  description: "T3.3.2 — run near-real connector smoke (sentinel Moltbook + EvoMap, no live HTTP)",
227
396
  execute: async (input) => {
228
397
  const surface = await Promise.resolve(opsRouter.dispatch("near_real_smoke", input));
398
+ flush();
229
399
  return surface;
230
400
  },
231
401
  },
@@ -249,6 +419,7 @@ export function createCliCommands(deps) {
249
419
  ? input.workspaceRoot
250
420
  : undefined,
251
421
  });
422
+ flush();
252
423
  return result;
253
424
  },
254
425
  },
@@ -261,10 +432,11 @@ export function createCliCommands(deps) {
261
432
  },
262
433
  },
263
434
  {
264
- name: "connector_status",
265
- description: "T1.2.3show connector inventory, trust/executable/conflict summary",
435
+ name: "credential",
436
+ description: "T1.4.1inspect or verify credential health without exposing plaintext",
266
437
  execute: async (input) => {
267
- const surface = await Promise.resolve(opsRouter.dispatch("connector_status", input));
438
+ const surface = await Promise.resolve(opsRouter.dispatch("credential", input));
439
+ flush();
268
440
  return surface;
269
441
  },
270
442
  },
@@ -273,6 +445,7 @@ export function createCliCommands(deps) {
273
445
  description: "T1.2.3 — dry-run test a connector by platformId (default dry-run)",
274
446
  execute: async (input) => {
275
447
  const surface = await Promise.resolve(opsRouter.dispatch("connector_test", input));
448
+ flush();
276
449
  return surface;
277
450
  },
278
451
  },
@@ -292,6 +465,7 @@ export function createCliCommands(deps) {
292
465
  description: "T1.2.4 — owner-governed goal operations: set, list, accept, reject",
293
466
  execute: async (input) => {
294
467
  const surface = await Promise.resolve(opsRouter.dispatch("goal", input));
468
+ flush();
295
469
  return surface;
296
470
  },
297
471
  },
@@ -290,6 +290,8 @@ export function createCommandRouter(options = {}) {
290
290
  readModels: runtime.readModels,
291
291
  actionBridge: runtime.actionBridge,
292
292
  opsRouter,
293
+ stateDb: runtime.stateDb,
294
+ observabilityDb: runtime.observabilityDb,
293
295
  });
294
296
  return {
295
297
  commands,
@@ -1144,24 +1144,30 @@ export function createOpsRouter(deps) {
1144
1144
  };
1145
1145
  return envelope;
1146
1146
  }
1147
- const fromVersion = typeof input?.from === "string" ? input.from : "";
1148
- const toVersion = typeof input?.to === "string" ? input.to : "";
1147
+ let fromVersion = typeof input?.from === "string" ? input.from : "";
1148
+ let toVersion = typeof input?.to === "string" ? input.to : "";
1149
1149
  if (!fromVersion || !toVersion) {
1150
- const envelope = {
1151
- ok: false,
1152
- command: "narrative:diff",
1153
- runtimeMode: "workspace_full_runtime",
1154
- surfaceMode: "cli",
1155
- generatedAt,
1156
- error: {
1157
- code: "MISSING_VERSIONS",
1158
- message: "narrative:diff requires 'from' and 'to' version arguments",
1159
- nextStep: "reinvoke_with_from_and_to",
1160
- },
1161
- warnings: [],
1162
- sourceRefs: [],
1163
- };
1164
- return envelope;
1150
+ // Auto-resolve the two most recent narrative timeline versions when not provided.
1151
+ const recent = await deps.narrativeTimelineDeps.stateMemoryPort.listNarrativeTimeline(new Date(0).toISOString(), new Date().toISOString(), { limit: 2 });
1152
+ if (recent.length < 2) {
1153
+ const envelope = {
1154
+ ok: false,
1155
+ command: "narrative:diff",
1156
+ runtimeMode: "workspace_full_runtime",
1157
+ surfaceMode: "cli",
1158
+ generatedAt,
1159
+ error: {
1160
+ code: "NARRATIVE_DIFF_REQUIRES_TWO_VERSIONS",
1161
+ message: `narrative:diff requires at least two timeline versions; found ${recent.length}. Pass explicit 'from' and 'to', or run snapshot:capture twice.`,
1162
+ nextStep: "run_snapshot_capture_twice_or_pass_from_and_to",
1163
+ },
1164
+ warnings: [],
1165
+ sourceRefs: [],
1166
+ };
1167
+ return envelope;
1168
+ }
1169
+ fromVersion = recent[1].version;
1170
+ toVersion = recent[0].version;
1165
1171
  }
1166
1172
  try {
1167
1173
  const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
@@ -5,6 +5,8 @@ export interface ObservabilityDatabase {
5
5
  sqlite: Database;
6
6
  db: ReturnType<typeof drizzle<typeof schema>>;
7
7
  schema: typeof schema;
8
+ /** Persist in-memory sql.js state to disk without closing the connection. */
9
+ flush(): void;
8
10
  close(): void;
9
11
  }
10
12
  export declare function createObservabilityDatabase(filename?: string): ObservabilityDatabase;
@@ -126,6 +126,12 @@ export function createObservabilityDatabase(filename = "observability.db") {
126
126
  sqlite,
127
127
  db,
128
128
  schema,
129
+ flush() {
130
+ if (!isMemory) {
131
+ const data = sqlite.export();
132
+ fs.writeFileSync(dbPath, Buffer.from(data));
133
+ }
134
+ },
129
135
  close() {
130
136
  if (!isMemory) {
131
137
  const data = sqlite.export();
@@ -5,6 +5,8 @@ export interface StateDatabase {
5
5
  sqlite: Database;
6
6
  db: ReturnType<typeof drizzle<typeof schema>>;
7
7
  schema: typeof schema;
8
+ /** Persist in-memory sql.js state to disk without closing the connection. */
9
+ flush(): void;
8
10
  close(): void;
9
11
  }
10
12
  export declare function createStateDatabase(filename?: string): StateDatabase;
@@ -220,14 +220,14 @@ const STATE_SCHEMA_SQL = `
220
220
  payload_json TEXT,
221
221
  lifecycle_status TEXT NOT NULL DEFAULT 'pending'
222
222
  );
223
- CREATE TABLE IF NOT EXISTS action_closure_record (
224
- id TEXT PRIMARY KEY,
225
- created_at TEXT NOT NULL,
226
- cycle_id TEXT NOT NULL,
227
- platform_id TEXT,
228
- capability_id TEXT,
229
- proposal_id TEXT,
230
- decision_id TEXT,
223
+ CREATE TABLE IF NOT EXISTS action_closure_record (
224
+ id TEXT PRIMARY KEY,
225
+ created_at TEXT NOT NULL,
226
+ cycle_id TEXT NOT NULL,
227
+ platform_id TEXT,
228
+ capability_id TEXT,
229
+ proposal_id TEXT,
230
+ decision_id TEXT,
231
231
  status TEXT NOT NULL,
232
232
  reason TEXT,
233
233
  next_state TEXT,
@@ -394,6 +394,12 @@ export function createStateDatabase(filename = "state.db") {
394
394
  sqlite,
395
395
  db,
396
396
  schema,
397
+ flush() {
398
+ if (!isMemory) {
399
+ const data = sqlite.export();
400
+ fs.writeFileSync(dbPath, Buffer.from(data));
401
+ }
402
+ },
397
403
  close() {
398
404
  if (!isMemory) {
399
405
  const data = sqlite.export();