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.
@@ -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,7 @@
1
+ function renderTemplate(template, vars) {
2
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => vars[key] ?? '');
3
+ }
4
+
5
+ module.exports = {
6
+ renderTemplate
7
+ };
@@ -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
+ };
@@ -0,0 +1,7 @@
1
+ Evening review with {{assistant_name}}.
2
+
3
+ Completed: {{done_count}} / {{total_count}}
4
+ Wins: {{wins}}
5
+ Lesson: {{lesson}}
6
+
7
+ Tomorrow first action: {{tomorrow_first_action}}
@@ -0,0 +1,8 @@
1
+ Midday check-in from {{assistant_name}}.
2
+
3
+ Status:
4
+ - Task 1: {{status_1}}
5
+ - Task 2: {{status_2}}
6
+ - Task 3: {{status_3}}
7
+
8
+ If blocked, pick one unblock action and run a 25-minute focus sprint.