clawpilot 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -67
- package/bin/cli.js +591 -100
- package/docs/PUBLISH_CHECKLIST.md +31 -22
- package/docs/plans/2026-02-13-v0.3-clawra-gap-plan.md +199 -0
- package/docs/plans/2026-02-13-v0.3-implementation.md +434 -0
- package/docs/releases/v0.1.2.md +57 -0
- package/docs/releases/v0.2.0-draft.md +24 -0
- package/docs/releases/v0.2.1-draft.md +19 -0
- package/docs/troubleshooting.md +44 -0
- package/package.json +1 -1
- package/skill/SKILL.md +33 -22
- package/src/config/migrations.js +68 -0
- package/src/install.js +248 -181
- package/src/preflight.js +288 -0
- package/src/runtime/index.js +127 -0
- package/src/runtime/openclaw-gateway.js +135 -0
- package/src/runtime/productivity.js +42 -0
- package/src/runtime/role-pack.js +11 -0
- package/src/runtime/social-formatters.js +22 -0
- package/src/runtime/state-store.js +33 -0
- package/src/runtime/template-renderer.js +7 -0
- package/src/runtime/weekly-report.js +19 -0
- package/templates/content/evening.md +7 -0
- package/templates/content/midday.md +8 -0
- package/templates/content/morning.md +8 -0
- package/templates/content/report.md +5 -0
- package/templates/content/social-linkedin.md +8 -0
- package/templates/content/social-telegram.md +8 -0
- package/templates/content/social-x.md +8 -0
- package/templates/role-packs/hana.json +7 -0
- package/templates/role-packs/minji.json +7 -0
- package/templates/soul-injection.md +38 -14
package/src/preflight.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const TROUBLESHOOTING_DOC = 'docs/troubleshooting.md';
|
|
6
|
+
const MIN_OPENCLAW_VERSION = '1.0.0';
|
|
7
|
+
const SKILL_ID = 'clawpilot-productivity';
|
|
8
|
+
|
|
9
|
+
function createIssue(code, reason, fix) {
|
|
10
|
+
return {
|
|
11
|
+
code,
|
|
12
|
+
reason,
|
|
13
|
+
fix,
|
|
14
|
+
docs: TROUBLESHOOTING_DOC
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createWarning(code, reason, fix) {
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
reason,
|
|
22
|
+
fix,
|
|
23
|
+
docs: TROUBLESHOOTING_DOC
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseSemver(text) {
|
|
28
|
+
const match = String(text || '').match(/(\d+)\.(\d+)\.(\d+)/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
major: Number(match[1]),
|
|
34
|
+
minor: Number(match[2]),
|
|
35
|
+
patch: Number(match[3]),
|
|
36
|
+
raw: match[0]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareSemver(a, b) {
|
|
41
|
+
if (a.major !== b.major) {
|
|
42
|
+
return a.major - b.major;
|
|
43
|
+
}
|
|
44
|
+
if (a.minor !== b.minor) {
|
|
45
|
+
return a.minor - b.minor;
|
|
46
|
+
}
|
|
47
|
+
return a.patch - b.patch;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runCommand(commandRunner, command, args) {
|
|
51
|
+
try {
|
|
52
|
+
return commandRunner(command, args, { encoding: 'utf8' });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
status: 1,
|
|
56
|
+
error
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkOpenClaw({ commandRunner }) {
|
|
62
|
+
const result = runCommand(commandRunner, 'openclaw', ['--version']);
|
|
63
|
+
const output = `${result.stdout || ''} ${result.stderr || ''}`.trim();
|
|
64
|
+
const errorCode = result.error && result.error.code;
|
|
65
|
+
|
|
66
|
+
if (result.status === 0) {
|
|
67
|
+
const parsed = parseSemver(output);
|
|
68
|
+
const min = parseSemver(MIN_OPENCLAW_VERSION);
|
|
69
|
+
if (parsed && min && compareSemver(parsed, min) < 0) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
issue: createIssue(
|
|
73
|
+
'openclaw_version_unsupported',
|
|
74
|
+
`OpenClaw version ${parsed.raw} is below required ${MIN_OPENCLAW_VERSION}.`,
|
|
75
|
+
`Upgrade OpenClaw to ${MIN_OPENCLAW_VERSION} or newer.`
|
|
76
|
+
)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!parsed) {
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
detail: output || 'openclaw version check passed',
|
|
84
|
+
warning: createWarning(
|
|
85
|
+
'openclaw_version_unknown',
|
|
86
|
+
'OpenClaw version output could not be parsed.',
|
|
87
|
+
'Run openclaw --version and verify it reports semantic version (x.y.z).'
|
|
88
|
+
)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
detail: output || 'openclaw version check passed'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (errorCode === 'ENOENT' || /not found|is not recognized/i.test(output)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
issue: createIssue(
|
|
102
|
+
'gateway_missing',
|
|
103
|
+
'openclaw CLI not found.',
|
|
104
|
+
'Install OpenClaw CLI and verify with: openclaw --version'
|
|
105
|
+
)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
issue: createIssue(
|
|
112
|
+
'gateway_unreachable',
|
|
113
|
+
output || 'openclaw CLI check failed.',
|
|
114
|
+
'Run openclaw --version and fix environment PATH or shell profile.'
|
|
115
|
+
)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function checkWritableDirectory({ fsOps, dirPath, code, label }) {
|
|
120
|
+
try {
|
|
121
|
+
fsOps.mkdirSync(dirPath, { recursive: true });
|
|
122
|
+
return { ok: true, detail: `${label} writable` };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
issue: createIssue(
|
|
127
|
+
code,
|
|
128
|
+
`${label} is not writable: ${dirPath}`,
|
|
129
|
+
`Grant write access to ${dirPath} or use --home <path> with writable location.`
|
|
130
|
+
)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkConfig({ fsOps, configPath }) {
|
|
136
|
+
if (!fsOps.existsSync(configPath)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
detail: 'openclaw.json will be created',
|
|
140
|
+
config: {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const config = JSON.parse(fsOps.readFileSync(configPath, 'utf8'));
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
detail: 'openclaw.json is valid JSON',
|
|
149
|
+
config
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
issue: createIssue(
|
|
155
|
+
'config_invalid_json',
|
|
156
|
+
'openclaw.json is not valid JSON.',
|
|
157
|
+
'Fix JSON syntax in openclaw.json before running install.'
|
|
158
|
+
)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkGatewayReadiness({ config, env }) {
|
|
164
|
+
const entry = config?.skills?.entries?.[SKILL_ID];
|
|
165
|
+
if (!entry) {
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
detail: 'clawpilot config entry not found yet'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (entry.delivery?.mode && entry.delivery.mode !== 'openclaw-gateway') {
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
detail: `delivery mode is ${entry.delivery.mode}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const warnings = [];
|
|
180
|
+
if (!entry.delivery?.channel) {
|
|
181
|
+
warnings.push(
|
|
182
|
+
createWarning(
|
|
183
|
+
'delivery_channel_missing',
|
|
184
|
+
'Gateway delivery channel is not configured.',
|
|
185
|
+
'Set delivery.channel in openclaw.json or pass --channel during install/run.'
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hasToken = Boolean(
|
|
191
|
+
env?.OPENCLAW_GATEWAY_TOKEN ||
|
|
192
|
+
entry.env?.OPENCLAW_GATEWAY_TOKEN ||
|
|
193
|
+
entry.delivery?.token
|
|
194
|
+
);
|
|
195
|
+
if (!hasToken) {
|
|
196
|
+
warnings.push(
|
|
197
|
+
createWarning(
|
|
198
|
+
'gateway_token_missing',
|
|
199
|
+
'Gateway token source is not configured.',
|
|
200
|
+
'Set OPENCLAW_GATEWAY_TOKEN env var or configure token in OpenClaw settings.'
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
detail: 'gateway delivery readiness checked',
|
|
208
|
+
warnings
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function runPreflight({
|
|
213
|
+
openClawHome,
|
|
214
|
+
commandRunner = spawnSync,
|
|
215
|
+
fsOps = fs,
|
|
216
|
+
env = process.env
|
|
217
|
+
}) {
|
|
218
|
+
if (!openClawHome) {
|
|
219
|
+
throw new Error('openClawHome is required for preflight checks.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const skillsDir = path.join(openClawHome, 'skills');
|
|
223
|
+
const workspaceDir = path.join(openClawHome, 'workspace');
|
|
224
|
+
const configPath = path.join(openClawHome, 'openclaw.json');
|
|
225
|
+
|
|
226
|
+
const configCheck = checkConfig({ fsOps, configPath });
|
|
227
|
+
const checks = [
|
|
228
|
+
{ name: 'openclaw_cli', ...checkOpenClaw({ commandRunner }) },
|
|
229
|
+
{
|
|
230
|
+
name: 'openclaw_home_writable',
|
|
231
|
+
...checkWritableDirectory({
|
|
232
|
+
fsOps,
|
|
233
|
+
dirPath: openClawHome,
|
|
234
|
+
code: 'permission_denied',
|
|
235
|
+
label: 'OpenClaw home'
|
|
236
|
+
})
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'skills_dir_writable',
|
|
240
|
+
...checkWritableDirectory({
|
|
241
|
+
fsOps,
|
|
242
|
+
dirPath: skillsDir,
|
|
243
|
+
code: 'permission_denied',
|
|
244
|
+
label: 'OpenClaw skills directory'
|
|
245
|
+
})
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'workspace_dir_writable',
|
|
249
|
+
...checkWritableDirectory({
|
|
250
|
+
fsOps,
|
|
251
|
+
dirPath: workspaceDir,
|
|
252
|
+
code: 'permission_denied',
|
|
253
|
+
label: 'OpenClaw workspace directory'
|
|
254
|
+
})
|
|
255
|
+
},
|
|
256
|
+
{ name: 'openclaw_config', ...configCheck },
|
|
257
|
+
{
|
|
258
|
+
name: 'gateway_config_readiness',
|
|
259
|
+
...checkGatewayReadiness({ config: configCheck.config, env })
|
|
260
|
+
}
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const issues = checks.filter((check) => !check.ok).map((check) => check.issue);
|
|
264
|
+
const warnings = checks
|
|
265
|
+
.flatMap((check) => {
|
|
266
|
+
const entries = [];
|
|
267
|
+
if (check.warning) {
|
|
268
|
+
entries.push(check.warning);
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(check.warnings)) {
|
|
271
|
+
entries.push(...check.warnings);
|
|
272
|
+
}
|
|
273
|
+
return entries;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
ok: issues.length === 0,
|
|
278
|
+
checks,
|
|
279
|
+
issues,
|
|
280
|
+
warnings
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
MIN_OPENCLAW_VERSION,
|
|
286
|
+
runPreflight,
|
|
287
|
+
TROUBLESHOOTING_DOC
|
|
288
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const { loadRolePack } = require('./role-pack');
|
|
4
|
+
const { loadState, saveState } = require('./state-store');
|
|
5
|
+
const { handleMorning, handleMidday, handleEvening } = require('./productivity');
|
|
6
|
+
const { sendViaGateway } = require('./openclaw-gateway');
|
|
7
|
+
const { buildWeeklyReport } = require('./weekly-report');
|
|
8
|
+
const { TROUBLESHOOTING_DOC } = require('../preflight');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TASKS = [
|
|
11
|
+
'Define top priority',
|
|
12
|
+
'Complete one high-impact deliverable',
|
|
13
|
+
'Share progress update'
|
|
14
|
+
];
|
|
15
|
+
const SKILL_ID = 'clawpilot-productivity';
|
|
16
|
+
|
|
17
|
+
function resolveStateFile({ stateFile, openClawHome }) {
|
|
18
|
+
if (stateFile) {
|
|
19
|
+
return path.resolve(stateFile);
|
|
20
|
+
}
|
|
21
|
+
if (openClawHome) {
|
|
22
|
+
return path.join(openClawHome, 'workspace', 'clawpilot-runtime-state.json');
|
|
23
|
+
}
|
|
24
|
+
return path.resolve('.clawpilot-state.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readRuntimeConfig(openClawHome) {
|
|
28
|
+
if (!openClawHome) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const configPath = path.join(openClawHome, 'openclaw.json');
|
|
33
|
+
if (!fs.existsSync(configPath)) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
39
|
+
return config.skills?.entries?.[SKILL_ID] || {};
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createRuntimeError(code, reason, fix) {
|
|
46
|
+
const error = new Error(reason);
|
|
47
|
+
error.code = code;
|
|
48
|
+
error.reason = reason;
|
|
49
|
+
error.fix = fix;
|
|
50
|
+
error.docs = TROUBLESHOOTING_DOC;
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runRuntimeCommand(options) {
|
|
55
|
+
const packageRoot = options.packageRoot || process.cwd();
|
|
56
|
+
const command = options.command || 'morning';
|
|
57
|
+
const runtimeConfig = readRuntimeConfig(options.openClawHome);
|
|
58
|
+
const configuredRolePack = runtimeConfig.runtime?.defaults?.rolePack || runtimeConfig.rolePack || 'hana';
|
|
59
|
+
const rolePack = loadRolePack(packageRoot, options.rolePack || configuredRolePack);
|
|
60
|
+
const stateFilePath = resolveStateFile(options);
|
|
61
|
+
const state = loadState(stateFilePath);
|
|
62
|
+
const dateKey = options.dateKey || new Date().toISOString().slice(0, 10);
|
|
63
|
+
const resolvedChannel = options.channel ?? runtimeConfig.delivery?.channel ?? null;
|
|
64
|
+
|
|
65
|
+
const handlers = {
|
|
66
|
+
morning: () =>
|
|
67
|
+
handleMorning({
|
|
68
|
+
dateKey,
|
|
69
|
+
state,
|
|
70
|
+
tasks: options.tasks && options.tasks.length > 0 ? options.tasks : DEFAULT_TASKS,
|
|
71
|
+
assistantName: rolePack.name
|
|
72
|
+
}),
|
|
73
|
+
midday: () =>
|
|
74
|
+
handleMidday({
|
|
75
|
+
dateKey,
|
|
76
|
+
state,
|
|
77
|
+
statuses: options.statuses || []
|
|
78
|
+
}),
|
|
79
|
+
evening: () =>
|
|
80
|
+
handleEvening({
|
|
81
|
+
dateKey,
|
|
82
|
+
state
|
|
83
|
+
}),
|
|
84
|
+
report: () =>
|
|
85
|
+
{
|
|
86
|
+
const report = buildWeeklyReport(state);
|
|
87
|
+
return {
|
|
88
|
+
message: `${rolePack.name} weekly report\n${report.summary}\nCompletion rate: ${report.completionRate}`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!handlers[command]) {
|
|
94
|
+
throw new Error(`Unsupported runtime command: ${command}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = handlers[command]();
|
|
98
|
+
saveState(stateFilePath, state);
|
|
99
|
+
|
|
100
|
+
if (!options.dryRun) {
|
|
101
|
+
if (!resolvedChannel) {
|
|
102
|
+
throw createRuntimeError(
|
|
103
|
+
'channel_required',
|
|
104
|
+
'Channel is required when sending without --dry-run.',
|
|
105
|
+
'Use --channel <target> or set delivery.channel in openclaw.json.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
sendViaGateway({
|
|
109
|
+
channel: resolvedChannel,
|
|
110
|
+
message: result.message,
|
|
111
|
+
media: rolePack.avatarUrl
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
command,
|
|
117
|
+
rolePack,
|
|
118
|
+
message: result.message,
|
|
119
|
+
channel: resolvedChannel,
|
|
120
|
+
stateFile: stateFilePath,
|
|
121
|
+
deliveryMode: options.dryRun ? 'dry-run' : 'send'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
runRuntimeCommand
|
|
127
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { spawnSync } = require('node:child_process');
|
|
2
|
+
const { TROUBLESHOOTING_DOC } = require('../preflight');
|
|
3
|
+
|
|
4
|
+
function buildGatewayArgs({ channel, message, media }) {
|
|
5
|
+
const args = [
|
|
6
|
+
'message',
|
|
7
|
+
'send',
|
|
8
|
+
'--action',
|
|
9
|
+
'send',
|
|
10
|
+
'--channel',
|
|
11
|
+
channel,
|
|
12
|
+
'--message',
|
|
13
|
+
message
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
if (media) {
|
|
17
|
+
args.push('--media', media);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return args;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function classifyGatewayFailure(result = {}) {
|
|
24
|
+
const stderr = result.stderr || '';
|
|
25
|
+
const stdout = result.stdout || '';
|
|
26
|
+
const errorMessage = result.error && result.error.message ? result.error.message : '';
|
|
27
|
+
const errorCode = result.error && result.error.code ? result.error.code : '';
|
|
28
|
+
const text = `${stderr}\n${stdout}\n${errorMessage}`.toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (errorCode === 'ENOENT' || /enoent|openclaw.*not found|is not recognized/.test(text)) {
|
|
31
|
+
return {
|
|
32
|
+
code: 'gateway_missing',
|
|
33
|
+
reason: 'OpenClaw CLI is not available.',
|
|
34
|
+
fix: 'Install OpenClaw and verify with: openclaw --version'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (/invalid token|unauthorized|forbidden|401|403|auth/.test(text)) {
|
|
39
|
+
return {
|
|
40
|
+
code: 'auth_invalid',
|
|
41
|
+
reason: 'Gateway authentication failed.',
|
|
42
|
+
fix: 'Refresh gateway token and verify channel auth settings.'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (/channel.*not found|chat.*not found|unknown channel|404/.test(text)) {
|
|
47
|
+
return {
|
|
48
|
+
code: 'channel_not_found',
|
|
49
|
+
reason: 'Target channel was not found.',
|
|
50
|
+
fix: 'Check --channel value and make sure the destination exists.'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (/permission denied|eacces/.test(text)) {
|
|
55
|
+
return {
|
|
56
|
+
code: 'permission_denied',
|
|
57
|
+
reason: 'Permission denied while sending through gateway.',
|
|
58
|
+
fix: 'Grant required permissions to the OpenClaw process and channel.'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (/gateway.*unavailable|gateway.*offline|connection refused|econnrefused/.test(text)) {
|
|
63
|
+
return {
|
|
64
|
+
code: 'gateway_unreachable',
|
|
65
|
+
reason: 'OpenClaw gateway is not reachable.',
|
|
66
|
+
fix: 'Start the gateway and verify endpoint connectivity.'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (/timed?out|etimedout|enotfound|econnreset|network/.test(text)) {
|
|
71
|
+
return {
|
|
72
|
+
code: 'network_timeout',
|
|
73
|
+
reason: 'Network failure occurred while sending.',
|
|
74
|
+
fix: 'Retry the command and verify network/DNS connectivity.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (/invalid payload|bad request|missing required|400/.test(text)) {
|
|
79
|
+
return {
|
|
80
|
+
code: 'invalid_payload',
|
|
81
|
+
reason: 'Gateway rejected the payload.',
|
|
82
|
+
fix: 'Validate message/channel/media values before sending.'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (/rate limit|too many requests|429/.test(text)) {
|
|
87
|
+
return {
|
|
88
|
+
code: 'rate_limited',
|
|
89
|
+
reason: 'Gateway or upstream provider rate-limited the request.',
|
|
90
|
+
fix: 'Retry with backoff and reduce send frequency.'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
code: 'unknown',
|
|
96
|
+
reason: 'Gateway send failed.',
|
|
97
|
+
fix: 'Check gateway logs and troubleshooting documentation.'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class GatewaySendError extends Error {
|
|
102
|
+
constructor({ code, reason, fix, result }) {
|
|
103
|
+
super(`${code}: ${reason}`);
|
|
104
|
+
this.name = 'GatewaySendError';
|
|
105
|
+
this.code = code;
|
|
106
|
+
this.reason = reason;
|
|
107
|
+
this.fix = fix;
|
|
108
|
+
this.docs = TROUBLESHOOTING_DOC;
|
|
109
|
+
this.status = result.status ?? null;
|
|
110
|
+
this.stderr = result.stderr || '';
|
|
111
|
+
this.stdout = result.stdout || '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sendViaGateway(payload, runner = spawnSync) {
|
|
116
|
+
const args = buildGatewayArgs(payload);
|
|
117
|
+
const result = runner('openclaw', args, { encoding: 'utf8' });
|
|
118
|
+
|
|
119
|
+
if (result.error || result.status !== 0) {
|
|
120
|
+
const failure = classifyGatewayFailure(result);
|
|
121
|
+
throw new GatewaySendError({
|
|
122
|
+
...failure,
|
|
123
|
+
result
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result.stdout;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
buildGatewayArgs,
|
|
132
|
+
classifyGatewayFailure,
|
|
133
|
+
GatewaySendError,
|
|
134
|
+
sendViaGateway
|
|
135
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { ensureDayState } = require('./state-store');
|
|
2
|
+
|
|
3
|
+
function handleMorning({ dateKey, state, tasks, assistantName }) {
|
|
4
|
+
const day = ensureDayState(state, dateKey);
|
|
5
|
+
day.tasks = tasks.map((title) => ({
|
|
6
|
+
title,
|
|
7
|
+
status: 'pending'
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
message: `${assistantName} morning plan:\n- ${tasks.join('\n- ')}`
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function handleMidday({ dateKey, state, statuses }) {
|
|
16
|
+
const day = ensureDayState(state, dateKey);
|
|
17
|
+
day.tasks = day.tasks.map((task, index) => ({
|
|
18
|
+
...task,
|
|
19
|
+
status: statuses[index] || task.status
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const blockedCount = day.tasks.filter((task) => task.status === 'blocked').length;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
message: `Midday check-in complete. blocked count: ${blockedCount}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleEvening({ dateKey, state }) {
|
|
30
|
+
const day = ensureDayState(state, dateKey);
|
|
31
|
+
const doneCount = day.tasks.filter((task) => task.status === 'done').length;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
message: `Evening review: ${doneCount}/${day.tasks.length} done. First task for tomorrow: define priorities.`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
handleMorning,
|
|
40
|
+
handleMidday,
|
|
41
|
+
handleEvening
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function loadRolePack(packageRoot, name = 'hana') {
|
|
5
|
+
const rolePackPath = path.join(packageRoot, 'templates', 'role-packs', `${name}.json`);
|
|
6
|
+
return JSON.parse(fs.readFileSync(rolePackPath, 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
loadRolePack
|
|
11
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function formatWins(wins) {
|
|
2
|
+
return wins.map((win) => `- ${win}`).join('\n');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function formatSocialPost(channel, data) {
|
|
6
|
+
const winsBlock = formatWins(data.wins || []);
|
|
7
|
+
const lesson = data.lesson || '';
|
|
8
|
+
|
|
9
|
+
if (channel === 'linkedin') {
|
|
10
|
+
return `Today I shipped:\n${winsBlock}\nKey lesson: ${lesson}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (channel === 'x') {
|
|
14
|
+
return `Wins:\n${winsBlock}\nLesson: ${lesson}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `Daily recap\n${winsBlock}\nLesson: ${lesson}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
formatSocialPost
|
|
22
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function loadState(filePath) {
|
|
5
|
+
if (!fs.existsSync(filePath)) {
|
|
6
|
+
return { days: {} };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function saveState(filePath, state) {
|
|
13
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
14
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDayState(state, dateKey) {
|
|
18
|
+
if (!state.days[dateKey]) {
|
|
19
|
+
state.days[dateKey] = {
|
|
20
|
+
tasks: [],
|
|
21
|
+
rescueSprintsUsed: 0,
|
|
22
|
+
notes: {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return state.days[dateKey];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
loadState,
|
|
31
|
+
saveState,
|
|
32
|
+
ensureDayState
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function buildWeeklyReport(state) {
|
|
2
|
+
const days = state.days || {};
|
|
3
|
+
const tasks = Object.values(days).flatMap((day) => day.tasks || []);
|
|
4
|
+
const doneCount = tasks.filter((task) => task.status === 'done').length;
|
|
5
|
+
const blockedCount = tasks.filter((task) => task.status === 'blocked').length;
|
|
6
|
+
const totalCount = tasks.length;
|
|
7
|
+
const completionRate = totalCount > 0
|
|
8
|
+
? `${Math.round((doneCount / totalCount) * 100)}%`
|
|
9
|
+
: '0%';
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
completionRate,
|
|
13
|
+
summary: `Done: ${doneCount}, Blocked: ${blockedCount}, Total: ${totalCount}`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
buildWeeklyReport
|
|
19
|
+
};
|