glm-coding-router 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/dist/bin/glm-chat.js +40 -0
  4. package/dist/bin/glm-review.js +47 -0
  5. package/dist/bin/glm-worker.js +57 -0
  6. package/dist/cli.js +108 -0
  7. package/dist/commands/config.js +39 -0
  8. package/dist/commands/context.js +20 -0
  9. package/dist/commands/doctor-command.js +64 -0
  10. package/dist/commands/doctor.js +93 -0
  11. package/dist/commands/init.js +123 -0
  12. package/dist/commands/key.js +56 -0
  13. package/dist/commands/project-init.js +61 -0
  14. package/dist/commands/project-remove.js +36 -0
  15. package/dist/commands/skill.js +43 -0
  16. package/dist/commands/status.js +57 -0
  17. package/dist/commands/uninstall.js +81 -0
  18. package/dist/core/claude.js +104 -0
  19. package/dist/core/config.js +150 -0
  20. package/dist/core/env.js +22 -0
  21. package/dist/core/errors.js +121 -0
  22. package/dist/core/logging.js +54 -0
  23. package/dist/core/main-guard.js +20 -0
  24. package/dist/core/paths.js +13 -0
  25. package/dist/core/platform.js +20 -0
  26. package/dist/core/process.js +60 -0
  27. package/dist/core/prompt.js +32 -0
  28. package/dist/core/version.js +3 -0
  29. package/dist/core/zai-key.js +84 -0
  30. package/dist/integrations/claude.js +18 -0
  31. package/dist/integrations/codex.js +18 -0
  32. package/dist/integrations/index.js +3 -0
  33. package/dist/integrations/skill.js +35 -0
  34. package/dist/project/atomic-write.js +21 -0
  35. package/dist/project/managed-block.js +100 -0
  36. package/dist/project/managed-file.js +67 -0
  37. package/dist/project/ownership.js +42 -0
  38. package/dist/project/project-root.js +26 -0
  39. package/dist/templates/agents-block.js +46 -0
  40. package/dist/templates/claude-block.js +49 -0
  41. package/dist/templates/glm-delegation-skill.js +68 -0
  42. package/package.json +46 -0
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Atomic file replacement (spec §22): write tmp → fsync → rename.
5
+ * On failure the original file is left untouched. Callers translate
6
+ * filesystem errors into their own actionable errors.
7
+ */
8
+ export function atomicWriteFile(filePath, content) {
9
+ const dir = path.dirname(filePath);
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ const tmp = `${filePath}.glm-tmp-${process.pid}-${Date.now()}`;
12
+ const fd = fs.openSync(tmp, "w");
13
+ try {
14
+ fs.writeFileSync(fd, content, "utf8");
15
+ fs.fsyncSync(fd);
16
+ }
17
+ finally {
18
+ fs.closeSync(fd);
19
+ }
20
+ fs.renameSync(tmp, filePath);
21
+ }
@@ -0,0 +1,100 @@
1
+ import { Errors } from "../core/errors.js";
2
+ export const START_MARKER = "<!-- glm-coding-router:start -->";
3
+ export const END_MARKER = "<!-- glm-coding-router:end -->";
4
+ export class ManagedBlockError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "ManagedBlockError";
8
+ }
9
+ }
10
+ /** Detect the file's dominant EOL; default LF (spec §48). */
11
+ function detectEol(content) {
12
+ const firstCr = content.indexOf("\r");
13
+ if (firstCr === -1)
14
+ return "\n";
15
+ const firstLf = content.indexOf("\n");
16
+ if (firstLf === -1)
17
+ return "\n";
18
+ return firstCr < firstLf - 1 ? "\n" : "\r\n";
19
+ }
20
+ function convertEol(text, eol) {
21
+ return eol === "\n" ? text.replace(/\r\n/g, "\n") : text.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n");
22
+ }
23
+ function countOccurrences(content, needle) {
24
+ return content.split(needle).length - 1;
25
+ }
26
+ /**
27
+ * Validate marker pairing (spec §48): on a malformed pair the caller must
28
+ * not modify the file and must surface an actionable error.
29
+ */
30
+ export function validateMarkers(content, fileName) {
31
+ const starts = countOccurrences(content, START_MARKER);
32
+ const ends = countOccurrences(content, END_MARKER);
33
+ if (starts === 0 && ends === 0)
34
+ return;
35
+ if (starts > 1) {
36
+ throw Errors.managedBlockCorrupt(fileName, "multiple start markers");
37
+ }
38
+ if (ends > 1) {
39
+ throw Errors.managedBlockCorrupt(fileName, "multiple end markers");
40
+ }
41
+ if (starts === 1 && ends === 0) {
42
+ throw Errors.managedBlockCorrupt(fileName, "start marker without end marker");
43
+ }
44
+ if (starts === 0 && ends === 1) {
45
+ throw Errors.managedBlockCorrupt(fileName, "end marker without start marker");
46
+ }
47
+ if (content.indexOf(START_MARKER) > content.indexOf(END_MARKER)) {
48
+ throw Errors.managedBlockCorrupt(fileName, "end marker appears before start marker");
49
+ }
50
+ }
51
+ export function hasManagedBlock(content) {
52
+ return content.includes(START_MARKER) || content.includes(END_MARKER);
53
+ }
54
+ /**
55
+ * Insert or replace the managed block (spec §21):
56
+ * - absent → append with a blank-line separator
57
+ * - present → replace in place, preserving everything outside the markers
58
+ * Idempotent: never duplicates, preserves the file's EOL style and
59
+ * trailing-newline state.
60
+ */
61
+ export function upsertManagedBlock(content, block, fileName) {
62
+ validateMarkers(content, fileName);
63
+ const eol = detectEol(content);
64
+ const blockText = convertEol(block.trim(), eol);
65
+ if (content.includes(START_MARKER)) {
66
+ const startIdx = content.indexOf(START_MARKER);
67
+ const endIdx = content.indexOf(END_MARKER) + END_MARKER.length;
68
+ const prefix = content.slice(0, startIdx);
69
+ const suffix = content.slice(endIdx);
70
+ return prefix + blockText + suffix;
71
+ }
72
+ if (content.trim().length === 0) {
73
+ return blockText + eol;
74
+ }
75
+ const base = content.replace(/(?:\r?\n)+$/, "");
76
+ return base + eol + eol + blockText + eol;
77
+ }
78
+ /**
79
+ * Remove the managed block, preserving all user content (spec §43).
80
+ * A file that contained only the block becomes empty (""), letting callers
81
+ * decide whether to delete a router-created file.
82
+ */
83
+ export function removeManagedBlock(content, fileName) {
84
+ validateMarkers(content, fileName);
85
+ if (!content.includes(START_MARKER)) {
86
+ return content;
87
+ }
88
+ const eol = detectEol(content);
89
+ const startIdx = content.indexOf(START_MARKER);
90
+ const endIdx = content.indexOf(END_MARKER) + END_MARKER.length;
91
+ const prefix = content.slice(0, startIdx).replace(/(?:\r?\n)+$/, "");
92
+ const suffix = content.slice(endIdx).replace(/^(?:\r?\n)+/, "");
93
+ if (prefix.length === 0 && suffix.length === 0)
94
+ return "";
95
+ if (prefix.length === 0)
96
+ return suffix;
97
+ if (suffix.length === 0)
98
+ return prefix + eol;
99
+ return prefix + eol + eol + suffix;
100
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Errors } from "../core/errors.js";
4
+ import { hasManagedBlock, removeManagedBlock, upsertManagedBlock } from "./managed-block.js";
5
+ import { atomicWriteFile } from "./atomic-write.js";
6
+ import { forgetFile, isOwnedFile, recordCreatedFile } from "./ownership.js";
7
+ function toChange(file, created, oldContent, newContent, deleted = false) {
8
+ return {
9
+ file,
10
+ created,
11
+ changed: oldContent !== newContent || deleted,
12
+ oldContent,
13
+ newContent,
14
+ deleted: deleted,
15
+ };
16
+ }
17
+ /** Insert or replace the managed block in a file, atomically (spec §21, §22). */
18
+ export function upsertManagedFile(options) {
19
+ const { file, block, home, dryRun } = options;
20
+ const fileName = path.basename(file);
21
+ const existed = fs.existsSync(file);
22
+ const oldContent = existed ? fs.readFileSync(file, "utf8") : "";
23
+ const newContent = upsertManagedBlock(oldContent, block, fileName);
24
+ if (!dryRun && newContent !== oldContent) {
25
+ try {
26
+ atomicWriteFile(file, newContent);
27
+ }
28
+ catch (cause) {
29
+ throw Errors.managedFileWriteFailed(file, cause instanceof Error ? cause.message : String(cause));
30
+ }
31
+ if (!existed) {
32
+ recordCreatedFile(file, home);
33
+ }
34
+ }
35
+ return toChange(file, !existed, oldContent, newContent);
36
+ }
37
+ /** Remove the managed block; delete router-created files that become empty (spec §43). */
38
+ export function removeManagedFile(file, home, dryRun = false) {
39
+ const fileName = path.basename(file);
40
+ if (!fs.existsSync(file)) {
41
+ return toChange(file, false, "", "", false);
42
+ }
43
+ const oldContent = fs.readFileSync(file, "utf8");
44
+ if (!hasManagedBlock(oldContent)) {
45
+ return toChange(file, false, oldContent, oldContent, false);
46
+ }
47
+ const newContent = removeManagedBlock(oldContent, fileName);
48
+ const owned = isOwnedFile(file, home);
49
+ const emptyAfterRemove = newContent.length === 0;
50
+ if (dryRun) {
51
+ return toChange(file, false, oldContent, newContent, owned && emptyAfterRemove);
52
+ }
53
+ try {
54
+ if (emptyAfterRemove && owned) {
55
+ fs.rmSync(file);
56
+ forgetFile(file, home);
57
+ return toChange(file, false, oldContent, newContent, true);
58
+ }
59
+ if (newContent !== oldContent) {
60
+ atomicWriteFile(file, newContent);
61
+ }
62
+ }
63
+ catch (cause) {
64
+ throw Errors.managedFileWriteFailed(file, cause instanceof Error ? cause.message : String(cause));
65
+ }
66
+ return toChange(file, false, oldContent, newContent, false);
67
+ }
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ownershipPath } from "../core/paths.js";
4
+ function readRecords(home) {
5
+ const file = ownershipPath(home);
6
+ try {
7
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
8
+ if (raw && typeof raw.files === "object" && raw.files !== null) {
9
+ return { files: raw.files };
10
+ }
11
+ }
12
+ catch {
13
+ // Missing or corrupt metadata is non-fatal: fall back to empty.
14
+ }
15
+ return { files: {} };
16
+ }
17
+ function writeRecords(record, home) {
18
+ const file = ownershipPath(home);
19
+ fs.mkdirSync(path.dirname(file), { recursive: true });
20
+ const tmp = `${file}.tmp-${process.pid}`;
21
+ fs.writeFileSync(tmp, JSON.stringify(record, null, 2) + "\n", "utf8");
22
+ fs.renameSync(tmp, file);
23
+ }
24
+ export function recordCreatedFile(filePath, home) {
25
+ const normalized = path.resolve(filePath);
26
+ const records = readRecords(home);
27
+ if (normalized in records.files)
28
+ return;
29
+ records.files[normalized] = { createdAt: new Date().toISOString() };
30
+ writeRecords(records, home);
31
+ }
32
+ export function isOwnedFile(filePath, home) {
33
+ return path.resolve(filePath) in readRecords(home).files;
34
+ }
35
+ export function forgetFile(filePath, home) {
36
+ const normalized = path.resolve(filePath);
37
+ const records = readRecords(home);
38
+ if (!(normalized in records.files))
39
+ return;
40
+ delete records.files[normalized];
41
+ writeRecords(records, home);
42
+ }
@@ -0,0 +1,26 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { logger } from "../core/logging.js";
4
+ /**
5
+ * Project root detection (spec §19):
6
+ * git rev-parse --show-toplevel → cwd
7
+ */
8
+ export function findProjectRoot(cwd = process.cwd()) {
9
+ try {
10
+ const output = execFileSync("git", ["rev-parse", "--show-toplevel"], {
11
+ cwd,
12
+ encoding: "utf8",
13
+ windowsHide: true,
14
+ stdio: ["ignore", "pipe", "ignore"],
15
+ });
16
+ const root = output.trim();
17
+ if (root.length > 0) {
18
+ // git prints forward slashes on Windows; normalize for the platform.
19
+ return path.resolve(root);
20
+ }
21
+ }
22
+ catch (error) {
23
+ logger.debug(`git rev-parse failed (${error instanceof Error ? error.message : "unknown"}); using cwd`);
24
+ }
25
+ return cwd;
26
+ }
@@ -0,0 +1,46 @@
1
+ /** Managed block content for AGENTS.md (spec §24). Keep in sync with the spec. */
2
+ export const AGENTS_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
+
4
+ ## GLM Worker Delegation
5
+
6
+ Available commands:
7
+
8
+ - \`glm-worker "<task>"\`
9
+ - \`glm-review "<task>"\`
10
+
11
+ Codex is the primary orchestrator.
12
+
13
+ Delegate:
14
+ - CRUD
15
+ - boilerplate
16
+ - tests
17
+ - documentation
18
+ - mechanical refactoring
19
+ - repository exploration
20
+ - straightforward implementation
21
+
22
+ Keep in Codex:
23
+ - requirements
24
+ - planning
25
+ - architecture
26
+ - ambiguous business logic
27
+ - complex debugging
28
+ - security decisions
29
+ - integration
30
+ - final review
31
+
32
+ Before delegation create a task packet:
33
+
34
+ TASK
35
+ SCOPE
36
+ FILES ALLOWED TO MODIFY
37
+ FILES NOT TO MODIFY
38
+ REQUIREMENTS
39
+ CONSTRAINTS
40
+ ACCEPTANCE CRITERIA
41
+ VALIDATION
42
+ EXPECTED OUTPUT
43
+
44
+ Never trust a worker's success report without inspecting the resulting changes.
45
+
46
+ <!-- glm-coding-router:end -->`;
@@ -0,0 +1,49 @@
1
+ /** Managed block content for CLAUDE.md (spec §20). Keep in sync with the spec. */
2
+ export const CLAUDE_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
+
4
+ ## GLM Worker Delegation
5
+
6
+ GLM workers available:
7
+
8
+ - \`glm-worker "<task>"\`
9
+ - \`glm-review "<task>"\`
10
+
11
+ Delegate well-scoped, implementation-heavy work to GLM.
12
+
13
+ Use GLM for:
14
+ - repository exploration
15
+ - CRUD
16
+ - boilerplate
17
+ - tests
18
+ - documentation
19
+ - mechanical refactoring
20
+ - straightforward implementation
21
+
22
+ Claude remains responsible for:
23
+ - requirements
24
+ - architecture
25
+ - ambiguous business rules
26
+ - security-sensitive decisions
27
+ - complex debugging
28
+ - integration
29
+ - final review
30
+
31
+ Before delegation, define:
32
+ - task
33
+ - scope
34
+ - allowed files
35
+ - forbidden files
36
+ - requirements
37
+ - constraints
38
+ - acceptance criteria
39
+ - validation command
40
+ - expected output
41
+
42
+ After worker completion:
43
+ 1. inspect the actual diff
44
+ 2. validate against requirements
45
+ 3. run relevant tests
46
+ 4. resolve integration problems
47
+ 5. accept only after verification
48
+
49
+ <!-- glm-coding-router:end -->`;
@@ -0,0 +1,68 @@
1
+ /** Codex skill definition (spec §26). Keep in sync with the spec. */
2
+ export const GLM_DELEGATION_SKILL_NAME = "glm-delegation";
3
+ export const GLM_DELEGATION_SKILL_MD = `---
4
+ name: glm-delegation
5
+ description: >
6
+ Delegate well-scoped implementation, testing,
7
+ repository exploration, boilerplate, CRUD,
8
+ documentation, and mechanical refactoring to
9
+ GLM Coding Plan workers.
10
+ ---
11
+
12
+ # GLM Delegation
13
+
14
+ Available commands:
15
+
16
+ glm-worker "<task>"
17
+ glm-review "<task>"
18
+
19
+ ## Use glm-review for
20
+
21
+ - repository exploration
22
+ - dependency analysis
23
+ - locating implementations
24
+ - call-chain discovery
25
+ - code review
26
+
27
+ ## Use glm-worker for
28
+
29
+ - CRUD
30
+ - unit tests
31
+ - implementation
32
+ - documentation
33
+ - repetitive changes
34
+ - mechanical refactoring
35
+
36
+ ## Keep in primary Codex agent
37
+
38
+ - requirements
39
+ - architecture
40
+ - ambiguous rules
41
+ - security-sensitive design
42
+ - difficult debugging
43
+ - integration
44
+ - final acceptance
45
+
46
+ ## Delegation packet
47
+
48
+ Always provide:
49
+
50
+ TASK
51
+ SCOPE
52
+ ALLOWED FILES
53
+ FORBIDDEN FILES
54
+ REQUIREMENTS
55
+ CONSTRAINTS
56
+ ACCEPTANCE CRITERIA
57
+ VALIDATION
58
+ EXPECTED OUTPUT
59
+
60
+ ## Verification
61
+
62
+ After GLM finishes:
63
+
64
+ - inspect the actual diff
65
+ - independently run relevant validation
66
+ - compare implementation with requirements
67
+ - reject or correct worker output when needed
68
+ `;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "glm-coding-router",
3
+ "version": "0.1.0",
4
+ "description": "GLM Coding Plan workers for Claude Code and Codex",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "hieu9721",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/hieu9721/GLM-coding-router.git"
11
+ },
12
+ "bin": {
13
+ "glm-router": "./dist/cli.js",
14
+ "glm-chat": "./dist/bin/glm-chat.js",
15
+ "glm-worker": "./dist/bin/glm-worker.js",
16
+ "glm-review": "./dist/bin/glm-review.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "dev": "tsx src/cli.ts",
23
+ "build": "tsc",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "lint": "eslint src tests",
27
+ "prepublishOnly": "npm run build && npm test"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^15.0.0",
34
+ "prompts": "^2.4.2",
35
+ "zod": "^4.6.5"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.20.3",
39
+ "@types/prompts": "^2.4.9",
40
+ "eslint": "^9.39.5",
41
+ "tsx": "^4.23.13",
42
+ "typescript": "^5.9.3",
43
+ "typescript-eslint": "^8.70.0",
44
+ "vitest": "^5.0.1"
45
+ }
46
+ }