nex-code 0.4.25 → 0.4.27

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.
@@ -1,43 +0,0 @@
1
- # DevOps Agent
2
-
3
- You are a DevOps-specialized coding agent with infrastructure management capabilities that go beyond code editing. You have direct access to:
4
-
5
- ## Infrastructure Tools
6
-
7
- - **ssh_exec**: Execute commands on remote servers via SSH profiles (`.nex/servers.json`)
8
- - **service_manage**: Start/stop/restart systemd services on remote servers
9
- - **service_logs**: Fetch journalctl logs from remote services
10
- - **container_list/logs/exec/manage**: Docker container lifecycle management
11
- - **deploy**: Named deployment workflows via `.nex/deploy.json`
12
- - **remote_agent**: Delegate coding tasks to nex-code on remote servers
13
-
14
- ## Deployment Workflow
15
-
16
- 1. Check server status: `ssh_exec` to verify connectivity
17
- 2. Review deploy config: read `.nex/deploy.json` for named configs
18
- 3. Run deployment: `deploy` tool with named config or explicit params
19
- 4. Verify: Check service status and health endpoints
20
- 5. Rollback: Re-deploy previous version if health check fails
21
-
22
- ## Server Configuration
23
-
24
- Server profiles live in `.nex/servers.json`:
25
-
26
- ```json
27
- {
28
- "prod": {
29
- "host": "prod.example.com",
30
- "user": "deploy",
31
- "key": "~/.ssh/deploy_key"
32
- },
33
- "staging": { "host": "staging.example.com", "user": "deploy" }
34
- }
35
- ```
36
-
37
- ## Best Practices
38
-
39
- - Always check service status before and after changes
40
- - Use named deploy configs for repeatable deployments
41
- - Check logs when services fail to start
42
- - Use health check URLs to verify deployments
43
- - Keep deployment configs in version control
@@ -1,180 +0,0 @@
1
- /**
2
- * cli/skills/session-search.js — Cross-Session Search
3
- * Search past sessions by keyword to recall previous approaches.
4
- * Inspired by Hermes Agent's FTS session search.
5
- */
6
-
7
- const fs = require("fs");
8
- const path = require("path");
9
-
10
- function getSessionsDir() {
11
- return path.join(process.cwd(), ".nex", "sessions");
12
- }
13
-
14
- /**
15
- * Search sessions for keyword matches.
16
- * Returns matching sessions with context snippets around each match.
17
- *
18
- * @param {string} query — Search query (case-insensitive substring match)
19
- * @param {{ maxResults?: number, maxSnippets?: number }} opts
20
- * @returns {Array<{ name, updatedAt, model, matchCount, snippets: string[] }>}
21
- */
22
- function searchSessions(query, opts = {}) {
23
- const dir = getSessionsDir();
24
- if (!fs.existsSync(dir)) return [];
25
-
26
- const maxResults = opts.maxResults || 5;
27
- const maxSnippets = opts.maxSnippets || 3;
28
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
29
-
30
- const queryLower = query.toLowerCase();
31
- const results = [];
32
-
33
- for (const f of files) {
34
- try {
35
- const raw = fs.readFileSync(path.join(dir, f), "utf-8");
36
- const session = JSON.parse(raw);
37
- if (!session.messages || !Array.isArray(session.messages)) continue;
38
-
39
- // Search through user and assistant messages
40
- const snippets = [];
41
- let matchCount = 0;
42
-
43
- for (const msg of session.messages) {
44
- if (!msg.content || typeof msg.content !== "string") continue;
45
- const contentLower = msg.content.toLowerCase();
46
- let searchFrom = 0;
47
-
48
- while (searchFrom < contentLower.length) {
49
- const idx = contentLower.indexOf(queryLower, searchFrom);
50
- if (idx === -1) break;
51
- matchCount++;
52
-
53
- if (snippets.length < maxSnippets) {
54
- // Extract context around match (80 chars before, 120 after)
55
- const start = Math.max(0, idx - 80);
56
- const end = Math.min(msg.content.length, idx + query.length + 120);
57
- const prefix = start > 0 ? "..." : "";
58
- const suffix = end < msg.content.length ? "..." : "";
59
- const snippet =
60
- `[${msg.role}] ${prefix}${msg.content.slice(start, end)}${suffix}`;
61
- snippets.push(snippet.replace(/\n/g, " ").trim());
62
- }
63
-
64
- searchFrom = idx + query.length;
65
- }
66
- }
67
-
68
- if (matchCount > 0) {
69
- results.push({
70
- name: session.name || f.replace(".json", ""),
71
- updatedAt: session.updatedAt || null,
72
- model: session.model || null,
73
- messageCount: session.messageCount || session.messages.length,
74
- matchCount,
75
- snippets,
76
- });
77
- }
78
- } catch {
79
- // skip corrupt files
80
- }
81
- }
82
-
83
- // Sort by match count (most relevant first), then by date
84
- results.sort((a, b) => {
85
- if (b.matchCount !== a.matchCount) return b.matchCount - a.matchCount;
86
- return (b.updatedAt || "").localeCompare(a.updatedAt || "");
87
- });
88
-
89
- return results.slice(0, maxResults);
90
- }
91
-
92
- module.exports = {
93
- name: "session-search",
94
- description:
95
- "Search past sessions by keyword to recall previous approaches, " +
96
- "solutions, and conversations.",
97
-
98
- instructions: `You have a session search tool for cross-session recall.
99
-
100
- ## When to use
101
-
102
- - When the user asks about past work ("what did we do last time?", "how did we fix X?")
103
- - When you need to recall a previous approach before starting similar work
104
- - When the user references something from an earlier session
105
-
106
- ## Tips
107
-
108
- - Search for specific keywords, error messages, or file names
109
- - Combine results from multiple searches to build context
110
- - Session search only covers saved sessions in .nex/sessions/`,
111
-
112
- tools: [
113
- {
114
- type: "function",
115
- function: {
116
- name: "search_sessions",
117
- description:
118
- "Search past sessions by keyword. Returns matching sessions with context " +
119
- "snippets showing where the keyword appears. Use this to recall previous " +
120
- "approaches, solutions, or conversations.",
121
- parameters: {
122
- type: "object",
123
- properties: {
124
- query: {
125
- type: "string",
126
- description:
127
- "Search keyword or phrase (case-insensitive). Use specific terms " +
128
- "like error messages, file names, or tool names for best results.",
129
- },
130
- max_results: {
131
- type: "number",
132
- description:
133
- "Maximum number of sessions to return (default: 5)",
134
- },
135
- },
136
- required: ["query"],
137
- },
138
- },
139
- execute: async (args) => {
140
- const { query, max_results } = args;
141
-
142
- if (!query || typeof query !== "string" || query.trim().length < 2) {
143
- return JSON.stringify({
144
- status: "error",
145
- error: "Query must be at least 2 characters",
146
- });
147
- }
148
-
149
- const results = searchSessions(query.trim(), {
150
- maxResults: max_results || 5,
151
- });
152
-
153
- if (results.length === 0) {
154
- return JSON.stringify({
155
- status: "ok",
156
- message: `No sessions found matching "${query}"`,
157
- results: [],
158
- });
159
- }
160
-
161
- return JSON.stringify({
162
- status: "ok",
163
- query,
164
- total_matches: results.reduce((s, r) => s + r.matchCount, 0),
165
- sessions: results.map((r) => ({
166
- name: r.name,
167
- updated: r.updatedAt,
168
- model: r.model,
169
- messages: r.messageCount,
170
- matches: r.matchCount,
171
- snippets: r.snippets,
172
- })),
173
- });
174
- },
175
- },
176
- ],
177
-
178
- // Export searchSessions for direct use and testing
179
- _searchSessions: searchSessions,
180
- };
@@ -1,304 +0,0 @@
1
- /**
2
- * cli/skills/skill-learning.js — Skill Auto-Learning
3
- * Lets the agent create and update reusable skills from experience.
4
- * Inspired by Hermes Agent's closed learning loop.
5
- */
6
-
7
- const fs = require("fs");
8
- const path = require("path");
9
-
10
- function getSkillsDir() {
11
- return path.join(process.cwd(), ".nex", "skills");
12
- }
13
-
14
- function ensureSkillsDir() {
15
- const dir = getSkillsDir();
16
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
- return dir;
18
- }
19
-
20
- /**
21
- * Validate a skill name: lowercase, alphanumeric + hyphens, 2-64 chars
22
- */
23
- function validateName(name) {
24
- if (!name || typeof name !== "string") return "name is required";
25
- if (name.length < 2 || name.length > 64) return "name must be 2-64 characters";
26
- if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1)
27
- return "name must be lowercase alphanumeric with hyphens (e.g. deploy-workflow)";
28
- return null;
29
- }
30
-
31
- /**
32
- * Build a .md skill file with YAML frontmatter
33
- */
34
- function buildSkillContent(name, description, triggers, content) {
35
- const triggerBlock =
36
- triggers && triggers.length > 0
37
- ? `trigger:\n${triggers.map((t) => ` - "${t}"`).join("\n")}\n`
38
- : "";
39
- return `---
40
- name: ${name}
41
- description: ${description}
42
- ${triggerBlock}---
43
-
44
- ${content}
45
- `;
46
- }
47
-
48
- module.exports = {
49
- name: "skill-learning",
50
- description:
51
- "Create and update reusable skills from experience. " +
52
- "After complex tasks, the agent can save its approach as a skill for future reuse.",
53
-
54
- instructions: `You have tools to create and improve reusable skills.
55
-
56
- ## When to create a skill
57
-
58
- After completing a complex task (5+ tool calls), fixing a tricky multi-step issue,
59
- or discovering a non-obvious workflow, save the approach as a reusable skill using
60
- skill_learn_create. Good candidates:
61
-
62
- - Deployment procedures with specific steps
63
- - Debugging workflows for recurring issues
64
- - Project-specific patterns (build, test, release)
65
- - Recurring maintenance or migration tasks
66
-
67
- Do NOT create skills for trivial one-off tasks or generic knowledge.
68
-
69
- ## When to update a skill
70
-
71
- When using a skill and finding it outdated, incomplete, or wrong, patch it immediately
72
- with skill_learn_patch. Skills that are not maintained become liabilities.
73
-
74
- ## Skill quality
75
-
76
- - Instructions should be actionable step-by-step guides
77
- - Include the WHY behind non-obvious steps
78
- - Add trigger keywords so the skill activates automatically on relevant prompts
79
- - Keep skills focused — one workflow per skill, not a knowledge dump`,
80
-
81
- commands: [
82
- {
83
- cmd: "/skills-learned",
84
- desc: "List all user-created skills in .nex/skills/",
85
- handler: () => {
86
- const dir = getSkillsDir();
87
- if (!fs.existsSync(dir)) return "No learned skills yet.";
88
- const files = fs
89
- .readdirSync(dir)
90
- .filter((f) => f.endsWith(".md") || f.endsWith(".js"));
91
- if (files.length === 0) return "No learned skills yet.";
92
- return (
93
- "Learned skills:\n" +
94
- files.map((f) => ` - ${f.replace(/\.(md|js)$/, "")}`).join("\n")
95
- );
96
- },
97
- },
98
- ],
99
-
100
- tools: [
101
- {
102
- type: "function",
103
- function: {
104
- name: "learn_create",
105
- description:
106
- "Create a new reusable skill from the current task's approach. " +
107
- "The skill will be saved to .nex/skills/ and loaded automatically in future sessions.",
108
- parameters: {
109
- type: "object",
110
- properties: {
111
- name: {
112
- type: "string",
113
- description:
114
- "Skill name: lowercase, hyphens allowed, 2-64 chars (e.g. deploy-nextjs, fix-docker-dns)",
115
- },
116
- description: {
117
- type: "string",
118
- description: "One-line description of what this skill does",
119
- },
120
- triggers: {
121
- type: "array",
122
- items: { type: "string" },
123
- description:
124
- "Keywords that should activate this skill (e.g. [\"deploy\", \"nextjs\", \"vercel\"])",
125
- },
126
- content: {
127
- type: "string",
128
- description:
129
- "Skill instructions in markdown. Should be a step-by-step guide the agent can follow.",
130
- },
131
- },
132
- required: ["name", "description", "content"],
133
- },
134
- },
135
- execute: async (args) => {
136
- const { name, description, content, triggers } = args;
137
-
138
- // Validate name
139
- const nameErr = validateName(name);
140
- if (nameErr) return JSON.stringify({ status: "error", error: nameErr });
141
-
142
- // Validate content
143
- if (!content || content.trim().length < 20) {
144
- return JSON.stringify({
145
- status: "error",
146
- error: "Content must be at least 20 characters",
147
- });
148
- }
149
- if (!description || description.trim().length < 5) {
150
- return JSON.stringify({
151
- status: "error",
152
- error: "Description must be at least 5 characters",
153
- });
154
- }
155
-
156
- const dir = ensureSkillsDir();
157
- const filePath = path.join(dir, `${name}.md`);
158
-
159
- // Check if skill already exists
160
- if (fs.existsSync(filePath)) {
161
- return JSON.stringify({
162
- status: "error",
163
- error: `Skill "${name}" already exists. Use skill_learn_patch to update it.`,
164
- });
165
- }
166
-
167
- // Write skill file
168
- const skillContent = buildSkillContent(
169
- name,
170
- description,
171
- triggers || [],
172
- content.trim(),
173
- );
174
- fs.writeFileSync(filePath, skillContent, "utf-8");
175
-
176
- // Reload skills if the loader is available
177
- try {
178
- const { loadAllSkills } = require("../skills");
179
- loadAllSkills();
180
- } catch {
181
- // skills.js may not be loadable in all contexts
182
- }
183
-
184
- return JSON.stringify({
185
- status: "created",
186
- name,
187
- path: filePath,
188
- triggers: triggers || [],
189
- note: "Skill will be active in the next session (or after /reload-skills).",
190
- });
191
- },
192
- },
193
- {
194
- type: "function",
195
- function: {
196
- name: "learn_patch",
197
- description:
198
- "Update an existing skill by replacing a section of its content. " +
199
- "Use this when a skill is outdated, incomplete, or wrong.",
200
- parameters: {
201
- type: "object",
202
- properties: {
203
- name: {
204
- type: "string",
205
- description: "Name of the skill to patch",
206
- },
207
- old_text: {
208
- type: "string",
209
- description: "Exact text to find in the skill file",
210
- },
211
- new_text: {
212
- type: "string",
213
- description: "Replacement text",
214
- },
215
- },
216
- required: ["name", "old_text", "new_text"],
217
- },
218
- },
219
- execute: async (args) => {
220
- const { name, old_text, new_text } = args;
221
-
222
- if (!name) {
223
- return JSON.stringify({ status: "error", error: "name is required" });
224
- }
225
-
226
- const dir = getSkillsDir();
227
- // Try both .md and .js extensions
228
- let filePath = path.join(dir, `${name}.md`);
229
- if (!fs.existsSync(filePath)) {
230
- filePath = path.join(dir, `${name}.js`);
231
- }
232
- if (!fs.existsSync(filePath)) {
233
- return JSON.stringify({
234
- status: "error",
235
- error: `Skill "${name}" not found in ${dir}`,
236
- });
237
- }
238
-
239
- const content = fs.readFileSync(filePath, "utf-8");
240
- if (!content.includes(old_text)) {
241
- return JSON.stringify({
242
- status: "error",
243
- error: "old_text not found in skill file. Read the skill first to get the exact text.",
244
- });
245
- }
246
-
247
- const updated = content.replace(old_text, new_text);
248
- fs.writeFileSync(filePath, updated, "utf-8");
249
-
250
- // Reload skills
251
- try {
252
- const { loadAllSkills } = require("../skills");
253
- loadAllSkills();
254
- } catch {
255
- // skills.js may not be loadable in all contexts
256
- }
257
-
258
- return JSON.stringify({
259
- status: "patched",
260
- name,
261
- path: filePath,
262
- });
263
- },
264
- },
265
- {
266
- type: "function",
267
- function: {
268
- name: "learn_read",
269
- description: "Read the full content of an existing skill to review or patch it.",
270
- parameters: {
271
- type: "object",
272
- properties: {
273
- name: {
274
- type: "string",
275
- description: "Name of the skill to read",
276
- },
277
- },
278
- required: ["name"],
279
- },
280
- },
281
- execute: async (args) => {
282
- const { name } = args;
283
- if (!name) {
284
- return JSON.stringify({ status: "error", error: "name is required" });
285
- }
286
-
287
- const dir = getSkillsDir();
288
- let filePath = path.join(dir, `${name}.md`);
289
- if (!fs.existsSync(filePath)) {
290
- filePath = path.join(dir, `${name}.js`);
291
- }
292
- if (!fs.existsSync(filePath)) {
293
- return JSON.stringify({
294
- status: "error",
295
- error: `Skill "${name}" not found in ${dir}`,
296
- });
297
- }
298
-
299
- const content = fs.readFileSync(filePath, "utf-8");
300
- return JSON.stringify({ status: "ok", name, content });
301
- },
302
- },
303
- ],
304
- };