@masslessai/push-todo 3.8.0 → 3.8.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/lib/cli.js +1 -1
- package/lib/connect.js +1 -1
- package/lib/daemon.js +67 -8
- package/package.json +1 -1
- package/scripts/postinstall.js +54 -35
- package/scripts/preuninstall.js +26 -20
package/lib/cli.js
CHANGED
|
@@ -85,7 +85,7 @@ ${bold('EXAMPLES:')}
|
|
|
85
85
|
|
|
86
86
|
${bold('CONNECT OPTIONS:')}
|
|
87
87
|
--reauth Force re-authentication
|
|
88
|
-
--client <type> Client type (claude-code, openai-codex,
|
|
88
|
+
--client <type> Client type (claude-code, openai-codex, openclaw)
|
|
89
89
|
--check-version Check for updates (JSON output)
|
|
90
90
|
--update Update to latest version
|
|
91
91
|
--validate-key Validate API key (JSON output)
|
package/lib/connect.js
CHANGED
|
@@ -55,7 +55,7 @@ const VERSION = getVersion();
|
|
|
55
55
|
const CLIENT_NAMES = {
|
|
56
56
|
'claude-code': 'Claude Code',
|
|
57
57
|
'openai-codex': 'OpenAI Codex',
|
|
58
|
-
'
|
|
58
|
+
'openclaw': 'OpenClaw'
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// ============================================================================
|
package/lib/daemon.js
CHANGED
|
@@ -901,7 +901,7 @@ async function markTaskAsCompleted(displayNumber, taskId, comment) {
|
|
|
901
901
|
* Checks for existing branch commits and PRs to avoid redundant re-execution.
|
|
902
902
|
* Returns true if the task was healed (status updated, no re-execution needed).
|
|
903
903
|
*/
|
|
904
|
-
async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
904
|
+
async function autoHealExistingWork(displayNumber, summary, projectPath, taskId) {
|
|
905
905
|
const suffix = getWorktreeSuffix();
|
|
906
906
|
const branch = `push-${displayNumber}-${suffix}`;
|
|
907
907
|
const gitCwd = projectPath || process.cwd();
|
|
@@ -931,7 +931,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
|
931
931
|
let prState = null;
|
|
932
932
|
try {
|
|
933
933
|
const prResult = execSync(
|
|
934
|
-
`gh pr list --head ${branch} --json url,state --jq '.[0]' 2>/dev/null`,
|
|
934
|
+
`gh pr list --head ${branch} --state all --json url,state --jq '.[0]' 2>/dev/null`,
|
|
935
935
|
{ cwd: gitCwd, timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
936
936
|
).toString().trim();
|
|
937
937
|
if (prResult) {
|
|
@@ -950,25 +950,59 @@ async function autoHealExistingWork(displayNumber, summary, projectPath) {
|
|
|
950
950
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
951
951
|
summary: executionSummary
|
|
952
952
|
});
|
|
953
|
+
|
|
954
|
+
// Auto-complete since PR is already merged
|
|
955
|
+
let status = 'session_finished';
|
|
956
|
+
if (getAutoCompleteEnabled() && taskId) {
|
|
957
|
+
const comment = `Auto-healed: PR already merged. ${prUrl}`;
|
|
958
|
+
const completed = await markTaskAsCompleted(displayNumber, taskId, comment);
|
|
959
|
+
if (completed) {
|
|
960
|
+
log(`Task #${displayNumber}: auto-completed (PR was already merged)`);
|
|
961
|
+
status = 'completed';
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
953
965
|
completedToday.push({
|
|
954
966
|
displayNumber, summary,
|
|
955
967
|
completedAt: new Date().toISOString(),
|
|
956
|
-
duration: 0, status
|
|
968
|
+
duration: 0, status, prUrl
|
|
957
969
|
});
|
|
958
970
|
return true;
|
|
959
971
|
}
|
|
960
972
|
|
|
961
973
|
if (prUrl && prState === 'OPEN') {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
974
|
+
log(`Task #${displayNumber}: PR already open (${prUrl}), attempting merge`);
|
|
975
|
+
|
|
976
|
+
// Try to merge the existing PR
|
|
977
|
+
let merged = false;
|
|
978
|
+
if (getAutoMergeEnabled()) {
|
|
979
|
+
// Clean up worktree first so gh pr merge --delete-branch can delete the local branch
|
|
980
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
981
|
+
merged = mergePRForTask(displayNumber, prUrl, projectPath);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const executionSummary = merged
|
|
985
|
+
? `Auto-healed: previous PR merged. ${prUrl}`
|
|
986
|
+
: `Auto-healed: previous execution completed. PR pending review: ${prUrl}`;
|
|
965
987
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
966
988
|
summary: executionSummary
|
|
967
989
|
});
|
|
990
|
+
|
|
991
|
+
// Auto-complete if merge succeeded
|
|
992
|
+
let status = 'session_finished';
|
|
993
|
+
if (getAutoCompleteEnabled() && merged && taskId) {
|
|
994
|
+
const comment = `Auto-healed and merged. ${prUrl}`;
|
|
995
|
+
const completed = await markTaskAsCompleted(displayNumber, taskId, comment);
|
|
996
|
+
if (completed) {
|
|
997
|
+
log(`Task #${displayNumber}: auto-completed after auto-heal merge`);
|
|
998
|
+
status = 'completed';
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
968
1002
|
completedToday.push({
|
|
969
1003
|
displayNumber, summary,
|
|
970
1004
|
completedAt: new Date().toISOString(),
|
|
971
|
-
duration: 0, status
|
|
1005
|
+
duration: 0, status, prUrl
|
|
972
1006
|
});
|
|
973
1007
|
return true;
|
|
974
1008
|
}
|
|
@@ -1213,7 +1247,8 @@ async function executeTask(task) {
|
|
|
1213
1247
|
}
|
|
1214
1248
|
|
|
1215
1249
|
// Auto-heal: check if previous execution already completed work for this task
|
|
1216
|
-
const
|
|
1250
|
+
const taskId = task.id || task.todo_id || '';
|
|
1251
|
+
const healed = await autoHealExistingWork(displayNumber, summary, projectPath, taskId);
|
|
1217
1252
|
if (healed) {
|
|
1218
1253
|
log(`Task #${displayNumber}: auto-healed from previous execution, skipping re-execution`);
|
|
1219
1254
|
return null;
|
|
@@ -1423,6 +1458,30 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1423
1458
|
merged = mergePRForTask(displayNumber, prUrl, projectPath);
|
|
1424
1459
|
}
|
|
1425
1460
|
|
|
1461
|
+
// Safety net: if no new PR was created (Claude found work already done),
|
|
1462
|
+
// check if a previous PR for this branch was already merged.
|
|
1463
|
+
// See: docs/20260211_auto_complete_failure_investigation.md (Fix D)
|
|
1464
|
+
if (!prUrl && !merged) {
|
|
1465
|
+
const suffix = getWorktreeSuffix();
|
|
1466
|
+
const branch = `push-${displayNumber}-${suffix}`;
|
|
1467
|
+
try {
|
|
1468
|
+
const prCheck = execFileSync('gh', [
|
|
1469
|
+
'pr', 'list', '--head', branch, '--state', 'merged',
|
|
1470
|
+
'--json', 'url', '--jq', '.[0].url'
|
|
1471
|
+
], {
|
|
1472
|
+
cwd: projectPath || process.cwd(),
|
|
1473
|
+
timeout: 15000,
|
|
1474
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1475
|
+
}).toString().trim();
|
|
1476
|
+
if (prCheck) {
|
|
1477
|
+
log(`Task #${displayNumber}: found previously merged PR: ${prCheck}`);
|
|
1478
|
+
merged = true;
|
|
1479
|
+
}
|
|
1480
|
+
} catch {
|
|
1481
|
+
// gh not available or no merged PR found
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1426
1485
|
// Auto-complete task after successful merge (configurable, default ON)
|
|
1427
1486
|
const taskId = info.taskId;
|
|
1428
1487
|
if (getAutoCompleteEnabled() && merged && taskId) {
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Sets up integrations for ALL detected AI coding clients:
|
|
6
6
|
* 1. Claude Code - symlink to ~/.claude/skills/ (gives clean /push-todo command)
|
|
7
7
|
* 2. OpenAI Codex - AGENTS.md in ~/.codex/
|
|
8
|
-
* 3.
|
|
8
|
+
* 3. OpenClaw - SKILL.md in ~/.openclaw/skills/ (legacy: ~/.clawdbot/)
|
|
9
9
|
* 4. Downloads native keychain helper binary (macOS)
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -35,10 +35,10 @@ const CODEX_SKILL_DIR = join(CODEX_DIR, 'skills', 'push-todo');
|
|
|
35
35
|
const CODEX_SKILL_FILE = join(CODEX_SKILL_DIR, 'SKILL.md');
|
|
36
36
|
const CODEX_AGENTS_FILE = join(CODEX_DIR, 'AGENTS.md');
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
38
|
+
// OpenClaw locations (formerly Clawdbot)
|
|
39
|
+
const OPENCLAW_DIR = join(homedir(), '.openclaw');
|
|
40
|
+
const OPENCLAW_LEGACY_DIR = join(homedir(), '.clawdbot');
|
|
41
|
+
const OPENCLAW_SKILL_DIR = join(OPENCLAW_DIR, 'skills', 'push-todo');
|
|
42
42
|
|
|
43
43
|
const BINARY_NAME = 'push-keychain-helper';
|
|
44
44
|
const BINARY_DIR = join(__dirname, '../bin');
|
|
@@ -234,50 +234,69 @@ At the start of each session, check for pending tasks from the Push iOS app:
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
* Set up
|
|
238
|
-
* Creates symlink ~/.
|
|
237
|
+
* Set up OpenClaw integration (formerly Clawdbot).
|
|
238
|
+
* Creates symlink ~/.openclaw/skills/push-todo -> npm package
|
|
239
|
+
* Also cleans up legacy ~/.clawdbot/skills/push-todo if present.
|
|
239
240
|
*
|
|
240
|
-
* @returns {boolean} True if
|
|
241
|
+
* @returns {boolean} True if OpenClaw was detected and set up
|
|
241
242
|
*/
|
|
242
|
-
function
|
|
243
|
-
//
|
|
244
|
-
|
|
243
|
+
function setupOpenClaw() {
|
|
244
|
+
// Detect OpenClaw (new) or Clawdbot (legacy)
|
|
245
|
+
const detected = existsSync(OPENCLAW_DIR) || existsSync(OPENCLAW_LEGACY_DIR);
|
|
246
|
+
if (!detected) {
|
|
245
247
|
return false;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
// Prefer ~/.openclaw/ (new); fall back to ~/.clawdbot/ (legacy)
|
|
251
|
+
const targetDir = existsSync(OPENCLAW_DIR) ? OPENCLAW_DIR : OPENCLAW_LEGACY_DIR;
|
|
252
|
+
const skillDir = join(targetDir, 'skills', 'push-todo');
|
|
253
|
+
const label = existsSync(OPENCLAW_DIR) ? 'OpenClaw' : 'OpenClaw (legacy ~/.clawdbot)';
|
|
254
|
+
|
|
255
|
+
console.log(`[push-todo] Detected ${label} installation`);
|
|
249
256
|
|
|
250
257
|
try {
|
|
251
258
|
// Ensure skills directory exists
|
|
252
|
-
|
|
253
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
259
|
+
mkdirSync(join(targetDir, 'skills'), { recursive: true });
|
|
254
260
|
|
|
255
261
|
// Create symlink (same as Claude Code approach)
|
|
256
|
-
if (existsSync(
|
|
257
|
-
const stats = lstatSync(
|
|
262
|
+
if (existsSync(skillDir)) {
|
|
263
|
+
const stats = lstatSync(skillDir);
|
|
258
264
|
if (stats.isSymbolicLink()) {
|
|
259
|
-
const target = readlinkSync(
|
|
265
|
+
const target = readlinkSync(skillDir);
|
|
260
266
|
if (target === PACKAGE_ROOT) {
|
|
261
|
-
console.log('[push-todo]
|
|
267
|
+
console.log('[push-todo] OpenClaw: Symlink already configured');
|
|
262
268
|
} else {
|
|
263
|
-
unlinkSync(
|
|
264
|
-
symlinkSync(PACKAGE_ROOT,
|
|
265
|
-
console.log('[push-todo]
|
|
269
|
+
unlinkSync(skillDir);
|
|
270
|
+
symlinkSync(PACKAGE_ROOT, skillDir);
|
|
271
|
+
console.log('[push-todo] OpenClaw: Updated symlink');
|
|
266
272
|
}
|
|
267
273
|
} else {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.log('[push-todo] Clawdbot: Replaced copy with symlink');
|
|
274
|
+
rmSync(skillDir, { recursive: true });
|
|
275
|
+
symlinkSync(PACKAGE_ROOT, skillDir);
|
|
276
|
+
console.log('[push-todo] OpenClaw: Replaced copy with symlink');
|
|
272
277
|
}
|
|
273
278
|
} else {
|
|
274
|
-
symlinkSync(PACKAGE_ROOT,
|
|
275
|
-
console.log('[push-todo]
|
|
279
|
+
symlinkSync(PACKAGE_ROOT, skillDir);
|
|
280
|
+
console.log('[push-todo] OpenClaw: Created symlink');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Clean up legacy Clawdbot symlink if we installed to the new location
|
|
284
|
+
if (existsSync(OPENCLAW_DIR) && existsSync(OPENCLAW_LEGACY_DIR)) {
|
|
285
|
+
const legacySkill = join(OPENCLAW_LEGACY_DIR, 'skills', 'push-todo');
|
|
286
|
+
if (existsSync(legacySkill)) {
|
|
287
|
+
try {
|
|
288
|
+
const stats = lstatSync(legacySkill);
|
|
289
|
+
if (stats.isSymbolicLink()) {
|
|
290
|
+
unlinkSync(legacySkill);
|
|
291
|
+
console.log('[push-todo] OpenClaw: Cleaned up legacy ~/.clawdbot symlink');
|
|
292
|
+
}
|
|
293
|
+
} catch { /* best-effort */ }
|
|
294
|
+
}
|
|
276
295
|
}
|
|
277
296
|
|
|
278
297
|
return true;
|
|
279
298
|
} catch (error) {
|
|
280
|
-
console.log(`[push-todo]
|
|
299
|
+
console.log(`[push-todo] OpenClaw: Setup failed: ${error.message}`);
|
|
281
300
|
return false;
|
|
282
301
|
}
|
|
283
302
|
}
|
|
@@ -343,15 +362,15 @@ async function main() {
|
|
|
343
362
|
const codexSuccess = setupCodex();
|
|
344
363
|
if (codexSuccess) console.log('');
|
|
345
364
|
|
|
346
|
-
// Step 5: Set up
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
365
|
+
// Step 5: Set up OpenClaw (if installed — formerly Clawdbot)
|
|
366
|
+
const openclawSuccess = setupOpenClaw();
|
|
367
|
+
if (openclawSuccess) console.log('');
|
|
349
368
|
|
|
350
369
|
// Track which clients were set up
|
|
351
370
|
const clients = [];
|
|
352
371
|
if (claudeSuccess) clients.push('Claude Code');
|
|
353
372
|
if (codexSuccess) clients.push('OpenAI Codex');
|
|
354
|
-
if (
|
|
373
|
+
if (openclawSuccess) clients.push('OpenClaw');
|
|
355
374
|
|
|
356
375
|
// Step 6: Download native binary (macOS only)
|
|
357
376
|
if (platform() !== 'darwin') {
|
|
@@ -392,7 +411,7 @@ async function main() {
|
|
|
392
411
|
console.log('[push-todo] push-todo List your tasks');
|
|
393
412
|
if (claudeSuccess) console.log('[push-todo] /push-todo Use in Claude Code');
|
|
394
413
|
if (codexSuccess) console.log('[push-todo] $push-todo Use in OpenAI Codex');
|
|
395
|
-
if (
|
|
414
|
+
if (openclawSuccess) console.log('[push-todo] /push-todo Use in OpenClaw');
|
|
396
415
|
return;
|
|
397
416
|
}
|
|
398
417
|
|
|
@@ -429,8 +448,8 @@ async function main() {
|
|
|
429
448
|
if (codexSuccess) {
|
|
430
449
|
console.log('[push-todo] $push-todo Use in OpenAI Codex');
|
|
431
450
|
}
|
|
432
|
-
if (
|
|
433
|
-
console.log('[push-todo] /push-todo Use in
|
|
451
|
+
if (openclawSuccess) {
|
|
452
|
+
console.log('[push-todo] /push-todo Use in OpenClaw');
|
|
434
453
|
}
|
|
435
454
|
}
|
|
436
455
|
|
package/scripts/preuninstall.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Pre-uninstall script for Push CLI.
|
|
4
4
|
*
|
|
5
|
-
* Removes
|
|
5
|
+
* Removes skill symlinks for all detected AI coding clients:
|
|
6
|
+
* 1. Claude Code - ~/.claude/skills/push-todo
|
|
7
|
+
* 2. OpenAI Codex - ~/.codex/skills/push-todo
|
|
8
|
+
* 3. OpenClaw - ~/.openclaw/skills/push-todo (and legacy ~/.clawdbot/)
|
|
9
|
+
* 4. Legacy plugin - ~/.claude/plugins/push-todo
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { existsSync, unlinkSync, lstatSync, readlinkSync } from 'fs';
|
|
@@ -16,37 +20,37 @@ const __dirname = dirname(__filename);
|
|
|
16
20
|
// Package root (one level up from scripts/)
|
|
17
21
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
18
22
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
23
|
+
// All symlink locations to clean up
|
|
24
|
+
const SYMLINKS = [
|
|
25
|
+
{ path: join(homedir(), '.claude', 'skills', 'push-todo'), label: 'Claude Code skill' },
|
|
26
|
+
{ path: join(homedir(), '.claude', 'plugins', 'push-todo'), label: 'Claude Code plugin (legacy)' },
|
|
27
|
+
{ path: join(homedir(), '.codex', 'skills', 'push-todo'), label: 'OpenAI Codex skill' },
|
|
28
|
+
{ path: join(homedir(), '.openclaw', 'skills', 'push-todo'), label: 'OpenClaw skill' },
|
|
29
|
+
{ path: join(homedir(), '.clawdbot', 'skills', 'push-todo'), label: 'OpenClaw skill (legacy ~/.clawdbot)' },
|
|
30
|
+
];
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
|
-
* Remove
|
|
33
|
+
* Remove a symlink if it points to this package.
|
|
24
34
|
*/
|
|
25
|
-
function
|
|
26
|
-
if (!existsSync(
|
|
27
|
-
console.log('[push-todo] No plugin symlink found, nothing to remove.');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
35
|
+
function removeSymlink({ path, label }) {
|
|
36
|
+
if (!existsSync(path)) return;
|
|
30
37
|
|
|
31
38
|
try {
|
|
32
|
-
const stats = lstatSync(
|
|
33
|
-
|
|
39
|
+
const stats = lstatSync(path);
|
|
34
40
|
if (!stats.isSymbolicLink()) {
|
|
35
|
-
console.log(
|
|
41
|
+
console.log(`[push-todo] ${label} is not a symlink, leaving it alone.`);
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
const target = readlinkSync(
|
|
40
|
-
|
|
41
|
-
// Only remove if it points to this package
|
|
45
|
+
const target = readlinkSync(path);
|
|
42
46
|
if (target === PACKAGE_ROOT || target.includes('node_modules/@masslessai/push-todo')) {
|
|
43
|
-
unlinkSync(
|
|
44
|
-
console.log(
|
|
47
|
+
unlinkSync(path);
|
|
48
|
+
console.log(`[push-todo] Removed ${label} symlink.`);
|
|
45
49
|
} else {
|
|
46
|
-
console.log(
|
|
50
|
+
console.log(`[push-todo] ${label} symlink points elsewhere, leaving it alone.`);
|
|
47
51
|
}
|
|
48
52
|
} catch (error) {
|
|
49
|
-
console.error(`[push-todo] Warning: Could not remove
|
|
53
|
+
console.error(`[push-todo] Warning: Could not remove ${label}: ${error.message}`);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -56,7 +60,9 @@ function removePluginSymlink() {
|
|
|
56
60
|
function main() {
|
|
57
61
|
console.log('[push-todo] Running pre-uninstall...');
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
for (const link of SYMLINKS) {
|
|
64
|
+
removeSymlink(link);
|
|
65
|
+
}
|
|
60
66
|
|
|
61
67
|
console.log('[push-todo] Uninstall cleanup complete.');
|
|
62
68
|
console.log('[push-todo] Your configuration at ~/.config/push/ has been preserved.');
|