@difflab/difflab-cli 0.4.0 → 0.6.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 +23 -2
- package/dist/index.js +260 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,17 @@ Clients are selected explicitly. Supported names: `pi`, `cursor`, `codex`, `clau
|
|
|
46
46
|
|
|
47
47
|
MCP clients launch `npx -y @difflab/difflab-cli mcp serve` over stdio, so no daemon or port is needed. Bun must be on the client's PATH.
|
|
48
48
|
|
|
49
|
+
## MCP todo tools
|
|
50
|
+
|
|
51
|
+
`mcp setup` does **not** create a todo file. Use `todo_init` to create a list. Each todo call requires an absolute `cwd` pointing to an existing directory and a relative `path` to a JSON todo file inside it. For example:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
todo_init({"cwd":"/home/alex","path":"todos.json"})
|
|
55
|
+
todo_add({"cwd":"/home/alex","path":"todos.json","text":"Review PR"})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`todo_list`, `todo_complete`, and `todo_remove` also require both fields; the latter two require a task `id` returned by `todo_list` or `todo_add`. Call `todo_init` before other operations on a new file. Parent traversal outside `cwd`, absolute `path` values, and symlinked paths are rejected. Keep personal todo files outside the repository or add their paths to `.gitignore`. Concurrent writes to the same file are not yet coordinated. Agents can load `skills/difflab-todo/SKILL.md` for usage instructions.
|
|
59
|
+
|
|
49
60
|
## Document templates
|
|
50
61
|
|
|
51
62
|
List the available templates or scaffold a new document from one:
|
|
@@ -55,9 +66,19 @@ difflab-cli templates list
|
|
|
55
66
|
difflab-cli templates scaffold spec-driven-plan docs/plans first-plan.md --cwd /absolute/project/path
|
|
56
67
|
```
|
|
57
68
|
|
|
58
|
-
The CLI defaults `--cwd` to its current directory. The MCP server exposes `template_list({})` and `scaffold({"name":"spec-driven-plan","cwd":"/absolute/project/path","path":"docs/plans","filename":"first-plan.md"})`. Both interfaces take a hyphenated template name, a relative destination **directory** (`path`, or `.` for `cwd`), and a separate **filename**. MCP always requires an absolute existing `cwd`. Missing destination directories are created. Available names: `spec-driven-plan`, `software-architecture-design`, `architecture-decision-record`, `product-requirements-document`, `code-review`, `planning-intent`, `ui-component-architecture`,
|
|
69
|
+
The CLI defaults `--cwd` to its current directory. The MCP server exposes `template_list({})` and `scaffold({"name":"spec-driven-plan","cwd":"/absolute/project/path","path":"docs/plans","filename":"first-plan.md"})`. Both interfaces take a hyphenated template name, a relative destination **directory** (`path`, or `.` for `cwd`), and a separate **filename**. MCP always requires an absolute existing `cwd`. Missing destination directories are created. Available names: `spec-driven-plan`, `software-architecture-design`, `architecture-decision-record`, `product-requirements-document`, `code-review`, `planning-intent`, `ui-component-architecture`, `pull-request-description` (adapted from Diffpi's PR template), `flow-definition`, and `flow-instance`.
|
|
70
|
+
|
|
71
|
+
On first use, missing bundled templates and filled examples are copied to `~/.difflab/templates` and `~/.difflab/templates/examples`. Edit the installed templates to customize future documents. Later runs add missing files but never replace existing ones, even after an upgrade. Examples are for reference, not selectable templates. Copying is literal: prompts and placeholders are not substituted. An unknown name, missing or invalid source, unsafe path, symlink, or existing destination causes an error; destination files are never overwritten. The target directory must remain inside `cwd` and the filename must be a single file name without separators. The one symlink exception is an initialized repository's `.difflab` link to its own user project store: `templates scaffold spec-driven-plan .difflab/plans/YYMMDD-name PLAN.md --cwd /absolute/repo` works in Git worktrees as well. To restore a bundled default, move your installed copy aside before listing again.
|
|
72
|
+
|
|
73
|
+
The portable `skills/difflab-plan`, `skills/difflab-review`, and `skills/difflab-flow` directories contain natural-language agent workflows for these templates. Install each skill in your client's Agent Skills directory. The MCP-only `log_append({"path":"/absolute/physical/project-store/plans/YYMMDD-name/logs.txt","message":"Phase started"})` tool creates an append-only progress log on first use; pass the physical path, not the `.difflab` symlink. There is no log CLI command.
|
|
74
|
+
|
|
75
|
+
## Flow definitions and runs
|
|
76
|
+
|
|
77
|
+
`flow new <name>` and `flow go <name>` are Agent Skill requests, not CLI subcommands. The agent creates a definition once at `~/.difflab/flows/<name>.md`. It calls MCP `scaffold` with `cwd=<absolute home>/.difflab`, `path="flows"`, and `filename="<name>.md"`. Do not use `cwd=<home>,path=".difflab/flows"`: that route is for a repository project store. You can edit a definition before another run. A new run uses a fresh snapshot and does not alter old runs.
|
|
78
|
+
|
|
79
|
+
From an initialized repository, ask “flow go autospec with input 'Export monthly sales' --local”. The agent selects one branch and creates `.difflab/flows/YYMMDD-autospec/FLOW.md`. It keeps inputs, selected permissions, step checkboxes, and verified outputs for later steps in that file. A same-day path requires an explicit resume choice or a distinct run ID. The first unchecked step is the resume point. The physical project-store path to the sibling `logs.txt` receives timestamped entries through MCP `log_append` only. The definition remains unchanged.
|
|
59
80
|
|
|
60
|
-
|
|
81
|
+
`--local` removes remote steps before creating the run. `--commit` permits only an explicitly declared local commit step when passed, and never permits a push. Remote review needs separate permission and the review skill's forge checks. Plan and review skills still control their own operations; a flow cannot override them. Merge is not supported as a flow step. Example definitions live in `skills/difflab-flow/examples/`.
|
|
61
82
|
|
|
62
83
|
## Develop
|
|
63
84
|
|
package/dist/index.js
CHANGED
|
@@ -12796,17 +12796,17 @@ var init_registry = __esm(() => {
|
|
|
12796
12796
|
// ../../libs/difflab-ts/src/gitx.ts
|
|
12797
12797
|
import { execFileSync } from "child_process";
|
|
12798
12798
|
import { appendFile, readFile } from "fs/promises";
|
|
12799
|
-
import { isAbsolute,
|
|
12799
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
12800
12800
|
function gitRoot(cwd) {
|
|
12801
12801
|
return runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
12802
12802
|
}
|
|
12803
12803
|
function discoverGitRepository(cwd) {
|
|
12804
12804
|
const root = gitRoot(cwd);
|
|
12805
|
-
const
|
|
12805
|
+
const exclude = runGit(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
12806
12806
|
return {
|
|
12807
12807
|
root,
|
|
12808
12808
|
originUrl: runGit(root, ["remote", "get-url", "origin"]),
|
|
12809
|
-
excludePath:
|
|
12809
|
+
excludePath: isAbsolute(exclude) ? exclude : resolve2(root, exclude)
|
|
12810
12810
|
};
|
|
12811
12811
|
}
|
|
12812
12812
|
async function addPathToGitExcludes(path, entry) {
|
|
@@ -26485,9 +26485,9 @@ var init_dist3 = __esm(() => {
|
|
|
26485
26485
|
|
|
26486
26486
|
// src/projects/config.ts
|
|
26487
26487
|
import { lstat as lstat2, readFile as readFile2, writeFile } from "fs/promises";
|
|
26488
|
-
import { join
|
|
26488
|
+
import { join } from "path";
|
|
26489
26489
|
function repoConfigPath(root = process.cwd()) {
|
|
26490
|
-
return
|
|
26490
|
+
return join(root, "difflab.yaml");
|
|
26491
26491
|
}
|
|
26492
26492
|
async function readRepoConfig(root = process.cwd()) {
|
|
26493
26493
|
const path = repoConfigPath(root);
|
|
@@ -26548,7 +26548,7 @@ var init_config = __esm(() => {
|
|
|
26548
26548
|
import { realpathSync } from "fs";
|
|
26549
26549
|
import { lstat as lstat3, mkdir } from "fs/promises";
|
|
26550
26550
|
import { homedir } from "os";
|
|
26551
|
-
import { join as
|
|
26551
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
26552
26552
|
function userPaths(home = process.env.HOME || homedir()) {
|
|
26553
26553
|
let tilde;
|
|
26554
26554
|
try {
|
|
@@ -26558,8 +26558,8 @@ function userPaths(home = process.env.HOME || homedir()) {
|
|
|
26558
26558
|
throw error;
|
|
26559
26559
|
tilde = resolve3(home);
|
|
26560
26560
|
}
|
|
26561
|
-
const root =
|
|
26562
|
-
return { root, projects:
|
|
26561
|
+
const root = join2(tilde, ".difflab");
|
|
26562
|
+
return { root, projects: join2(root, "projects"), database: join2(root, "difflab.sqlite") };
|
|
26563
26563
|
}
|
|
26564
26564
|
function createUserStore(home) {
|
|
26565
26565
|
return {
|
|
@@ -26645,7 +26645,7 @@ var init_osx = () => {};
|
|
|
26645
26645
|
|
|
26646
26646
|
// src/extensions/pathx.ts
|
|
26647
26647
|
import { lstat as lstat4, realpath, stat } from "fs/promises";
|
|
26648
|
-
import { isAbsolute as isAbsolute2, join as
|
|
26648
|
+
import { isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve4, sep } from "path";
|
|
26649
26649
|
async function resolvePath(cwd, path) {
|
|
26650
26650
|
if (!isAbsolute2(cwd))
|
|
26651
26651
|
throw new Error("cwd must be an absolute path to an existing directory");
|
|
@@ -26661,7 +26661,7 @@ async function resolvePath(cwd, path) {
|
|
|
26661
26661
|
}
|
|
26662
26662
|
let current = root;
|
|
26663
26663
|
for (const segment of inside.split(sep)) {
|
|
26664
|
-
current =
|
|
26664
|
+
current = join3(current, segment);
|
|
26665
26665
|
try {
|
|
26666
26666
|
if ((await lstat4(current)).isSymbolicLink()) {
|
|
26667
26667
|
throw new Error(`Symlink paths are not allowed for files: ${current}`);
|
|
@@ -26732,7 +26732,7 @@ var init_fsx = __esm(() => {
|
|
|
26732
26732
|
|
|
26733
26733
|
// src/projects/repo-store.ts
|
|
26734
26734
|
import { mkdir as mkdir3, readlink, symlink } from "fs/promises";
|
|
26735
|
-
import { dirname as dirname2, join as
|
|
26735
|
+
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
26736
26736
|
async function preflightRepoStore(root, target) {
|
|
26737
26737
|
for (const path of [root, dirname2(target)]) {
|
|
26738
26738
|
const entry = await lstatOrNull(path);
|
|
@@ -26742,7 +26742,7 @@ async function preflightRepoStore(root, target) {
|
|
|
26742
26742
|
const targetStat = await lstatOrNull(target);
|
|
26743
26743
|
if (targetStat && !targetStat.isDirectory())
|
|
26744
26744
|
throw new RepoStoreError(`Refusing existing target: ${target}`);
|
|
26745
|
-
const link =
|
|
26745
|
+
const link = join4(root, ".difflab");
|
|
26746
26746
|
const entry = await lstatOrNull(link);
|
|
26747
26747
|
if (entry) {
|
|
26748
26748
|
if (!entry.isSymbolicLink())
|
|
@@ -26752,7 +26752,7 @@ async function preflightRepoStore(root, target) {
|
|
|
26752
26752
|
}
|
|
26753
26753
|
}
|
|
26754
26754
|
async function checkRepoStore(root, target, excludePath) {
|
|
26755
|
-
const link =
|
|
26755
|
+
const link = join4(root, ".difflab");
|
|
26756
26756
|
const entry = await lstatOrNull(link);
|
|
26757
26757
|
if (!entry)
|
|
26758
26758
|
throw new RepoStoreError(`Missing .difflab symlink at ${link}`);
|
|
@@ -26776,7 +26776,7 @@ async function ensureRepoStore(root, target, excludePath) {
|
|
|
26776
26776
|
throw error;
|
|
26777
26777
|
}
|
|
26778
26778
|
}
|
|
26779
|
-
const link =
|
|
26779
|
+
const link = join4(root, ".difflab");
|
|
26780
26780
|
if (!await lstatOrNull(link)) {
|
|
26781
26781
|
try {
|
|
26782
26782
|
await symlink(target, link);
|
|
@@ -26794,7 +26794,7 @@ var init_repo_store = __esm(() => {
|
|
|
26794
26794
|
});
|
|
26795
26795
|
|
|
26796
26796
|
// src/context.ts
|
|
26797
|
-
import { join as
|
|
26797
|
+
import { join as join5 } from "path";
|
|
26798
26798
|
async function readProjectContext(cwd = process.cwd(), home) {
|
|
26799
26799
|
const git = discoverGitRepository(cwd);
|
|
26800
26800
|
const config = await readRepoConfig(git.root);
|
|
@@ -26815,7 +26815,7 @@ async function getProjectContext(git, projectId, home) {
|
|
|
26815
26815
|
const repository = project.repositories.find((item) => item.url === origin);
|
|
26816
26816
|
if (!repository)
|
|
26817
26817
|
throw new RepoStoreError("Repository is not registered in the selected project database");
|
|
26818
|
-
const target =
|
|
26818
|
+
const target = join5(userPaths(home).projects, project.id, repository.slug);
|
|
26819
26819
|
await checkRepoStore(git.root, target);
|
|
26820
26820
|
return {
|
|
26821
26821
|
project: { id: project.id, name: project.name },
|
|
@@ -48181,14 +48181,100 @@ var init_tools = __esm(() => {
|
|
|
48181
48181
|
];
|
|
48182
48182
|
});
|
|
48183
48183
|
|
|
48184
|
-
// src/
|
|
48184
|
+
// src/extensions/zodx.ts
|
|
48185
48185
|
import { isAbsolute as isAbsolute4 } from "path";
|
|
48186
|
+
var absolutePath, relativePath, fileName;
|
|
48187
|
+
var init_zodx = __esm(() => {
|
|
48188
|
+
init_zod();
|
|
48189
|
+
absolutePath = string2().refine(isAbsolute4, "path must be absolute");
|
|
48190
|
+
relativePath = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute4(value), "path must be relative");
|
|
48191
|
+
fileName = string2().refine((value) => value.trim().length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value), "filename must be a single non-empty file name");
|
|
48192
|
+
});
|
|
48193
|
+
|
|
48194
|
+
// src/logs/store.ts
|
|
48195
|
+
import { constants } from "fs";
|
|
48196
|
+
import { lstat as lstat6, open as open2, realpath as realpath2 } from "fs/promises";
|
|
48197
|
+
import { basename, dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve8 } from "path";
|
|
48198
|
+
async function appendProgressLog(path, message, now = new Date) {
|
|
48199
|
+
if (!isAbsolute5(path) || resolve8(path) !== path || basename(path) !== "logs.txt") {
|
|
48200
|
+
throw new ProgressLogError("path must be an absolute, normalized path ending in logs.txt");
|
|
48201
|
+
}
|
|
48202
|
+
if (!message.trim() || /[\r\n\v\f\0\u0085\u2028\u2029]/.test(message) || Buffer.byteLength(message, "utf8") > 2048) {
|
|
48203
|
+
throw new ProgressLogError("message must be one non-empty line of at most 2048 bytes");
|
|
48204
|
+
}
|
|
48205
|
+
const parent = dirname5(path);
|
|
48206
|
+
if (await realpath2(parent) !== parent) {
|
|
48207
|
+
throw new ProgressLogError("path must not contain symlinked directories");
|
|
48208
|
+
}
|
|
48209
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
48210
|
+
const stamp = `${pad(now.getFullYear() % 100)}${pad(now.getMonth() + 1)}${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
48211
|
+
const entry = `[${stamp}]: ${message}`;
|
|
48212
|
+
try {
|
|
48213
|
+
const existing = await lstat6(path);
|
|
48214
|
+
if (existing.isSymbolicLink())
|
|
48215
|
+
throw new ProgressLogError("logs.txt must not be a symlink");
|
|
48216
|
+
if (!existing.isFile())
|
|
48217
|
+
throw new ProgressLogError("logs.txt must be a regular file");
|
|
48218
|
+
} catch (error) {
|
|
48219
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
48220
|
+
throw error;
|
|
48221
|
+
}
|
|
48222
|
+
let handle;
|
|
48223
|
+
try {
|
|
48224
|
+
handle = await open2(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 384);
|
|
48225
|
+
} catch (error) {
|
|
48226
|
+
if (hasErrorCode(error, "ELOOP"))
|
|
48227
|
+
throw new ProgressLogError("logs.txt must not be a symlink");
|
|
48228
|
+
throw error;
|
|
48229
|
+
}
|
|
48230
|
+
try {
|
|
48231
|
+
if (!(await handle.stat()).isFile())
|
|
48232
|
+
throw new ProgressLogError("logs.txt must be a regular file");
|
|
48233
|
+
const line = Buffer.from(`${entry}
|
|
48234
|
+
`, "utf8");
|
|
48235
|
+
const { bytesWritten } = await handle.write(line);
|
|
48236
|
+
if (bytesWritten !== line.length)
|
|
48237
|
+
throw new ProgressLogError("Could not append the full log line");
|
|
48238
|
+
} finally {
|
|
48239
|
+
await handle.close();
|
|
48240
|
+
}
|
|
48241
|
+
return { path, entry };
|
|
48242
|
+
}
|
|
48243
|
+
var ProgressLogError;
|
|
48244
|
+
var init_store2 = __esm(() => {
|
|
48245
|
+
init_osx();
|
|
48246
|
+
ProgressLogError = class ProgressLogError extends Error {
|
|
48247
|
+
};
|
|
48248
|
+
});
|
|
48249
|
+
|
|
48250
|
+
// src/logs/tools.ts
|
|
48251
|
+
var logOutput, logTools;
|
|
48252
|
+
var init_tools2 = __esm(() => {
|
|
48253
|
+
init_zod();
|
|
48254
|
+
init_zodx();
|
|
48255
|
+
init_store2();
|
|
48256
|
+
logOutput = object({ result: object({ path: string2(), entry: string2() }) });
|
|
48257
|
+
logTools = [
|
|
48258
|
+
defineTool("log_append", {
|
|
48259
|
+
description: "Append one locally timestamped progress line to logs.txt at an absolute physical path. The parent directory must already exist; create the file if absent, never overwrite prior entries.",
|
|
48260
|
+
inputSchema: strictObject({
|
|
48261
|
+
path: absolutePath.describe("Absolute physical path to an existing plan directory/logs.txt"),
|
|
48262
|
+
message: string2().min(1).describe("Single-line progress message")
|
|
48263
|
+
}),
|
|
48264
|
+
outputSchema: logOutput,
|
|
48265
|
+
annotations: { openWorldHint: false }
|
|
48266
|
+
}, async ({ path, message }) => appendProgressLog(path, message), { toStructuredContent: (result) => ({ result }) })
|
|
48267
|
+
];
|
|
48268
|
+
});
|
|
48269
|
+
|
|
48270
|
+
// src/projects/tools.ts
|
|
48271
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
48186
48272
|
function createProjectTools(home) {
|
|
48187
48273
|
return [
|
|
48188
48274
|
defineTool("project_context", {
|
|
48189
48275
|
description: "Read the configured Difflab project and GitHub repository for an existing absolute cwd. Never sets up or modifies a repository.",
|
|
48190
48276
|
inputSchema: strictObject({
|
|
48191
|
-
cwd: string2().refine(
|
|
48277
|
+
cwd: string2().refine(isAbsolute6, "cwd must be an absolute existing directory")
|
|
48192
48278
|
}),
|
|
48193
48279
|
outputSchema: projectContextOutput,
|
|
48194
48280
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
@@ -48196,7 +48282,7 @@ function createProjectTools(home) {
|
|
|
48196
48282
|
];
|
|
48197
48283
|
}
|
|
48198
48284
|
var setupHint = "Use the difflab-init skill to set up this repository with the Difflab CLI.", projectContextOutput, projectTools;
|
|
48199
|
-
var
|
|
48285
|
+
var init_tools3 = __esm(() => {
|
|
48200
48286
|
init_zod();
|
|
48201
48287
|
init_context();
|
|
48202
48288
|
projectContextOutput = strictObject({
|
|
@@ -48213,16 +48299,6 @@ var init_tools2 = __esm(() => {
|
|
|
48213
48299
|
projectTools = createProjectTools();
|
|
48214
48300
|
});
|
|
48215
48301
|
|
|
48216
|
-
// src/extensions/zodx.ts
|
|
48217
|
-
import { isAbsolute as isAbsolute5 } from "path";
|
|
48218
|
-
var absolutePath, relativePath, fileName;
|
|
48219
|
-
var init_zodx = __esm(() => {
|
|
48220
|
-
init_zod();
|
|
48221
|
-
absolutePath = string2().refine(isAbsolute5, "path must be absolute");
|
|
48222
|
-
relativePath = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute5(value), "path must be relative");
|
|
48223
|
-
fileName = string2().refine((value) => value.trim().length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value), "filename must be a single non-empty file name");
|
|
48224
|
-
});
|
|
48225
|
-
|
|
48226
48302
|
// templates/spec-driven-plan.md
|
|
48227
48303
|
var spec_driven_plan_default = `# [Outcome to deliver]
|
|
48228
48304
|
|
|
@@ -48511,6 +48587,21 @@ var code_review_default = `# Review: [change or pull request]
|
|
|
48511
48587
|
|
|
48512
48588
|
[Evaluate authentication and autorization, permissions, secrets, dependencies, input validation, proper error handling, and trust boundaries. State SKIPPED if skipped with reason, for example for chores, simple scripting, pure cosmetic UI work, documentation, etc.]
|
|
48513
48589
|
|
|
48590
|
+
## Change Requests
|
|
48591
|
+
|
|
48592
|
+
Track actionable inline requests with one entry per request. End each generated request and response with its own model attribution quote; preserve human text unchanged.
|
|
48593
|
+
|
|
48594
|
+
### [file:line \u2014 request]
|
|
48595
|
+
|
|
48596
|
+
- **Status:** [open/in progress/resolved]
|
|
48597
|
+
- **Request:** [verbatim request or concise generated finding]
|
|
48598
|
+
|
|
48599
|
+
> Generated via [actual model name, only for a generated request]
|
|
48600
|
+
|
|
48601
|
+
- **Response:** [resolution, evidence, or reason this remains open]
|
|
48602
|
+
|
|
48603
|
+
> Generated via [actual model name, only for a generated response]
|
|
48604
|
+
|
|
48514
48605
|
## Findings
|
|
48515
48606
|
|
|
48516
48607
|
| Severity | Location | Evidence and impact | Required change or question |
|
|
@@ -48621,6 +48712,97 @@ var pull_request_description_default = `## Intent
|
|
|
48621
48712
|
`;
|
|
48622
48713
|
var init_pull_request_description = () => {};
|
|
48623
48714
|
|
|
48715
|
+
// templates/flow-definition.md
|
|
48716
|
+
var flow_definition_default = `# {{name}}
|
|
48717
|
+
|
|
48718
|
+
## Intent
|
|
48719
|
+
|
|
48720
|
+
{{intent}}
|
|
48721
|
+
|
|
48722
|
+
## Inputs
|
|
48723
|
+
|
|
48724
|
+
| Name | Required | Default | Description |
|
|
48725
|
+
| ----- | -------- | ------- | ------------------------------------------ |
|
|
48726
|
+
| input | yes | \u2014 | Free-form user request; bind as one value. |
|
|
48727
|
+
|
|
48728
|
+
## Flags
|
|
48729
|
+
|
|
48730
|
+
| Flag | Default | Capability | Effect |
|
|
48731
|
+
| --------------- | ------- | ------------- | ---------------------------------------------------------------------------------- |
|
|
48732
|
+
| --local | false | local | Omit steps with \`unless: --local\`; never forward to every skill. |
|
|
48733
|
+
| --commit | false | local-commit | Permit only explicitly commit-capable selected steps to make scoped local commits. |
|
|
48734
|
+
| --remote-review | false | remote-review | Opt in to remote review with independent forge authorization. |
|
|
48735
|
+
|
|
48736
|
+
## Steps
|
|
48737
|
+
|
|
48738
|
+
### 1. {{step-id}}
|
|
48739
|
+
|
|
48740
|
+
- Handler: \`plan new\`
|
|
48741
|
+
- When: always
|
|
48742
|
+
- Unless: never
|
|
48743
|
+
- Requires: local
|
|
48744
|
+
- Receives: \`{{input}}\`
|
|
48745
|
+
- Produces: \`plan_path\` (verified path to the created plan)
|
|
48746
|
+
- Instructions: {{step-instructions}}
|
|
48747
|
+
|
|
48748
|
+
### 2. {{next-step-id}}
|
|
48749
|
+
|
|
48750
|
+
- Handler: \`review new\`
|
|
48751
|
+
- When: \`--remote-review\`
|
|
48752
|
+
- Unless: \`--local\`
|
|
48753
|
+
- Requires: remote-review
|
|
48754
|
+
- Receives: \`{{steps.step-id.plan_path}}\`
|
|
48755
|
+
- Produces: \`review_path\` (verified review artifact)
|
|
48756
|
+
- Instructions: {{step-instructions}}
|
|
48757
|
+
|
|
48758
|
+
## Contract
|
|
48759
|
+
|
|
48760
|
+
Replace example rows and steps when creating a definition. Use stable unique lower-case hyphenated input and step IDs; output names may also contain underscores (\`^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$\`). Declare every input, flag, capability, output and condition. \`When\` and \`Unless\` accept only \`always\`, \`never\`, or one declared flag; no expressions, shell interpolation or implicit flags. The only tokens are \`{{input-name}}\` and \`{{steps.step-id.output-name}}\`. A later step can consume only an earlier declared output, verified when that step finishes. Define any remote or commit capability explicitly and require its independent opt-in flag. This file is reusable and has no run state; editing it affects future runs only.
|
|
48761
|
+
`;
|
|
48762
|
+
var init_flow_definition = () => {};
|
|
48763
|
+
|
|
48764
|
+
// templates/flow-instance.md
|
|
48765
|
+
var flow_instance_default = `# Flow run: {{name}}
|
|
48766
|
+
|
|
48767
|
+
- **Status:** in progress
|
|
48768
|
+
- **Definition:** {{absolute-definition-path}}
|
|
48769
|
+
- **Definition SHA-256:** {{definition-sha256}}
|
|
48770
|
+
- **Repository:** {{absolute-repository-root}}
|
|
48771
|
+
- **Run ID:** {{run-id}}
|
|
48772
|
+
|
|
48773
|
+
## Resolved inputs
|
|
48774
|
+
|
|
48775
|
+
| Input | Value |
|
|
48776
|
+
| ----- | --------- |
|
|
48777
|
+
| input | {{input}} |
|
|
48778
|
+
|
|
48779
|
+
## Selected flags and permissions
|
|
48780
|
+
|
|
48781
|
+
{{flags-and-capabilities}}
|
|
48782
|
+
|
|
48783
|
+
## Selection provenance
|
|
48784
|
+
|
|
48785
|
+
{{omitted-step-ids-and-reasons-only-no-instructions}}
|
|
48786
|
+
|
|
48787
|
+
## Steps
|
|
48788
|
+
|
|
48789
|
+
- [ ] 1. {{selected-step-id}} \u2014 {{resolved-handler}}
|
|
48790
|
+
- Requires: {{authorized-capability}}
|
|
48791
|
+
- Instructions: {{resolved-instructions}}
|
|
48792
|
+
- Receives: {{concrete-input-or-prior-step-handoff}}
|
|
48793
|
+
- Expected output: {{declared-output-name}}
|
|
48794
|
+
- Result/handoff: pending
|
|
48795
|
+
|
|
48796
|
+
## Blocker
|
|
48797
|
+
|
|
48798
|
+
None.
|
|
48799
|
+
|
|
48800
|
+
## Contract
|
|
48801
|
+
|
|
48802
|
+
This is the frozen execution snapshot. Include only selected executable steps; never copy unselected instructions here. Resolve all declared inputs before creating this file. References to earlier step outputs are typed handoff slots, filled only from verified results when those steps complete. Check a step only after success; on failure leave it unchecked, record the blocker and stop. Resume this exact instance at its first unchecked step without rereading the global definition.
|
|
48803
|
+
`;
|
|
48804
|
+
var init_flow_instance = () => {};
|
|
48805
|
+
|
|
48624
48806
|
// templates/examples/plan-feature.md
|
|
48625
48807
|
var plan_feature_default = `# Invite teammates to a project
|
|
48626
48808
|
|
|
@@ -49136,6 +49318,8 @@ var init_source = __esm(() => {
|
|
|
49136
49318
|
init_planning_intent();
|
|
49137
49319
|
init_ui_component_architecture();
|
|
49138
49320
|
init_pull_request_description();
|
|
49321
|
+
init_flow_definition();
|
|
49322
|
+
init_flow_instance();
|
|
49139
49323
|
init_plan_feature();
|
|
49140
49324
|
init_plan_migration();
|
|
49141
49325
|
init_design_service();
|
|
@@ -49154,7 +49338,9 @@ var init_source = __esm(() => {
|
|
|
49154
49338
|
{ name: "code-review", description: "Thorough code review" },
|
|
49155
49339
|
{ name: "planning-intent", description: "Planning intent for a user to fill in" },
|
|
49156
49340
|
{ name: "ui-component-architecture", description: "UI component design and implementation" },
|
|
49157
|
-
{ name: "pull-request-description", description: "Draft pull request description" }
|
|
49341
|
+
{ name: "pull-request-description", description: "Draft pull request description" },
|
|
49342
|
+
{ name: "flow-definition", description: "Reusable global Agent Skill flow definition" },
|
|
49343
|
+
{ name: "flow-instance", description: "Frozen repository-local flow run" }
|
|
49158
49344
|
];
|
|
49159
49345
|
contents = {
|
|
49160
49346
|
"spec-driven-plan.md": spec_driven_plan_default,
|
|
@@ -49165,6 +49351,8 @@ var init_source = __esm(() => {
|
|
|
49165
49351
|
"planning-intent.md": planning_intent_default,
|
|
49166
49352
|
"ui-component-architecture.md": ui_component_architecture_default,
|
|
49167
49353
|
"pull-request-description.md": pull_request_description_default,
|
|
49354
|
+
"flow-definition.md": flow_definition_default,
|
|
49355
|
+
"flow-instance.md": flow_instance_default,
|
|
49168
49356
|
"examples/plan-feature.md": plan_feature_default,
|
|
49169
49357
|
"examples/plan-migration.md": plan_migration_default,
|
|
49170
49358
|
"examples/design-service.md": design_service_default,
|
|
@@ -49193,9 +49381,9 @@ var exports_service = {};
|
|
|
49193
49381
|
__export(exports_service, {
|
|
49194
49382
|
TemplateService: () => TemplateService
|
|
49195
49383
|
});
|
|
49196
|
-
import { constants } from "fs";
|
|
49197
|
-
import { lstat as
|
|
49198
|
-
import { isAbsolute as
|
|
49384
|
+
import { constants as constants2 } from "fs";
|
|
49385
|
+
import { lstat as lstat7, open as open3, rm as rm3 } from "fs/promises";
|
|
49386
|
+
import { isAbsolute as isAbsolute7, join as join6 } from "path";
|
|
49199
49387
|
|
|
49200
49388
|
class TemplateService {
|
|
49201
49389
|
home;
|
|
@@ -49212,16 +49400,16 @@ class TemplateService {
|
|
|
49212
49400
|
if (!this.source.catalog.some((item) => item.name === name)) {
|
|
49213
49401
|
throw new UnknownTemplateError(`Unknown template: ${name}`);
|
|
49214
49402
|
}
|
|
49215
|
-
if (!path.trim() ||
|
|
49403
|
+
if (!path.trim() || isAbsolute7(path)) {
|
|
49216
49404
|
throw new TemplateError("path must be a non-empty relative directory inside cwd");
|
|
49217
49405
|
}
|
|
49218
49406
|
if (!filename.trim() || filename === "." || filename === ".." || /[/\\\0]/.test(filename)) {
|
|
49219
49407
|
throw new TemplateError("filename must be a single non-empty file name");
|
|
49220
49408
|
}
|
|
49221
49409
|
await this.installMissing();
|
|
49222
|
-
const installed = await resolvePath(this.home,
|
|
49410
|
+
const installed = await resolvePath(this.home, join6(".difflab", "templates", `${name}.md`));
|
|
49223
49411
|
const contents = await readMarkdown(installed);
|
|
49224
|
-
const destination = await ensurePathDirsExist(
|
|
49412
|
+
const destination = path.startsWith(".difflab/") ? await this.projectDestination(cwd, path.slice(".difflab/".length), filename) : await ensurePathDirsExist(join6(path, filename), cwd);
|
|
49225
49413
|
try {
|
|
49226
49414
|
await createExclusive(destination, contents);
|
|
49227
49415
|
} catch (error) {
|
|
@@ -49234,12 +49422,23 @@ class TemplateService {
|
|
|
49234
49422
|
}
|
|
49235
49423
|
return destination;
|
|
49236
49424
|
}
|
|
49425
|
+
async projectDestination(cwd, path, filename) {
|
|
49426
|
+
if (!isAbsolute7(cwd))
|
|
49427
|
+
throw new TemplateError("cwd must be an absolute path");
|
|
49428
|
+
const git = discoverGitRepository(cwd);
|
|
49429
|
+
const config = await readRepoConfig(git.root);
|
|
49430
|
+
if (!config)
|
|
49431
|
+
throw new TemplateError(`Initialize this repository with difflab init: ${git.root}`);
|
|
49432
|
+
const target = join6(userPaths(this.home).projects, config.project.id, repositorySlug(canonicalGitUrl(git.originUrl)));
|
|
49433
|
+
await checkRepoStore(git.root, target);
|
|
49434
|
+
return ensurePathDirsExist(join6(path, filename), target);
|
|
49435
|
+
}
|
|
49237
49436
|
async installMissing() {
|
|
49238
49437
|
for (const asset of this.source.assets) {
|
|
49239
|
-
const relative =
|
|
49438
|
+
const relative = join6(".difflab", "templates", asset);
|
|
49240
49439
|
const destination = await ensurePathDirsExist(relative, this.home);
|
|
49241
49440
|
try {
|
|
49242
|
-
if ((await
|
|
49441
|
+
if ((await lstat7(destination)).isSymbolicLink()) {
|
|
49243
49442
|
throw new TemplateSourceError(`Installed template asset is a symlink: ${destination}`);
|
|
49244
49443
|
}
|
|
49245
49444
|
await readMarkdown(destination);
|
|
@@ -49273,7 +49472,7 @@ async function readSource(source, asset) {
|
|
|
49273
49472
|
async function readMarkdown(path) {
|
|
49274
49473
|
let handle;
|
|
49275
49474
|
try {
|
|
49276
|
-
handle = await
|
|
49475
|
+
handle = await open3(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
49277
49476
|
if (!(await handle.stat()).isFile())
|
|
49278
49477
|
throw new TemplateSourceError(`Template is not a file: ${path}`);
|
|
49279
49478
|
const content = await handle.readFile("utf8");
|
|
@@ -49289,7 +49488,7 @@ async function readMarkdown(path) {
|
|
|
49289
49488
|
}
|
|
49290
49489
|
}
|
|
49291
49490
|
async function createExclusive(path, content) {
|
|
49292
|
-
const handle = await
|
|
49491
|
+
const handle = await open3(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
49293
49492
|
try {
|
|
49294
49493
|
await handle.writeFile(content, "utf8");
|
|
49295
49494
|
} catch (error) {
|
|
@@ -49305,6 +49504,10 @@ var init_service = __esm(() => {
|
|
|
49305
49504
|
init_fsx();
|
|
49306
49505
|
init_osx();
|
|
49307
49506
|
init_pathx();
|
|
49507
|
+
init_gitx2();
|
|
49508
|
+
init_config();
|
|
49509
|
+
init_repo_store();
|
|
49510
|
+
init_user_store();
|
|
49308
49511
|
init_source();
|
|
49309
49512
|
TemplateError = class TemplateError extends Error {
|
|
49310
49513
|
};
|
|
@@ -49318,7 +49521,7 @@ var init_service = __esm(() => {
|
|
|
49318
49521
|
|
|
49319
49522
|
// src/templates/tools.ts
|
|
49320
49523
|
var templateInfo, templateListOutput, scaffoldOutput, templateTools;
|
|
49321
|
-
var
|
|
49524
|
+
var init_tools4 = __esm(() => {
|
|
49322
49525
|
init_zod();
|
|
49323
49526
|
init_zodx();
|
|
49324
49527
|
init_service();
|
|
@@ -49332,7 +49535,7 @@ var init_tools3 = __esm(() => {
|
|
|
49332
49535
|
});
|
|
49333
49536
|
templateTools = [
|
|
49334
49537
|
defineTool("template_list", {
|
|
49335
|
-
description: "List
|
|
49538
|
+
description: "List document templates, including flow definitions and instances; install any missing defaults and examples in the user templates directory without overwriting local edits.",
|
|
49336
49539
|
inputSchema: strictObject({}),
|
|
49337
49540
|
outputSchema: templateListOutput,
|
|
49338
49541
|
annotations: { openWorldHint: false }
|
|
@@ -49357,7 +49560,7 @@ var init_tools3 = __esm(() => {
|
|
|
49357
49560
|
// src/mcp/server.ts
|
|
49358
49561
|
async function serveMcp(version = "0.1.0") {
|
|
49359
49562
|
const server = new McpServer({ name: "difflab", version });
|
|
49360
|
-
for (const tool of [...todoTools, ...projectTools, ...templateTools]) {
|
|
49563
|
+
for (const tool of [...todoTools, ...projectTools, ...templateTools, ...logTools]) {
|
|
49361
49564
|
server.registerTool(tool.name, tool.config, tool.callback);
|
|
49362
49565
|
}
|
|
49363
49566
|
await server.connect(new StdioServerTransport);
|
|
@@ -49368,6 +49571,7 @@ var init_server3 = __esm(() => {
|
|
|
49368
49571
|
init_tools();
|
|
49369
49572
|
init_tools2();
|
|
49370
49573
|
init_tools3();
|
|
49574
|
+
init_tools4();
|
|
49371
49575
|
});
|
|
49372
49576
|
|
|
49373
49577
|
// src/extensions/processx.ts
|
|
@@ -49442,7 +49646,7 @@ var init_json_config = __esm(() => {
|
|
|
49442
49646
|
});
|
|
49443
49647
|
|
|
49444
49648
|
// src/agents/claude-code.ts
|
|
49445
|
-
import { join as
|
|
49649
|
+
import { join as join7 } from "path";
|
|
49446
49650
|
var claudeCodeAdapter;
|
|
49447
49651
|
var init_claude_code = __esm(() => {
|
|
49448
49652
|
init_adapter();
|
|
@@ -49452,7 +49656,7 @@ var init_claude_code = __esm(() => {
|
|
|
49452
49656
|
id: "claude-code",
|
|
49453
49657
|
async setupMcpConfig(context = {}) {
|
|
49454
49658
|
const home = setupHome(context);
|
|
49455
|
-
const file =
|
|
49659
|
+
const file = join7(home, ".claude.json");
|
|
49456
49660
|
const status = serverStatus(await readJsonConfig(file), file);
|
|
49457
49661
|
if (status === "matching")
|
|
49458
49662
|
return { file, status: "existing" };
|
|
@@ -49468,7 +49672,7 @@ var init_claude_code = __esm(() => {
|
|
|
49468
49672
|
|
|
49469
49673
|
// src/agents/codex.ts
|
|
49470
49674
|
import { readFile as readFile5 } from "fs/promises";
|
|
49471
|
-
import { join as
|
|
49675
|
+
import { join as join8 } from "path";
|
|
49472
49676
|
function codexServerStatus(content) {
|
|
49473
49677
|
const lines = content.split(/\r?\n/);
|
|
49474
49678
|
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.(?:difflab|"difflab"|'difflab')\]\s*(?:#.*)?$/.test(line));
|
|
@@ -49492,7 +49696,7 @@ var init_codex = __esm(() => {
|
|
|
49492
49696
|
id: "codex",
|
|
49493
49697
|
async setupMcpConfig(context = {}) {
|
|
49494
49698
|
const home = setupHome(context);
|
|
49495
|
-
const file =
|
|
49699
|
+
const file = join8(home, ".codex", "config.toml");
|
|
49496
49700
|
let content = "";
|
|
49497
49701
|
try {
|
|
49498
49702
|
content = await readFile5(file, "utf8");
|
|
@@ -49514,7 +49718,7 @@ var init_codex = __esm(() => {
|
|
|
49514
49718
|
});
|
|
49515
49719
|
|
|
49516
49720
|
// src/agents/index.ts
|
|
49517
|
-
import { join as
|
|
49721
|
+
import { join as join9 } from "path";
|
|
49518
49722
|
function agentAdapter(name) {
|
|
49519
49723
|
return Object.hasOwn(agentAdapters, name) ? agentAdapters[name] : undefined;
|
|
49520
49724
|
}
|
|
@@ -49525,20 +49729,20 @@ var init_agents = __esm(() => {
|
|
|
49525
49729
|
init_codex();
|
|
49526
49730
|
init_json_config();
|
|
49527
49731
|
agentAdapters = {
|
|
49528
|
-
pi: jsonAgentAdapter("pi", (context) =>
|
|
49529
|
-
cursor: jsonAgentAdapter("cursor", (context) =>
|
|
49732
|
+
pi: jsonAgentAdapter("pi", (context) => join9(setupHome(context), ".config", "mcp", "mcp.json")),
|
|
49733
|
+
cursor: jsonAgentAdapter("cursor", (context) => join9(setupHome(context), ".cursor", "mcp.json")),
|
|
49530
49734
|
codex: codexAdapter,
|
|
49531
49735
|
"claude-code": claudeCodeAdapter,
|
|
49532
49736
|
"claude-desktop": jsonAgentAdapter("claude-desktop", (context) => {
|
|
49533
49737
|
const home = setupHome(context);
|
|
49534
49738
|
const platform = context.platform ?? process.platform;
|
|
49535
49739
|
if (platform === "darwin") {
|
|
49536
|
-
return
|
|
49740
|
+
return join9(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
49537
49741
|
}
|
|
49538
49742
|
if (platform === "win32") {
|
|
49539
|
-
return
|
|
49743
|
+
return join9(process.env.APPDATA ?? join9(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
49540
49744
|
}
|
|
49541
|
-
return
|
|
49745
|
+
return join9(home, ".config", "Claude", "claude_desktop_config.json");
|
|
49542
49746
|
})
|
|
49543
49747
|
};
|
|
49544
49748
|
agentNames = Object.keys(agentAdapters);
|
|
@@ -51626,7 +51830,7 @@ init_config();
|
|
|
51626
51830
|
init_context();
|
|
51627
51831
|
init_repo_store();
|
|
51628
51832
|
init_user_store();
|
|
51629
|
-
import { join as
|
|
51833
|
+
import { join as join10 } from "path";
|
|
51630
51834
|
function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
51631
51835
|
const program = new Command2().name("difflab").description("Difflab CLI and local MCP server").version(version);
|
|
51632
51836
|
program.action(() => program.outputHelp());
|
|
@@ -51660,7 +51864,7 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
51660
51864
|
if (config && config.project.id !== key)
|
|
51661
51865
|
throw new RepoStoreError(`Repository already belongs to project ${config.project.id}; requested ${key}`);
|
|
51662
51866
|
const origin = canonicalGitUrl(git.originUrl);
|
|
51663
|
-
const target =
|
|
51867
|
+
const target = join10(userPaths(options.home).projects, key, repositorySlug(origin));
|
|
51664
51868
|
await preflightRepoStore(git.root, target);
|
|
51665
51869
|
const project = await withUserStore(async (db) => {
|
|
51666
51870
|
const project = await db.projects.getProjectById(key);
|
|
@@ -51709,7 +51913,7 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
|
|
|
51709
51913
|
|
|
51710
51914
|
// src/index.ts
|
|
51711
51915
|
init_errors3();
|
|
51712
|
-
var version2 = "0.
|
|
51916
|
+
var version2 = "0.6.0";
|
|
51713
51917
|
try {
|
|
51714
51918
|
await createProgram(console.log, version2).parseAsync(process.argv);
|
|
51715
51919
|
} catch (error) {
|