supipowers 0.7.7 → 0.7.9
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/bin/ctx-mode-wrapper.mjs +5 -34
- package/package.json +1 -1
- package/src/commands/run.ts +71 -4
- package/src/storage/plans.ts +48 -10
- package/src/types.ts +1 -1
package/bin/ctx-mode-wrapper.mjs
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
// rules since that's what OMP actually loads as system prompt.
|
|
11
11
|
|
|
12
12
|
import { resolve, join, dirname } from "node:path";
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import os from "node:os";
|
|
17
16
|
|
|
18
17
|
const __wrapperDir = dirname(fileURLToPath(import.meta.url));
|
|
19
18
|
|
|
@@ -21,38 +20,10 @@ const __wrapperDir = dirname(fileURLToPath(import.meta.url));
|
|
|
21
20
|
const projectDir = resolve(process.cwd());
|
|
22
21
|
process.env.CLAUDE_PROJECT_DIR = projectDir;
|
|
23
22
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// content persists across sessions (context-mode creates per-PID filenames).
|
|
29
|
-
const ctxDbDir = join(projectDir, ".omp", "context-mode");
|
|
30
|
-
if (!existsSync(ctxDbDir)) mkdirSync(ctxDbDir, { recursive: true });
|
|
31
|
-
|
|
32
|
-
// Find the most recent existing DB and copy it for the new session
|
|
33
|
-
const currentDbName = `context-mode-${process.pid}.db`;
|
|
34
|
-
try {
|
|
35
|
-
const existing = readdirSync(ctxDbDir)
|
|
36
|
-
.filter(f => f.match(/^context-mode-\d+\.db$/) && f !== currentDbName);
|
|
37
|
-
if (existing.length > 0) {
|
|
38
|
-
// Pick the newest by mtime
|
|
39
|
-
const newest = existing
|
|
40
|
-
.map(f => ({ name: f, mtime: statSync(join(ctxDbDir, f)).mtimeMs }))
|
|
41
|
-
.sort((a, b) => b.mtime - a.mtime)[0];
|
|
42
|
-
if (newest) {
|
|
43
|
-
copyFileSync(join(ctxDbDir, newest.name), join(ctxDbDir, currentDbName));
|
|
44
|
-
// Also copy WAL/SHM if they exist
|
|
45
|
-
for (const suffix of ["-wal", "-shm"]) {
|
|
46
|
-
const src = join(ctxDbDir, newest.name + suffix);
|
|
47
|
-
if (existsSync(src)) {
|
|
48
|
-
copyFileSync(src, join(ctxDbDir, currentDbName + suffix));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch { /* best effort */ }
|
|
54
|
-
|
|
55
|
-
os.tmpdir = () => ctxDbDir;
|
|
23
|
+
// Note: context-mode uses per-PID ephemeral FTS5 databases that are cleaned
|
|
24
|
+
// up on process exit. Within a session, indexed content persists and ctx_search
|
|
25
|
+
// can query it. Across sessions, content must be re-indexed.
|
|
26
|
+
// The SKILL.md routing rules emphasize using ctx_search for follow-ups.
|
|
56
27
|
|
|
57
28
|
// Resolve start.mjs path from the first CLI argument
|
|
58
29
|
const startMjs = process.argv[2];
|
package/package.json
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -28,16 +28,76 @@ import { buildBranchFinishPrompt } from "../git/branch-finish.js";
|
|
|
28
28
|
import { detectBaseBranch } from "../git/base-branch.js";
|
|
29
29
|
import type { RunManifest, AgentResult } from "../types.js";
|
|
30
30
|
|
|
31
|
+
interface ParsedRunArgs {
|
|
32
|
+
profile?: string;
|
|
33
|
+
plan?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseRunArgs(args: string | undefined): ParsedRunArgs {
|
|
37
|
+
if (!args) return {};
|
|
38
|
+
const result: ParsedRunArgs = {};
|
|
39
|
+
const profileMatch = args.match(/--profile\s+(\S+)/);
|
|
40
|
+
if (profileMatch && !profileMatch[1].startsWith("--")) {
|
|
41
|
+
result.profile = profileMatch[1];
|
|
42
|
+
}
|
|
43
|
+
const planMatch = args.match(/--plan\s+(\S+)/);
|
|
44
|
+
if (planMatch && !planMatch[1].startsWith("--")) {
|
|
45
|
+
result.plan = planMatch[1];
|
|
46
|
+
}
|
|
47
|
+
// If no flags were matched, treat the whole string as a plan name (backwards compat)
|
|
48
|
+
if (!result.profile && !result.plan) {
|
|
49
|
+
const trimmed = args.trim();
|
|
50
|
+
if (trimmed && !trimmed.startsWith("--")) result.plan = trimmed;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatAge(isoDate: string): string {
|
|
56
|
+
const ms = Math.max(0, Date.now() - new Date(isoDate).getTime());
|
|
57
|
+
if (Number.isNaN(ms)) return "unknown";
|
|
58
|
+
const mins = Math.floor(ms / 60_000);
|
|
59
|
+
if (mins < 60) return `${mins}m`;
|
|
60
|
+
const hours = Math.floor(mins / 60);
|
|
61
|
+
if (hours < 24) return `${hours}h ${mins % 60}m`;
|
|
62
|
+
return `${Math.floor(hours / 24)}d ${hours % 24}h`;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
export function registerRunCommand(pi: ExtensionAPI): void {
|
|
32
66
|
pi.registerCommand("supi:run", {
|
|
33
67
|
description: "Execute a plan with sub-agent orchestration",
|
|
34
68
|
async handler(args, ctx) {
|
|
35
69
|
const config = loadConfig(ctx.cwd);
|
|
36
|
-
const
|
|
70
|
+
const parsed = parseRunArgs(args);
|
|
71
|
+
const profile = resolveProfile(ctx.cwd, config, parsed.profile);
|
|
37
72
|
|
|
38
73
|
let manifest = findActiveRun(ctx.cwd);
|
|
39
74
|
let branchName: string | null = null;
|
|
40
75
|
|
|
76
|
+
// Handle active run: prompt user to resume or start fresh
|
|
77
|
+
if (manifest) {
|
|
78
|
+
if (ctx.hasUI) {
|
|
79
|
+
const age = formatAge(manifest.startedAt);
|
|
80
|
+
const choice = await ctx.ui.select(
|
|
81
|
+
`Found active run ${manifest.id} for plan '${manifest.planRef}' (started ${age} ago)`,
|
|
82
|
+
["Resume", "Start fresh"],
|
|
83
|
+
);
|
|
84
|
+
if (!choice) return;
|
|
85
|
+
|
|
86
|
+
if (choice === "Start fresh") {
|
|
87
|
+
manifest.status = "cancelled";
|
|
88
|
+
manifest.completedAt = new Date().toISOString();
|
|
89
|
+
updateRun(ctx.cwd, manifest);
|
|
90
|
+
manifest = null;
|
|
91
|
+
} else {
|
|
92
|
+
notifyInfo(ctx, `Resuming run: ${manifest.id}`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// No UI — resume automatically
|
|
96
|
+
notifyInfo(ctx, `Resuming run: ${manifest.id}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create a new run if no active run or user chose "Start fresh"
|
|
41
101
|
if (!manifest) {
|
|
42
102
|
const plans = listPlans(ctx.cwd);
|
|
43
103
|
if (plans.length === 0) {
|
|
@@ -45,7 +105,8 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
45
105
|
return;
|
|
46
106
|
}
|
|
47
107
|
|
|
48
|
-
const planName =
|
|
108
|
+
const planName = parsed.plan || plans[0];
|
|
109
|
+
notifyInfo(ctx, "Using plan", planName);
|
|
49
110
|
const planContent = readPlanFile(ctx.cwd, planName);
|
|
50
111
|
if (!planContent) {
|
|
51
112
|
notifyError(ctx, "Plan not found", planName);
|
|
@@ -53,6 +114,14 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
53
114
|
}
|
|
54
115
|
|
|
55
116
|
const plan = parsePlan(planContent, planName);
|
|
117
|
+
if (plan.tasks.length === 0) {
|
|
118
|
+
notifyError(
|
|
119
|
+
ctx,
|
|
120
|
+
"No tasks found in plan",
|
|
121
|
+
"Task headers must use '### N. Name' or '### Task N: Name' format",
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
56
125
|
const batches = scheduleBatches(plan.tasks, config.orchestration.maxParallelAgents);
|
|
57
126
|
|
|
58
127
|
manifest = {
|
|
@@ -95,8 +164,6 @@ export function registerRunCommand(pi: ExtensionAPI): void {
|
|
|
95
164
|
notifyInfo(ctx, "Setting up worktree", `Branch: ${branchName}`);
|
|
96
165
|
}
|
|
97
166
|
}
|
|
98
|
-
} else {
|
|
99
|
-
notifyInfo(ctx, `Resuming run: ${manifest.id}`);
|
|
100
167
|
}
|
|
101
168
|
|
|
102
169
|
const planContent = readPlanFile(ctx.cwd, manifest.planRef);
|
package/src/storage/plans.ts
CHANGED
|
@@ -74,20 +74,28 @@ function extractContext(content: string): string {
|
|
|
74
74
|
return contextMatch?.[1]?.trim() ?? "";
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Strip fenced code blocks to prevent matching task headers inside examples */
|
|
78
|
+
function stripCodeBlocks(content: string): string {
|
|
79
|
+
return content.replace(/```[\s\S]*?```/g, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
function parseTasksFromMarkdown(content: string): PlanTask[] {
|
|
78
83
|
const tasks: PlanTask[] = [];
|
|
79
|
-
|
|
84
|
+
// Strip code blocks so we don't match headers inside examples
|
|
85
|
+
const stripped = stripCodeBlocks(content);
|
|
86
|
+
// Match both "### 1. Name" and "### Task 1: Name" formats, anchored to line start
|
|
87
|
+
const taskRegex = /^### (?:Task )?(\d+)[.:] (.+)/gm;
|
|
80
88
|
let match: RegExpExecArray | null;
|
|
81
89
|
|
|
82
|
-
while ((match = taskRegex.exec(
|
|
90
|
+
while ((match = taskRegex.exec(stripped)) !== null) {
|
|
83
91
|
const id = parseInt(match[1], 10);
|
|
84
92
|
const headerLine = match[2];
|
|
85
93
|
const startIdx = match.index + match[0].length;
|
|
86
|
-
const nextTaskMatch = /\n###
|
|
94
|
+
const nextTaskMatch = /\n### (?:Task )?\d+[.:] /.exec(stripped.slice(startIdx));
|
|
87
95
|
const endIdx = nextTaskMatch
|
|
88
96
|
? startIdx + nextTaskMatch.index
|
|
89
|
-
:
|
|
90
|
-
const body =
|
|
97
|
+
: stripped.length;
|
|
98
|
+
const body = stripped.slice(startIdx, endIdx);
|
|
91
99
|
|
|
92
100
|
const name = headerLine.replace(/\[.*?\]/g, "").trim();
|
|
93
101
|
const parallelism = parseParallelism(headerLine);
|
|
@@ -111,18 +119,48 @@ function parseParallelism(header: string): TaskParallelism {
|
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
function parseFiles(body: string): string[] {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
// Match header only (no greedy \s* that eats newlines)
|
|
123
|
+
// Supports: **files**: ..., **File**: ..., **Files:** ...
|
|
124
|
+
const headerMatch = body.match(/\*\*files?\*\*:/i) ?? body.match(/\*\*files?:\*\*/i);
|
|
125
|
+
if (!headerMatch) return [];
|
|
126
|
+
|
|
127
|
+
const afterHeader = body.slice(headerMatch.index! + headerMatch[0].length);
|
|
128
|
+
const firstLine = afterHeader.split("\n")[0].trim();
|
|
129
|
+
|
|
130
|
+
// Single-line format: **files**: src/a.ts, src/b.ts
|
|
131
|
+
if (firstLine && !firstLine.startsWith("-")) {
|
|
132
|
+
return firstLine.split(",").map((s) => s.trim());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Multi-line format: **Files:**\n- Modify: `src/a.ts`\n- Create: `src/b.ts`
|
|
136
|
+
const lines = afterHeader.split("\n");
|
|
137
|
+
const files: string[] = [];
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed.startsWith("-")) {
|
|
141
|
+
if (trimmed === "") continue; // skip blank lines between header and list
|
|
142
|
+
break; // non-list line = end of files section
|
|
143
|
+
}
|
|
144
|
+
// Strip "- Modify: `src/types.ts`" → "src/types.ts"
|
|
145
|
+
const content = trimmed.slice(1).trim(); // remove leading -
|
|
146
|
+
const afterPrefix = content.replace(/^(?:Modify|Create|Test|Delete|Rename|Move):\s*/i, "");
|
|
147
|
+
const filePath = afterPrefix.replace(/`/g, "").replace(/\s*\(.*\)\s*$/, "").trim();
|
|
148
|
+
if (filePath && !filePath.startsWith("(")) {
|
|
149
|
+
files.push(filePath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return files;
|
|
117
153
|
}
|
|
118
154
|
|
|
119
155
|
function parseCriteria(body: string): string {
|
|
120
|
-
|
|
156
|
+
// Match both **criteria**: ... and **Criteria:** ...
|
|
157
|
+
const match = body.match(/\*\*criteria\*\*:\s*(.+)/i) ?? body.match(/\*\*criteria:\*\*\s*(.+)/i);
|
|
121
158
|
return match?.[1]?.trim() ?? "";
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
function parseComplexity(body: string): TaskComplexity {
|
|
125
|
-
|
|
162
|
+
// Match both **complexity**: ... and **Complexity:** ...
|
|
163
|
+
const match = body.match(/\*\*complexity\*\*:\s*(\w+)/i) ?? body.match(/\*\*complexity:\*\*\s*(\w+)/i);
|
|
126
164
|
const val = match?.[1]?.toLowerCase();
|
|
127
165
|
if (val === "small" || val === "medium" || val === "large") return val;
|
|
128
166
|
return "medium";
|
package/src/types.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface RunBatch {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Overall run status */
|
|
56
|
-
export type RunStatus = "running" | "completed" | "paused" | "failed";
|
|
56
|
+
export type RunStatus = "running" | "completed" | "paused" | "failed" | "cancelled";
|
|
57
57
|
|
|
58
58
|
/** Run manifest stored on disk */
|
|
59
59
|
export interface RunManifest {
|