@tiens.nguyen/gu-cli 1.0.784 → 1.0.785
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/completed-flush.mjs +47 -0
- package/gonext_agent_chat.py +1 -1
- package/gu-repl.mjs +154 -26
- package/gu_hooks.py +102 -4
- package/gu_skills.py +64 -4
- package/hooks-command.mjs +14 -9
- package/hooks-config.mjs +130 -33
- package/mcp-command.mjs +11 -5
- package/mcp-config.mjs +99 -11
- package/package.json +3 -2
- package/skill-source.mjs +60 -0
- package/skills.mjs +24 -12
package/skill-source.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHOSE SKILLS THIS FOLDER USES — gu's or Claude Code's (task #325, /switch-skill).
|
|
3
|
+
*
|
|
4
|
+
* "gu" ~/.gonext and <project>/.gonext (the default; a folder that never chose)
|
|
5
|
+
* "claude" ~/.claude and <project>/.claude Claude Code's own files, read where they are
|
|
6
|
+
*
|
|
7
|
+
* One choice covers skills, MCP servers and hooks, because they depend on each other: a skill names
|
|
8
|
+
* an MCP server's tools, and the hooks feed the same database that server reads. Switching one of
|
|
9
|
+
* the three alone would leave them talking about different things.
|
|
10
|
+
*
|
|
11
|
+
* ONE-WAY. gu reads Claude Code's setup; Claude Code is never expected to read gu's. So nothing of
|
|
12
|
+
* gu's own is written into Claude's files: which project servers and hooks you approved, and the
|
|
13
|
+
* allow rules, stay in ~/.gonext whichever source is chosen.
|
|
14
|
+
*
|
|
15
|
+
* PER FOLDER, IN THE FOLDER'S SESSION FILE. The terminal writes it; the worker — a different
|
|
16
|
+
* process, which builds each turn's skills, servers and hooks — reads the same file. Every reader
|
|
17
|
+
* in this package resolves the source through readSkillSource, so the banner and the turn cannot
|
|
18
|
+
* disagree about which setup is in use.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
export const SKILL_SOURCES = ["gu", "claude"];
|
|
26
|
+
export const DEFAULT_SKILL_SOURCE = "gu";
|
|
27
|
+
/** The session file's key. */
|
|
28
|
+
export const SKILL_SOURCE_KEY = "skillSource";
|
|
29
|
+
|
|
30
|
+
export const normalizeSkillSource = (v) => (v === "claude" ? "claude" : DEFAULT_SKILL_SOURCE);
|
|
31
|
+
|
|
32
|
+
/** The folder each source keeps its files in — under the home directory and under the project. */
|
|
33
|
+
export const sourceDirName = (source) => (normalizeSkillSource(source) === "claude" ? ".claude" : ".gonext");
|
|
34
|
+
|
|
35
|
+
/** What a picker and a status line call it. */
|
|
36
|
+
export const sourceLabel = (source) => (normalizeSkillSource(source) === "claude" ? "Claude's skills" : "gu's skills");
|
|
37
|
+
|
|
38
|
+
/** The terminal's per-folder session file — the same name gu-repl.mjs uses. */
|
|
39
|
+
export function sessionFileFor(cwd, home = homedir()) {
|
|
40
|
+
const hash = createHash("sha256").update(String(cwd)).digest("hex").slice(0, 32);
|
|
41
|
+
return join(home, ".gonext", "sessions", `${hash}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This folder's choice. No folder, no session file, a file that cannot be read, any other value:
|
|
46
|
+
* gu's — the behaviour before there was a choice, and never an error.
|
|
47
|
+
*/
|
|
48
|
+
export function readSkillSource(cwd, home = homedir()) {
|
|
49
|
+
if (!cwd) return DEFAULT_SKILL_SOURCE;
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(readFileSync(sessionFileFor(cwd, home), "utf8"));
|
|
52
|
+
return normalizeSkillSource(raw?.[SKILL_SOURCE_KEY]);
|
|
53
|
+
} catch {
|
|
54
|
+
return DEFAULT_SKILL_SOURCE;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `source` when a caller names one, else the folder's own. */
|
|
59
|
+
export const resolveSkillSource = ({ source, cwd, home = homedir() } = {}) =>
|
|
60
|
+
source ? normalizeSkillSource(source) : readSkillSource(cwd, home);
|
package/skills.mjs
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* ~/.gonext/skills/<name>/SKILL.md yours, every folder
|
|
5
5
|
* <project>/.gonext/skills/<name>/SKILL.md this project only
|
|
6
6
|
*
|
|
7
|
+
* …or Claude Code's own, when the folder chose them with /switch-skill (#325, skill-source.mjs):
|
|
8
|
+
* ~/.claude/skills/<name>/SKILL.md and <project>/.claude/skills/<name>/SKILL.md
|
|
9
|
+
* The same file in the same layout — only the folder differs, so nothing is converted.
|
|
10
|
+
*
|
|
7
11
|
* A SKILL.md is YAML frontmatter (name, description) and a body of instructions. Only the name and
|
|
8
12
|
* description go into every prompt; the body is read when the agent calls use_skill(name) or the
|
|
9
13
|
* user types /<name> — never pasted into every turn. Same split as Claude Code, and for the same
|
|
@@ -24,16 +28,17 @@
|
|
|
24
28
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
25
29
|
import { homedir } from "node:os";
|
|
26
30
|
import { join } from "node:path";
|
|
31
|
+
import { resolveSkillSource, sourceDirName } from "./skill-source.mjs";
|
|
27
32
|
|
|
28
33
|
export const SKILL_FILE = "SKILL.md";
|
|
29
34
|
/** Claude Code's rule: lowercase letters, digits and hyphens, at most 64. It is also a slash command. */
|
|
30
35
|
export const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
31
36
|
|
|
32
|
-
export function userSkillsDir(home = homedir()) {
|
|
33
|
-
return join(home,
|
|
37
|
+
export function userSkillsDir(home = homedir(), source = "gu") {
|
|
38
|
+
return join(home, sourceDirName(source), "skills");
|
|
34
39
|
}
|
|
35
|
-
export function projectSkillsDir(cwd) {
|
|
36
|
-
return join(cwd,
|
|
40
|
+
export function projectSkillsDir(cwd, source = "gu") {
|
|
41
|
+
return join(cwd, sourceDirName(source), "skills");
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** Strip one pair of matching quotes. */
|
|
@@ -138,13 +143,17 @@ function scanDir(dir, scope) {
|
|
|
138
143
|
* the built-in wins, and the skill is still usable by the agent, just not as a slash command —
|
|
139
144
|
* that is `slash: false`.
|
|
140
145
|
*
|
|
141
|
-
*
|
|
146
|
+
* `source` = whose skills ("gu" | "claude"); left out, it is the folder's own choice (/switch-skill).
|
|
147
|
+
* Only that source's skills are listed — the two are never mixed.
|
|
148
|
+
*
|
|
149
|
+
* → { skills: [{ name, description, path, dir, scope, slash, source }], warnings: string[], source }
|
|
142
150
|
*/
|
|
143
|
-
export function discoverSkills({ cwd = process.cwd(), home = homedir(), reserved = [] } = {}) {
|
|
144
|
-
const
|
|
151
|
+
export function discoverSkills({ cwd = process.cwd(), home = homedir(), reserved = [], source } = {}) {
|
|
152
|
+
const src = resolveSkillSource({ source, cwd, home });
|
|
153
|
+
const user = scanDir(userSkillsDir(home, src), "user");
|
|
145
154
|
const project =
|
|
146
|
-
cwd && projectSkillsDir(cwd) !== userSkillsDir(home)
|
|
147
|
-
? scanDir(projectSkillsDir(cwd), "project")
|
|
155
|
+
cwd && projectSkillsDir(cwd, src) !== userSkillsDir(home, src)
|
|
156
|
+
? scanDir(projectSkillsDir(cwd, src), "project")
|
|
148
157
|
: { skills: [], warnings: [] };
|
|
149
158
|
const byName = new Map();
|
|
150
159
|
for (const s of user.skills) byName.set(s.name, s);
|
|
@@ -152,17 +161,20 @@ export function discoverSkills({ cwd = process.cwd(), home = homedir(), reserved
|
|
|
152
161
|
const taken = new Set(reserved.map((r) => r.replace(/^\//, "")));
|
|
153
162
|
const skills = [...byName.values()]
|
|
154
163
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
155
|
-
.map((s) => ({ ...s, slash: !taken.has(s.name) }));
|
|
156
|
-
return { skills, warnings: [...user.warnings, ...project.warnings] };
|
|
164
|
+
.map((s) => ({ ...s, slash: !taken.has(s.name), source: src }));
|
|
165
|
+
return { skills, warnings: [...user.warnings, ...project.warnings], source: src };
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
/** What the worker hands the agent for one turn: enough to list them and to read one. */
|
|
160
169
|
export function skillsForTurn(opts) {
|
|
161
|
-
|
|
170
|
+
// `source` goes along only for Claude's: the agent then reads the skill as one written for
|
|
171
|
+
// Claude Code's tools (gu_skills.py). gu's own keep the shape they always had.
|
|
172
|
+
return discoverSkills(opts).skills.map(({ name, description, path, dir, source }) => ({
|
|
162
173
|
name,
|
|
163
174
|
description,
|
|
164
175
|
path,
|
|
165
176
|
dir,
|
|
177
|
+
...(source === "claude" ? { source } : {}),
|
|
166
178
|
}));
|
|
167
179
|
}
|
|
168
180
|
|