@n8n-as-code/n8nac 2026.3.1-next.12
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/CHANGELOG.md +7 -0
- package/README.md +163 -0
- package/index.ts +147 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +52 -0
- package/src/cli.ts +266 -0
- package/src/tool.ts +363 -0
- package/src/workspace.ts +31 -0
- package/tsconfig.json +18 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @n8n-as-code/n8nac
|
|
2
|
+
|
|
3
|
+
## [2026.3.1](https://github.com/EtienneLescot/n8n-as-code/compare/@n8n-as-code/n8nac@v2026.3.0...@n8n-as-code/n8nac@v2026.3.1) (2026-03-13)
|
|
4
|
+
|
|
5
|
+
### Documentation
|
|
6
|
+
|
|
7
|
+
* align editor and integration release messaging ([e1d6198](https://github.com/EtienneLescot/n8n-as-code/commit/e1d6198c3c6c942afe024f34b4ad419005ed991c))
|
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @n8n-as-code/n8nac
|
|
2
|
+
|
|
3
|
+
**OpenClaw-native access to the `n8n-as-code` workflow stack.**
|
|
4
|
+
|
|
5
|
+
Use OpenClaw to build, update, validate, and manage n8n workflows with the same `n8nac` CLI and AI context model used across the wider `n8n-as-code` project.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @n8n-as-code/n8nac
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If you previously installed `@n8n-as-code/openclaw-plugin`, remove the old install first so OpenClaw re-registers the plugin cleanly under `n8nac`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openclaw plugins uninstall n8nac
|
|
17
|
+
openclaw plugins install @n8n-as-code/n8nac
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Restart the gateway, then run the setup wizard:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
openclaw n8nac:setup
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The wizard asks for your n8n host URL and API key once, saves them via
|
|
27
|
+
`n8nac init-auth`, selects your project, and generates an AI context file
|
|
28
|
+
(`AGENTS.md`) in the workspace (`~/.openclaw/n8nac/`).
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Once setup is done, just talk to OpenClaw:
|
|
33
|
+
|
|
34
|
+
> "Create an n8n workflow that sends a Slack message when a GitHub issue is opened"
|
|
35
|
+
|
|
36
|
+
> "Pull workflow 42 and add an error handler to it"
|
|
37
|
+
|
|
38
|
+
> "What operations does the Google Sheets node support?"
|
|
39
|
+
|
|
40
|
+
The plugin injects the full n8n-architect instructions into every conversation
|
|
41
|
+
so the AI knows the exact `n8nac` workflow (init-check → pull → edit → push → verify).
|
|
42
|
+
|
|
43
|
+
## CLI commands
|
|
44
|
+
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `openclaw n8nac:setup` | Interactive setup wizard |
|
|
48
|
+
| `openclaw n8nac:status` | Show workspace status |
|
|
49
|
+
|
|
50
|
+
Options for `n8nac:setup`:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
--host <url> n8n host URL (skip prompt)
|
|
54
|
+
--api-key <key> n8n API key (skip prompt)
|
|
55
|
+
--project-index <n> Project to select non-interactively
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Workspace
|
|
59
|
+
|
|
60
|
+
All files live in `~/.openclaw/n8nac/`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
~/.openclaw/n8nac/
|
|
64
|
+
n8nac-config.json ← project binding (written by n8nac init-project)
|
|
65
|
+
AGENTS.md ← AI context (written by n8nac update-ai)
|
|
66
|
+
workflows/ ← .workflow.ts files (your n8n workflows)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Agent tool
|
|
70
|
+
|
|
71
|
+
The plugin registers the `n8nac` tool with these actions:
|
|
72
|
+
|
|
73
|
+
| Action | Description |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `setup_check` | Check initialization state |
|
|
76
|
+
| `init_auth` | Save n8n credentials |
|
|
77
|
+
| `init_project` | Select n8n project |
|
|
78
|
+
| `list` | List all workflows |
|
|
79
|
+
| `pull` | Download a workflow by ID |
|
|
80
|
+
| `push` | Upload a workflow file |
|
|
81
|
+
| `verify` | Validate live workflow against schema |
|
|
82
|
+
| `skills` | Run any `npx n8nac skills` subcommand |
|
|
83
|
+
| `validate` | Validate a local `.workflow.ts` file |
|
|
84
|
+
|
|
85
|
+
## Local development
|
|
86
|
+
|
|
87
|
+
This section covers how to load the plugin from source during development so
|
|
88
|
+
that changes take effect immediately without an npm publish cycle.
|
|
89
|
+
|
|
90
|
+
### 1. Link the plugin directory
|
|
91
|
+
|
|
92
|
+
OpenClaw's `--link` flag registers a local path instead of installing a copy.
|
|
93
|
+
jiti is used to run TypeScript directly, so no build step is needed.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
openclaw plugins install --link \
|
|
97
|
+
/home/etienne/repos/n8n-as-code/plugins/openclaw/n8n-as-code
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
What this does:
|
|
101
|
+
- Adds the path to `plugins.load.paths` in `~/.openclaw/openclaw.json`
|
|
102
|
+
- Registers a `source: "path"` install record bound to the plugin ID `n8nac`
|
|
103
|
+
- No file copy — OpenClaw loads `index.ts` directly from the source tree
|
|
104
|
+
|
|
105
|
+
### 2. Verify the plugin is registered
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
openclaw plugins info n8nac
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You should see status `loaded` and the tool `n8nac` in the tools list.
|
|
112
|
+
|
|
113
|
+
### 3. Run the setup wizard
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
openclaw n8nac:setup
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Enter your n8n host and API key when prompted. The wizard writes
|
|
120
|
+
`~/.openclaw/n8nac/n8nac-config.json` and generates `AGENTS.md`.
|
|
121
|
+
|
|
122
|
+
### 4. Iterate on the code
|
|
123
|
+
|
|
124
|
+
- Edit any `.ts` file in `plugins/openclaw/n8n-as-code/`
|
|
125
|
+
- **Restart the gateway** to reload: `openclaw stop && openclaw start` (or the
|
|
126
|
+
equivalent service restart on your setup)
|
|
127
|
+
- The `before_prompt_build` hook, tool schema, and CLI commands all reload on
|
|
128
|
+
gateway start
|
|
129
|
+
|
|
130
|
+
### 5. Check gateway logs
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
tail -f ~/.openclaw/logs/openclaw-$(date +%Y-%m-%d).log | grep n8nac
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The plugin prefixes all `api.logger` calls with `[n8nac]`.
|
|
137
|
+
|
|
138
|
+
### 6. Inspect the n8nac workspace
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
~/.openclaw/n8nac/
|
|
142
|
+
n8nac-config.json ← written by init-project
|
|
143
|
+
AGENTS.md ← written by update-ai
|
|
144
|
+
workflows/ ← .workflow.ts files
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To reset and redo setup from scratch:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
rm -rf ~/.openclaw/n8nac && openclaw n8nac:setup
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 7. Unlink when done
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
openclaw plugins uninstall n8nac
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Source
|
|
162
|
+
|
|
163
|
+
Part of the [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) monorepo.
|
package/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
|
+
import { registerN8nAcCli } from "./src/cli.js";
|
|
5
|
+
import { createN8nAcTool } from "./src/tool.js";
|
|
6
|
+
import { getWorkspaceDir, isWorkspaceInitialized } from "./src/workspace.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// AGENTS.md context injection — populated once on service start
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const BOOTSTRAP_CONTEXT = `\
|
|
13
|
+
## n8n-as-code — Bootstrap
|
|
14
|
+
|
|
15
|
+
The n8n-as-code plugin is installed but the workspace has not been initialized yet.
|
|
16
|
+
|
|
17
|
+
**Tell the user:**
|
|
18
|
+
> "To start building n8n workflows I need your n8n host URL and API key."
|
|
19
|
+
|
|
20
|
+
Once you have both, call the \`n8nac\` tool with \`action: "init_auth"\`, then
|
|
21
|
+
\`action: "init_project"\` to finish setup.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const MISSING_AGENTS_CONTEXT = `\
|
|
25
|
+
## n8n-as-code — AI Context Missing
|
|
26
|
+
|
|
27
|
+
The workspace is initialized, but the generated AI context file (\`AGENTS.md\`) is missing or unreadable.
|
|
28
|
+
|
|
29
|
+
**Tell the user:**
|
|
30
|
+
> "Your n8n-as-code workspace is connected, but the AI context needs to be regenerated before I can safely guide workflow changes."
|
|
31
|
+
|
|
32
|
+
Ask the user to run \`npx --yes n8nac update-ai\` in the OpenClaw workspace, or rerun
|
|
33
|
+
\`openclaw n8nac:setup\` if they want the setup wizard to repair it.
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
let agentsContext: string | null = null;
|
|
37
|
+
|
|
38
|
+
function readConfig(workspaceDir: string): Record<string, string> {
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(join(workspaceDir, "n8nac-config.json"), "utf-8");
|
|
41
|
+
return JSON.parse(raw) as Record<string, string>;
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildStatusHeader(workspaceDir: string): string {
|
|
48
|
+
const cfg = readConfig(workspaceDir);
|
|
49
|
+
const host = cfg.host ?? "(unknown)";
|
|
50
|
+
const project = cfg.projectName ?? cfg.projectId ?? "(unknown)";
|
|
51
|
+
return [
|
|
52
|
+
"## ✅ n8n-as-code Workspace Status",
|
|
53
|
+
"",
|
|
54
|
+
"**The workspace is already fully initialized. Do NOT ask the user for credentials.**",
|
|
55
|
+
"",
|
|
56
|
+
`- Workspace directory: \`${workspaceDir}\``,
|
|
57
|
+
`- n8n host: \`${host}\``,
|
|
58
|
+
`- Active project: \`${project}\``,
|
|
59
|
+
"",
|
|
60
|
+
"Skip the 'Workspace Bootstrap' section below — setup is complete.",
|
|
61
|
+
"Proceed directly to the user's request using the `n8nac` tool.",
|
|
62
|
+
"",
|
|
63
|
+
"---",
|
|
64
|
+
"",
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadAgentsContext(workspaceDir: string): string | null {
|
|
69
|
+
const p = join(workspaceDir, "AGENTS.md");
|
|
70
|
+
if (!existsSync(p)) return null;
|
|
71
|
+
try {
|
|
72
|
+
const raw = readFileSync(p, "utf-8");
|
|
73
|
+
return buildStatusHeader(workspaceDir) + raw;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Plugin
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const n8nAcPlugin = {
|
|
84
|
+
id: "n8nac",
|
|
85
|
+
name: "n8n-as-code",
|
|
86
|
+
description:
|
|
87
|
+
"Create and manage n8n workflows from OpenClaw using n8n-as-code (n8nac). " +
|
|
88
|
+
"Guides through workspace initialization, workflow CRUD, and AI-powered node schema lookup.",
|
|
89
|
+
|
|
90
|
+
register(api: OpenClawPluginApi) {
|
|
91
|
+
const workspaceDir = getWorkspaceDir();
|
|
92
|
+
|
|
93
|
+
// Ensure the plugin workspace directory always exists.
|
|
94
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// -- Context injection ---------------------------------------------------
|
|
97
|
+
// Prepend n8n-architect instructions to every prompt build.
|
|
98
|
+
api.on("before_prompt_build", () => {
|
|
99
|
+
const initialized = isWorkspaceInitialized(workspaceDir);
|
|
100
|
+
// Lazy-load: setup may have run after the gateway started, so the
|
|
101
|
+
// service start() missed it. Re-attempt on every prompt until loaded.
|
|
102
|
+
// The status header embeds host + project, so re-read on every call
|
|
103
|
+
// when not yet cached to pick up fresh config after setup.
|
|
104
|
+
if (agentsContext === null && initialized) {
|
|
105
|
+
agentsContext = loadAgentsContext(workspaceDir);
|
|
106
|
+
}
|
|
107
|
+
const context = agentsContext ?? (initialized ? MISSING_AGENTS_CONTEXT : BOOTSTRAP_CONTEXT);
|
|
108
|
+
if (!context) return;
|
|
109
|
+
return { prependContext: context };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// -- Agent tool ----------------------------------------------------------
|
|
113
|
+
api.registerTool(createN8nAcTool({ workspaceDir }));
|
|
114
|
+
|
|
115
|
+
// -- CLI wizard ----------------------------------------------------------
|
|
116
|
+
api.registerCli(
|
|
117
|
+
({ program }) => registerN8nAcCli({ program, workspaceDir }),
|
|
118
|
+
{ commands: ["n8nac:setup", "n8nac:status"] },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// -- Service -------------------------------------------------------------
|
|
122
|
+
// On gateway start: refresh the AGENTS.md cache so the agent always has
|
|
123
|
+
// up-to-date node knowledge.
|
|
124
|
+
api.registerService({
|
|
125
|
+
id: "n8nac-context",
|
|
126
|
+
start: async () => {
|
|
127
|
+
// Invalidate so next before_prompt_build re-reads from disk.
|
|
128
|
+
agentsContext = null;
|
|
129
|
+
if (isWorkspaceInitialized(workspaceDir)) {
|
|
130
|
+
agentsContext = loadAgentsContext(workspaceDir);
|
|
131
|
+
if (agentsContext) {
|
|
132
|
+
api.logger.info("[n8nac] Workspace ready — AI context loaded.");
|
|
133
|
+
} else {
|
|
134
|
+
api.logger.warn("[n8nac] Workspace ready, but AGENTS.md is missing or unreadable.");
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
api.logger.info("[n8nac] Workspace not initialized. Run `openclaw n8nac:setup`.");
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
stop: async () => {
|
|
141
|
+
agentsContext = null;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default n8nAcPlugin;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "n8nac",
|
|
3
|
+
"name": "n8n-as-code",
|
|
4
|
+
"description": "Create and manage n8n workflows from OpenClaw. Guides the AI through n8nac initialization and provides workflow tools.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"enabled": {
|
|
10
|
+
"type": "boolean",
|
|
11
|
+
"default": false,
|
|
12
|
+
"description": "Enable the n8n-as-code integration"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@n8n-as-code/n8nac",
|
|
3
|
+
"version": "2026.3.1-next.12",
|
|
4
|
+
"description": "OpenClaw plugin for n8n-as-code — create and manage n8n workflows from OpenClaw",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n",
|
|
7
|
+
"workflow",
|
|
8
|
+
"automation",
|
|
9
|
+
"openclaw",
|
|
10
|
+
"openclaw-plugin",
|
|
11
|
+
"n8nac"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/EtienneLescot/n8n-as-code.git",
|
|
16
|
+
"directory": "plugins/openclaw/n8n-as-code"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "npm run typecheck",
|
|
22
|
+
"clean": "rm -rf node_modules/.vitest",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^1.0.1",
|
|
28
|
+
"@sinclair/typebox": "0.34.48"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"openclaw": "^2026.2.26",
|
|
33
|
+
"typescript": "^5.8.0",
|
|
34
|
+
"vitest": "^1.6.1"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"README.md",
|
|
39
|
+
"index.ts",
|
|
40
|
+
"openclaw.plugin.json",
|
|
41
|
+
"src/",
|
|
42
|
+
"tsconfig.json"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"openclaw": ">=2026.2.0"
|
|
46
|
+
},
|
|
47
|
+
"openclaw": {
|
|
48
|
+
"extensions": [
|
|
49
|
+
"./index.ts"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import type { ChildProcess, ChildProcessWithoutNullStreams } from "node:child_process";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
6
|
+
import { isWorkspaceInitialized } from "./workspace.js";
|
|
7
|
+
|
|
8
|
+
type CliProgram = Parameters<Parameters<OpenClawPluginApi["registerCli"]>[0]>[0]["program"];
|
|
9
|
+
|
|
10
|
+
type CliOpts = {
|
|
11
|
+
program: CliProgram;
|
|
12
|
+
workspaceDir: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RunResult = {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
timedOut: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function runN8nac(
|
|
23
|
+
args: string[],
|
|
24
|
+
opts: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
timeout: number;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
stdio?: "pipe" | "inherit";
|
|
29
|
+
},
|
|
30
|
+
): Promise<RunResult> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const baseOptions = {
|
|
33
|
+
cwd: opts.cwd,
|
|
34
|
+
env: { ...process.env, ...opts.env },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const child: ChildProcess | ChildProcessWithoutNullStreams =
|
|
38
|
+
opts.stdio === "inherit"
|
|
39
|
+
? spawn("npx", ["--yes", "n8nac", ...args], { ...baseOptions, stdio: "inherit" })
|
|
40
|
+
: spawn("npx", ["--yes", "n8nac", ...args], { ...baseOptions, stdio: "pipe" });
|
|
41
|
+
|
|
42
|
+
let stdout = "";
|
|
43
|
+
let stderr = "";
|
|
44
|
+
let timedOut = false;
|
|
45
|
+
let settled = false;
|
|
46
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
47
|
+
|
|
48
|
+
const finish = (result: RunResult) => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
if (killTimer) clearTimeout(killTimer);
|
|
53
|
+
resolve(result);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if ("stdout" in child && child.stdout) {
|
|
57
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
58
|
+
stdout += chunk.toString();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if ("stderr" in child && child.stderr) {
|
|
63
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
64
|
+
stderr += chunk.toString();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
timedOut = true;
|
|
70
|
+
child.kill("SIGTERM");
|
|
71
|
+
killTimer = setTimeout(() => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
child.kill("SIGKILL");
|
|
74
|
+
finish({ stdout, stderr: stderr || "Process timed out.", exitCode: 1, timedOut: true });
|
|
75
|
+
}, 2_000);
|
|
76
|
+
}, opts.timeout);
|
|
77
|
+
|
|
78
|
+
child.on("error", (error: Error) => {
|
|
79
|
+
finish({ stdout, stderr: error.message || stderr, exitCode: 1, timedOut });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on("close", (code: number | null) => {
|
|
83
|
+
finish({ stdout, stderr, exitCode: code ?? 1, timedOut });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function registerN8nAcCli({ program, workspaceDir }: CliOpts): void {
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
// n8nac:status — quick health check
|
|
91
|
+
// -------------------------------------------------------------------------
|
|
92
|
+
program
|
|
93
|
+
.command("n8nac:status")
|
|
94
|
+
.description("Show n8n-as-code workspace status")
|
|
95
|
+
.action(() => {
|
|
96
|
+
const initialized = isWorkspaceInitialized(workspaceDir);
|
|
97
|
+
console.log(`\nn8n-as-code workspace: ${workspaceDir}`);
|
|
98
|
+
console.log(`Status: ${initialized ? "✓ Initialized" : "✗ Not initialized"}`);
|
|
99
|
+
if (!initialized) {
|
|
100
|
+
console.log("\nRun `openclaw n8nac:setup` to connect your n8n instance.");
|
|
101
|
+
}
|
|
102
|
+
console.log();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// n8nac:setup — interactive wizard
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
program
|
|
109
|
+
.command("n8nac:setup")
|
|
110
|
+
.description("Initialize or reconfigure the n8n-as-code workspace")
|
|
111
|
+
.option("--host <url>", "n8n host URL (skip prompt)")
|
|
112
|
+
.option("--api-key <key>", "n8n API key (skip prompt)")
|
|
113
|
+
.option("--project-index <n>", "Project index to select non-interactively")
|
|
114
|
+
.action(async (opts: { host?: string; apiKey?: string; projectIndex?: string }) => {
|
|
115
|
+
p.intro("n8n-as-code setup");
|
|
116
|
+
|
|
117
|
+
// Ensure workspace dir exists.
|
|
118
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
119
|
+
p.log.info(`Workspace: ${workspaceDir}`);
|
|
120
|
+
|
|
121
|
+
// ------------------------------------------------------------------
|
|
122
|
+
// Step 1: Collect credentials
|
|
123
|
+
// ------------------------------------------------------------------
|
|
124
|
+
let host = opts.host ?? "";
|
|
125
|
+
if (!host) {
|
|
126
|
+
const answer = await p.text({
|
|
127
|
+
message: "n8n host URL",
|
|
128
|
+
placeholder: "https://your-n8n.example.com",
|
|
129
|
+
validate: (v) => (v && v.startsWith("http") ? undefined : "Must start with http:// or https://"),
|
|
130
|
+
});
|
|
131
|
+
if (p.isCancel(answer)) {
|
|
132
|
+
p.cancel("Setup cancelled.");
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
host = answer as string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let apiKey = opts.apiKey ?? "";
|
|
139
|
+
if (!apiKey) {
|
|
140
|
+
const answer = await p.password({
|
|
141
|
+
message: "n8n API key",
|
|
142
|
+
validate: (v) => (v && v.length > 0 ? undefined : "API key cannot be empty"),
|
|
143
|
+
});
|
|
144
|
+
if (p.isCancel(answer)) {
|
|
145
|
+
p.cancel("Setup cancelled.");
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
apiKey = answer as string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ------------------------------------------------------------------
|
|
152
|
+
// Step 2: init-auth
|
|
153
|
+
// ------------------------------------------------------------------
|
|
154
|
+
const authSpinner = p.spinner();
|
|
155
|
+
authSpinner.start("Saving credentials…");
|
|
156
|
+
|
|
157
|
+
const authResult = await runN8nac(["init-auth", "--host", host], {
|
|
158
|
+
cwd: workspaceDir,
|
|
159
|
+
timeout: 60_000,
|
|
160
|
+
env: { N8N_API_KEY: apiKey },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (authResult.exitCode !== 0) {
|
|
164
|
+
authSpinner.stop("Failed to save credentials.");
|
|
165
|
+
if (authResult.timedOut) {
|
|
166
|
+
p.log.error("n8nac init-auth timed out.");
|
|
167
|
+
}
|
|
168
|
+
p.log.error(authResult.stderr || authResult.stdout || "Unknown error.");
|
|
169
|
+
p.outro("Setup failed. Check your host URL and API key and try again.");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
authSpinner.stop("Credentials saved ✓");
|
|
173
|
+
|
|
174
|
+
// ------------------------------------------------------------------
|
|
175
|
+
// Step 3: init-project
|
|
176
|
+
// If the user passed --project-index, run non-interactively.
|
|
177
|
+
// Otherwise inherit stdio so n8nac's own interactive project picker can appear.
|
|
178
|
+
// ------------------------------------------------------------------
|
|
179
|
+
const projectSpinner = p.spinner();
|
|
180
|
+
const projectArgs = ["init-project", "--sync-folder", "workflows"];
|
|
181
|
+
let projectResult: RunResult;
|
|
182
|
+
|
|
183
|
+
if (opts.projectIndex) {
|
|
184
|
+
const projectIdx = Number.parseInt(opts.projectIndex, 10);
|
|
185
|
+
if (!Number.isInteger(projectIdx) || projectIdx < 1) {
|
|
186
|
+
p.log.error("--project-index must be a positive integer.");
|
|
187
|
+
p.outro("Setup failed.");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
projectSpinner.start("Selecting project…");
|
|
191
|
+
projectResult = await runN8nac([...projectArgs, "--project-index", String(projectIdx)], {
|
|
192
|
+
cwd: workspaceDir,
|
|
193
|
+
timeout: 120_000,
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
projectSpinner.start("Opening project picker…");
|
|
197
|
+
projectSpinner.stop("Project picker ready");
|
|
198
|
+
projectResult = await runN8nac(projectArgs, {
|
|
199
|
+
cwd: workspaceDir,
|
|
200
|
+
timeout: 120_000,
|
|
201
|
+
stdio: "inherit",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (projectResult.exitCode !== 0) {
|
|
206
|
+
projectSpinner.stop("Failed to select project.");
|
|
207
|
+
if (projectResult.timedOut) {
|
|
208
|
+
p.log.error("n8nac init-project timed out.");
|
|
209
|
+
}
|
|
210
|
+
p.log.error(projectResult.stderr || projectResult.stdout || "Unknown error.");
|
|
211
|
+
p.log.info("If you have multiple projects, rerun with --project-index <n>.");
|
|
212
|
+
p.outro("Setup failed.");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
projectSpinner.stop("Project selected ✓");
|
|
216
|
+
|
|
217
|
+
// ------------------------------------------------------------------
|
|
218
|
+
// Step 4: update-ai — generate AGENTS.md
|
|
219
|
+
// ------------------------------------------------------------------
|
|
220
|
+
const aiSpinner = p.spinner();
|
|
221
|
+
aiSpinner.start("Generating AI context (AGENTS.md)…");
|
|
222
|
+
const aiResult = await runN8nac(["update-ai"], {
|
|
223
|
+
cwd: workspaceDir,
|
|
224
|
+
timeout: 60_000,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (aiResult.exitCode !== 0) {
|
|
228
|
+
aiSpinner.stop("Failed to generate AI context.");
|
|
229
|
+
if (aiResult.timedOut) {
|
|
230
|
+
p.log.error("n8nac update-ai timed out.");
|
|
231
|
+
}
|
|
232
|
+
p.log.error(aiResult.stderr || aiResult.stdout || "Unknown error.");
|
|
233
|
+
p.outro(
|
|
234
|
+
"Setup partially completed: credentials and project were saved, but AGENTS.md generation failed. " +
|
|
235
|
+
"Run `npx --yes n8nac update-ai` after fixing the issue.",
|
|
236
|
+
);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
aiSpinner.stop("AI context ready ✓");
|
|
240
|
+
|
|
241
|
+
p.log.step("What's next?");
|
|
242
|
+
p.log.message(
|
|
243
|
+
[
|
|
244
|
+
" 1. Restart the OpenClaw gateway to activate the plugin:",
|
|
245
|
+
" openclaw gateway restart",
|
|
246
|
+
"",
|
|
247
|
+
" 2. Ask OpenClaw to create a workflow in plain language, for example:",
|
|
248
|
+
' "Create an n8n workflow that sends a Slack message every morning"',
|
|
249
|
+
"",
|
|
250
|
+
" 3. Useful commands:",
|
|
251
|
+
" openclaw n8nac:status — check workspace + connection health",
|
|
252
|
+
" openclaw n8nac:setup — reconfigure host / API key",
|
|
253
|
+
"",
|
|
254
|
+
" 4. Manage workflows directly:",
|
|
255
|
+
" npx n8nac list — list local & remote workflows",
|
|
256
|
+
" npx n8nac pull <id> — download a workflow from n8n",
|
|
257
|
+
" npx n8nac push <file> — upload a workflow to n8n",
|
|
258
|
+
].join("\n"),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
p.outro(
|
|
262
|
+
`Setup complete!\n` +
|
|
263
|
+
`Workspace: ${workspaceDir}`,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { isWorkspaceInitialized } from "./workspace.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Schema
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const ACTIONS = [
|
|
10
|
+
"setup_check",
|
|
11
|
+
"init_auth",
|
|
12
|
+
"init_project",
|
|
13
|
+
"list",
|
|
14
|
+
"pull",
|
|
15
|
+
"push",
|
|
16
|
+
"verify",
|
|
17
|
+
"skills",
|
|
18
|
+
"validate",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
const LIST_SCOPES = ["all", "local", "remote", "distant"] as const;
|
|
22
|
+
|
|
23
|
+
const N8nAcToolSchema = Type.Object({
|
|
24
|
+
action: Type.Unsafe<(typeof ACTIONS)[number]>({
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: [...ACTIONS],
|
|
27
|
+
description: [
|
|
28
|
+
"Action to perform:",
|
|
29
|
+
" setup_check — check whether the workspace is initialized.",
|
|
30
|
+
" init_auth — save n8n credentials. Requires n8nHost and n8nApiKey.",
|
|
31
|
+
" init_project — select the n8n project. Optionally pass projectId, projectName, or projectIndex (1-based, default 1).",
|
|
32
|
+
" list — list all workflows with their sync status.",
|
|
33
|
+
" pull — download a workflow from n8n. Requires workflowId.",
|
|
34
|
+
" push — upload a local workflow file. Requires filename (e.g. my-flow.workflow.ts).",
|
|
35
|
+
" verify — fetch a workflow from n8n and validate it. Requires workflowId.",
|
|
36
|
+
" skills — run any n8nac skills subcommand. Requires skillsArgs (e.g. 'search telegram' or 'node-info googleSheets').",
|
|
37
|
+
" validate — validate a local workflow file. Requires validateFile.",
|
|
38
|
+
].join("\n"),
|
|
39
|
+
}),
|
|
40
|
+
// init_auth
|
|
41
|
+
n8nHost: Type.Optional(
|
|
42
|
+
Type.String({ description: "n8n host URL (for init_auth). Example: https://your-n8n.example.com" }),
|
|
43
|
+
),
|
|
44
|
+
n8nApiKey: Type.Optional(Type.String({ description: "n8n API key (for init_auth)" })),
|
|
45
|
+
// init_project
|
|
46
|
+
projectId: Type.Optional(Type.String({ description: "n8n project ID (for init_project)" })),
|
|
47
|
+
projectName: Type.Optional(Type.String({ description: "n8n project name (for init_project)" })),
|
|
48
|
+
projectIndex: Type.Optional(
|
|
49
|
+
Type.Number({ description: "n8n project index, 1-based (for init_project, default: 1)" }),
|
|
50
|
+
),
|
|
51
|
+
listScope: Type.Optional(
|
|
52
|
+
Type.Unsafe<(typeof LIST_SCOPES)[number]>({
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: [...LIST_SCOPES],
|
|
55
|
+
description: "Workflow list scope (for list). One of: all, local, remote, distant.",
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
// pull / verify
|
|
59
|
+
workflowId: Type.Optional(Type.String({ description: "Workflow ID (for pull, verify)" })),
|
|
60
|
+
// push
|
|
61
|
+
filename: Type.Optional(
|
|
62
|
+
Type.String({
|
|
63
|
+
description:
|
|
64
|
+
"Workflow filename including .workflow.ts extension (for push). " +
|
|
65
|
+
"Example: my-flow.workflow.ts. Do NOT pass a path.",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
// skills
|
|
69
|
+
skillsArgs: Type.Optional(
|
|
70
|
+
Type.String({
|
|
71
|
+
description:
|
|
72
|
+
"Arguments for the n8nac skills subcommand (for skills action). " +
|
|
73
|
+
"Examples: 'search telegram', 'node-info googleSheets', 'examples search slack', 'docs OpenAI'",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
skillsArgv: Type.Optional(
|
|
77
|
+
Type.Array(Type.String(), {
|
|
78
|
+
description:
|
|
79
|
+
"Array form of arguments for the n8nac skills subcommand (preferred when values contain spaces). " +
|
|
80
|
+
'Example: ["examples", "search", "slack notification"]',
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
// validate
|
|
84
|
+
validateFile: Type.Optional(
|
|
85
|
+
Type.String({ description: "Workflow file path to validate (for validate action)" }),
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Helpers
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
type RunResult = {
|
|
94
|
+
stdout: string;
|
|
95
|
+
stderr: string;
|
|
96
|
+
exitCode: number;
|
|
97
|
+
timedOut: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function runNpx(
|
|
101
|
+
args: string[],
|
|
102
|
+
cwd: string,
|
|
103
|
+
env?: NodeJS.ProcessEnv,
|
|
104
|
+
): Promise<RunResult> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const child = spawn("npx", ["--yes", "n8nac", ...args], {
|
|
107
|
+
cwd,
|
|
108
|
+
env: { ...process.env, ...env },
|
|
109
|
+
stdio: "pipe",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let stdout = "";
|
|
113
|
+
let stderr = "";
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
let settled = false;
|
|
116
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
117
|
+
|
|
118
|
+
const finish = (result: RunResult) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
if (killTimer) clearTimeout(killTimer);
|
|
123
|
+
resolve(result);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
child.stdout.on("data", (chunk) => {
|
|
127
|
+
stdout += chunk.toString();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
child.stderr.on("data", (chunk) => {
|
|
131
|
+
stderr += chunk.toString();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
timedOut = true;
|
|
136
|
+
child.kill("SIGTERM");
|
|
137
|
+
killTimer = setTimeout(() => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
child.kill("SIGKILL");
|
|
140
|
+
finish({ stdout, stderr: stderr || "Process timed out.", exitCode: 1, timedOut: true });
|
|
141
|
+
}, 2_000);
|
|
142
|
+
}, 120_000);
|
|
143
|
+
|
|
144
|
+
child.on("error", (error) => {
|
|
145
|
+
finish({ stdout, stderr: error.message || stderr, exitCode: 1, timedOut });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.on("close", (code) => {
|
|
149
|
+
finish({ stdout, stderr, exitCode: code ?? 1, timedOut });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ok(data: unknown) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
157
|
+
details: data,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function str(v: unknown): string {
|
|
162
|
+
return typeof v === "string" ? v.trim() : "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function splitArgv(input: string): string[] | null {
|
|
166
|
+
const args: string[] = [];
|
|
167
|
+
let current = "";
|
|
168
|
+
let quote: '"' | "'" | null = null;
|
|
169
|
+
let escaping = false;
|
|
170
|
+
|
|
171
|
+
for (const ch of input) {
|
|
172
|
+
if (escaping) {
|
|
173
|
+
current += ch;
|
|
174
|
+
escaping = false;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ch === "\\" && quote !== "'") {
|
|
179
|
+
escaping = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (quote) {
|
|
184
|
+
if (ch === quote) {
|
|
185
|
+
quote = null;
|
|
186
|
+
} else {
|
|
187
|
+
current += ch;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (ch === '"' || ch === "'") {
|
|
193
|
+
quote = ch;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (/\s/.test(ch)) {
|
|
198
|
+
if (current) {
|
|
199
|
+
args.push(current);
|
|
200
|
+
current = "";
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
current += ch;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (escaping) current += "\\";
|
|
209
|
+
if (quote) return null;
|
|
210
|
+
if (current) args.push(current);
|
|
211
|
+
return args;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Tool factory
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
export function createN8nAcTool(opts: { workspaceDir: string }) {
|
|
219
|
+
const { workspaceDir } = opts;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
name: "n8nac",
|
|
223
|
+
label: "n8n-as-code",
|
|
224
|
+
description:
|
|
225
|
+
"Create and manage n8n workflows using n8n-as-code. " +
|
|
226
|
+
"Handles workspace initialization (init_auth → init_project), " +
|
|
227
|
+
"workflow sync (list, pull, push, verify), and AI knowledge lookup (skills, validate). " +
|
|
228
|
+
"Always call setup_check first to determine initialization state.",
|
|
229
|
+
parameters: N8nAcToolSchema,
|
|
230
|
+
|
|
231
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
232
|
+
const action = str(params.action);
|
|
233
|
+
|
|
234
|
+
// ---- setup_check --------------------------------------------------
|
|
235
|
+
if (action === "setup_check") {
|
|
236
|
+
const initialized = isWorkspaceInitialized(workspaceDir);
|
|
237
|
+
return ok({
|
|
238
|
+
initialized,
|
|
239
|
+
workspaceDir,
|
|
240
|
+
next: initialized
|
|
241
|
+
? "Workspace is ready. Use list, pull, push, verify, or skills."
|
|
242
|
+
: "Workspace not initialized. Ask the user for their n8n host URL and API key, then call init_auth.",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---- init_auth ----------------------------------------------------
|
|
247
|
+
if (action === "init_auth") {
|
|
248
|
+
const host = str(params.n8nHost);
|
|
249
|
+
const key = str(params.n8nApiKey);
|
|
250
|
+
if (!host || !key) {
|
|
251
|
+
return ok({ error: "n8nHost and n8nApiKey are required for init_auth" });
|
|
252
|
+
}
|
|
253
|
+
const r = await runNpx(["init-auth", "--host", host], workspaceDir, { N8N_API_KEY: key });
|
|
254
|
+
if (r.exitCode !== 0) {
|
|
255
|
+
return ok({ error: r.stderr || r.stdout, exitCode: r.exitCode });
|
|
256
|
+
}
|
|
257
|
+
return ok({
|
|
258
|
+
ok: true,
|
|
259
|
+
output: r.stdout,
|
|
260
|
+
next: "Credentials saved. Now call init_project. If you need to inspect remote workflows first, use list with listScope: 'remote'.",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- init_project -------------------------------------------------
|
|
265
|
+
if (action === "init_project") {
|
|
266
|
+
const id = str(params.projectId);
|
|
267
|
+
const name = str(params.projectName);
|
|
268
|
+
const idx = typeof params.projectIndex === "number" ? params.projectIndex : 1;
|
|
269
|
+
const args: string[] = ["init-project", "--sync-folder", "workflows"];
|
|
270
|
+
if (id) args.push("--project-id", id);
|
|
271
|
+
else if (name) args.push("--project-name", name);
|
|
272
|
+
else args.push("--project-index", String(idx));
|
|
273
|
+
|
|
274
|
+
const r = await runNpx(args, workspaceDir);
|
|
275
|
+
if (r.exitCode !== 0) {
|
|
276
|
+
return ok({ error: r.stderr || r.stdout, exitCode: r.exitCode });
|
|
277
|
+
}
|
|
278
|
+
// Refresh AGENTS.md after successful init
|
|
279
|
+
const ai = await runNpx(["update-ai"], workspaceDir);
|
|
280
|
+
if (ai.exitCode !== 0) {
|
|
281
|
+
return ok({
|
|
282
|
+
ok: true,
|
|
283
|
+
output: r.stdout,
|
|
284
|
+
warning: ai.stderr || ai.stdout || "AGENTS.md regeneration failed.",
|
|
285
|
+
next:
|
|
286
|
+
"Workspace initialized, but AI context regeneration failed. Run `npx --yes n8nac update-ai` before relying on agent-guided workflow work.",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return ok({
|
|
290
|
+
ok: true,
|
|
291
|
+
output: r.stdout,
|
|
292
|
+
next: "Workspace initialized. AGENTS.md regenerated. You can now list, pull, push, and verify workflows.",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---- list ---------------------------------------------------------
|
|
297
|
+
if (action === "list") {
|
|
298
|
+
const scope = str(params.listScope) || "all";
|
|
299
|
+
const args = ["list"];
|
|
300
|
+
if (scope === "local" || scope === "remote" || scope === "distant") {
|
|
301
|
+
args.push(`--${scope}`);
|
|
302
|
+
}
|
|
303
|
+
const r = await runNpx(args, workspaceDir);
|
|
304
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- pull ---------------------------------------------------------
|
|
308
|
+
if (action === "pull") {
|
|
309
|
+
const id = str(params.workflowId);
|
|
310
|
+
if (!id) return ok({ error: "workflowId is required for pull" });
|
|
311
|
+
const r = await runNpx(["pull", id], workspaceDir);
|
|
312
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- push ---------------------------------------------------------
|
|
316
|
+
if (action === "push") {
|
|
317
|
+
const file = str(params.filename);
|
|
318
|
+
if (!file) return ok({ error: "filename is required for push (e.g. my-flow.workflow.ts)" });
|
|
319
|
+
const r = await runNpx(["push", file, "--verify"], workspaceDir);
|
|
320
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- verify -------------------------------------------------------
|
|
324
|
+
if (action === "verify") {
|
|
325
|
+
const id = str(params.workflowId);
|
|
326
|
+
if (!id) return ok({ error: "workflowId is required for verify" });
|
|
327
|
+
const r = await runNpx(["verify", id], workspaceDir);
|
|
328
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- skills -------------------------------------------------------
|
|
332
|
+
if (action === "skills") {
|
|
333
|
+
const skillsArgv = Array.isArray(params.skillsArgv)
|
|
334
|
+
? params.skillsArgv.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
335
|
+
: [];
|
|
336
|
+
const skillsArgs = str(params.skillsArgs);
|
|
337
|
+
if (!skillsArgv.length && !skillsArgs) {
|
|
338
|
+
return ok({
|
|
339
|
+
error:
|
|
340
|
+
"skillsArgv or skillsArgs is required. Examples: skillsArgv: ['examples', 'search', 'slack notification']",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const parsedArgs = skillsArgv.length ? skillsArgv : splitArgv(skillsArgs);
|
|
344
|
+
if (!parsedArgs) {
|
|
345
|
+
return ok({ error: "skillsArgs contains an unterminated quote. Prefer skillsArgv when values contain spaces." });
|
|
346
|
+
}
|
|
347
|
+
const args = ["skills", ...parsedArgs];
|
|
348
|
+
const r = await runNpx(args, workspaceDir);
|
|
349
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- validate -----------------------------------------------------
|
|
353
|
+
if (action === "validate") {
|
|
354
|
+
const file = str(params.validateFile);
|
|
355
|
+
if (!file) return ok({ error: "validateFile is required for validate" });
|
|
356
|
+
const r = await runNpx(["skills", "validate", file], workspaceDir);
|
|
357
|
+
return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return ok({ error: `Unknown action: ${action}` });
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fixed workspace directory for V1.
|
|
7
|
+
* All n8nac files (n8nac-config.json, AGENTS.md, workflows/) live here.
|
|
8
|
+
*/
|
|
9
|
+
export function getWorkspaceDir(): string {
|
|
10
|
+
return join(homedir(), ".openclaw", "n8nac");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true when n8nac has been initialized in the given directory,
|
|
15
|
+
* meaning the config exists and contains a selected project + sync folder.
|
|
16
|
+
*/
|
|
17
|
+
export function isWorkspaceInitialized(workspaceDir: string): boolean {
|
|
18
|
+
const configPath = join(workspaceDir, "n8nac-config.json");
|
|
19
|
+
if (!existsSync(configPath)) return false;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
23
|
+
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
24
|
+
const projectId = typeof config.projectId === "string" ? config.projectId.trim() : "";
|
|
25
|
+
const projectName = typeof config.projectName === "string" ? config.projectName.trim() : "";
|
|
26
|
+
const syncFolder = typeof config.syncFolder === "string" ? config.syncFolder.trim() : "";
|
|
27
|
+
return projectId.length > 0 && projectName.length > 0 && syncFolder.length > 0;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowImportingTsExtensions": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
|
+
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
|
8
|
+
"module": "NodeNext",
|
|
9
|
+
"moduleResolution": "NodeNext",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"target": "es2023"
|
|
15
|
+
},
|
|
16
|
+
"include": ["index.ts", "src/**/*", "tests/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|