create-ironclaws 1.0.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 +101 -0
- package/bin/create.js +394 -0
- package/package.json +33 -0
- package/template/.env.example +38 -0
- package/template/CLAUDE.md +104 -0
- package/template/agent-credentials.yaml +33 -0
- package/template/agents.yaml +22 -0
- package/template/container/Dockerfile +70 -0
- package/template/container/Dockerfile.argus +34 -0
- package/template/container/agent-runner/package-lock.json +1524 -0
- package/template/container/agent-runner/package.json +23 -0
- package/template/container/agent-runner/src/index.ts +630 -0
- package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
- package/template/container/agent-runner/tsconfig.json +15 -0
- package/template/container/build-argus.sh +25 -0
- package/template/container/build.sh +23 -0
- package/template/container/skills/agent-browser/SKILL.md +159 -0
- package/template/container/skills/agent-status/SKILL.md +69 -0
- package/template/container/skills/capabilities/SKILL.md +100 -0
- package/template/container/skills/edit-agent/SKILL.md +93 -0
- package/template/container/skills/slack-formatting/SKILL.md +92 -0
- package/template/container/skills/status/SKILL.md +104 -0
- package/template/container/tools/elastic_query.py +161 -0
- package/template/container/tools/gdrive_tool.py +185 -0
- package/template/container/tools/jira_tool.py +433 -0
- package/template/container/tools/slack_history_tool.py +144 -0
- package/template/container/tools/youtube_tool.py +174 -0
- package/template/docker-compose.yml +54 -0
- package/template/docs/how-it-works.md +496 -0
- package/template/eslint.config.js +32 -0
- package/template/groups/forge/CLAUDE.md +107 -0
- package/template/package-lock.json +5278 -0
- package/template/package.json +52 -0
- package/template/scripts/github-app-token.py +58 -0
- package/template/scripts/register-expense-agent.sh +121 -0
- package/template/scripts/run-migrations.ts +105 -0
- package/template/scripts/setup-onecli-secrets.sh +252 -0
- package/template/setup-agents.sh +142 -0
- package/template/src/channels/index.ts +13 -0
- package/template/src/channels/registry.test.ts +42 -0
- package/template/src/channels/registry.ts +28 -0
- package/template/src/channels/slack.test.ts +859 -0
- package/template/src/channels/slack.ts +373 -0
- package/template/src/claw-skill.test.ts +45 -0
- package/template/src/config.ts +94 -0
- package/template/src/container-runner.test.ts +221 -0
- package/template/src/container-runner.ts +1029 -0
- package/template/src/container-runtime.test.ts +149 -0
- package/template/src/container-runtime.ts +124 -0
- package/template/src/db-migration.test.ts +67 -0
- package/template/src/db.test.ts +484 -0
- package/template/src/db.ts +837 -0
- package/template/src/env.ts +42 -0
- package/template/src/formatting.test.ts +294 -0
- package/template/src/github-token.ts +48 -0
- package/template/src/google-token.ts +75 -0
- package/template/src/group-folder.test.ts +43 -0
- package/template/src/group-folder.ts +44 -0
- package/template/src/group-queue.test.ts +484 -0
- package/template/src/group-queue.ts +363 -0
- package/template/src/http-server.ts +343 -0
- package/template/src/index.ts +960 -0
- package/template/src/ipc-auth.test.ts +679 -0
- package/template/src/ipc.ts +548 -0
- package/template/src/logger.ts +16 -0
- package/template/src/mount-security.ts +421 -0
- package/template/src/network-policy.ts +119 -0
- package/template/src/remote-control.test.ts +397 -0
- package/template/src/remote-control.ts +224 -0
- package/template/src/router.ts +52 -0
- package/template/src/routing.test.ts +170 -0
- package/template/src/sender-allowlist.test.ts +216 -0
- package/template/src/sender-allowlist.ts +128 -0
- package/template/src/task-scheduler.test.ts +129 -0
- package/template/src/task-scheduler.ts +290 -0
- package/template/src/timezone.test.ts +73 -0
- package/template/src/timezone.ts +37 -0
- package/template/src/types.ts +114 -0
- package/template/src/worktree.ts +206 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Clone Manager for NanoClaw
|
|
3
|
+
*
|
|
4
|
+
* Creates per-group isolated clones so container agents work on a clean copy
|
|
5
|
+
* of the production repository, never the user's local checkout.
|
|
6
|
+
*
|
|
7
|
+
* Clone strategy:
|
|
8
|
+
* - Always clones from the GitHub remote (not local disk).
|
|
9
|
+
* The remote URL is read from GITHUB_REPO in config, authenticated with
|
|
10
|
+
* a fresh GitHub App token. This ensures agents always see production code
|
|
11
|
+
* even when NanoClaw runs on a VM with no local repo present.
|
|
12
|
+
* - On every subsequent run the clone is fetched from GitHub and reset to
|
|
13
|
+
* the latest origin/main, so agents never operate on stale code.
|
|
14
|
+
*
|
|
15
|
+
* Fails fast if no GitHub token can be generated — no point starting an
|
|
16
|
+
* agent that won't be able to push its work.
|
|
17
|
+
*
|
|
18
|
+
* All git commands use execFileSync (no shell) to prevent injection via
|
|
19
|
+
* paths or branch names containing shell metacharacters.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'child_process';
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
|
|
25
|
+
import { DATA_DIR } from './config.js';
|
|
26
|
+
import { readEnvFile } from './env.js';
|
|
27
|
+
import { generateGitHubToken } from './github-token.js';
|
|
28
|
+
import { logger } from './logger.js';
|
|
29
|
+
|
|
30
|
+
const WORKTREES_DIR = path.join(DATA_DIR, 'worktrees');
|
|
31
|
+
|
|
32
|
+
const GIT_OPTS: { encoding: 'utf-8'; stdio: ['pipe', 'pipe', 'pipe'] } = {
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build an authenticated GitHub clone URL from GITHUB_REPO config + token.
|
|
39
|
+
* Returns null if GITHUB_REPO or the token is missing.
|
|
40
|
+
*/
|
|
41
|
+
function buildGitHubUrl(token: string): string | null {
|
|
42
|
+
const githubRepo = readEnvFile(['GITHUB_REPO']).GITHUB_REPO;
|
|
43
|
+
if (!githubRepo) return null;
|
|
44
|
+
return `https://x-access-token:${token}@github.com/${githubRepo}.git`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the remote URL from a local repo path.
|
|
49
|
+
* Returns null if the path is not a git repo or has no origin remote.
|
|
50
|
+
*/
|
|
51
|
+
function getRemoteUrl(repoPath: string): string | null {
|
|
52
|
+
if (!fs.existsSync(repoPath)) return null;
|
|
53
|
+
try {
|
|
54
|
+
return execFileSync(
|
|
55
|
+
'git', ['remote', 'get-url', 'origin'],
|
|
56
|
+
{ ...GIT_OPTS, cwd: repoPath },
|
|
57
|
+
).trim();
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build git env with the GitHub App token for authenticated fetches.
|
|
65
|
+
* Throws if no valid token can be generated — fail fast so the agent
|
|
66
|
+
* doesn't do work it can't push.
|
|
67
|
+
*/
|
|
68
|
+
async function getGitAuthEnv(): Promise<{ env: Record<string, string>; token: string }> {
|
|
69
|
+
const envConfig = readEnvFile(['GITHUB_TOKEN']);
|
|
70
|
+
const token = await generateGitHubToken(envConfig.GITHUB_TOKEN);
|
|
71
|
+
|
|
72
|
+
if (!token) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'No GitHub token available — cannot clone from GitHub. ' +
|
|
75
|
+
'Check GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, github-app.pem, or GITHUB_TOKEN in .env',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
token,
|
|
81
|
+
env: {
|
|
82
|
+
GH_TOKEN: token,
|
|
83
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
84
|
+
GIT_CONFIG_COUNT: '1',
|
|
85
|
+
GIT_CONFIG_KEY_0: 'credential.helper',
|
|
86
|
+
GIT_CONFIG_VALUE_0: `!f() { echo "username=x-access-token"; echo "password=${token}"; }; f`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ensure an isolated clone exists for a given group and source repo.
|
|
93
|
+
* Always fetches from GitHub and resets to latest origin/main so agents
|
|
94
|
+
* work on production code regardless of what's on local disk.
|
|
95
|
+
* Returns the absolute path to the clone directory.
|
|
96
|
+
*
|
|
97
|
+
* Throws if the GitHub App token can't be generated — fail fast.
|
|
98
|
+
*/
|
|
99
|
+
export async function ensureWorktree(
|
|
100
|
+
sourceRepoPath: string,
|
|
101
|
+
groupFolder: string,
|
|
102
|
+
containerPath: string,
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
const cloneBase = path.join(WORKTREES_DIR, groupFolder);
|
|
105
|
+
const cloneDir = path.join(cloneBase, containerPath);
|
|
106
|
+
|
|
107
|
+
fs.mkdirSync(cloneBase, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const { env: gitAuthEnv, token } = await getGitAuthEnv();
|
|
110
|
+
const execEnv = { ...process.env, ...gitAuthEnv };
|
|
111
|
+
const defaultBranch = 'main';
|
|
112
|
+
|
|
113
|
+
const dotGit = path.join(cloneDir, '.git');
|
|
114
|
+
// Must be a directory (self-contained clone), not a file (old worktree reference).
|
|
115
|
+
// Worktree .git files contain host paths that don't resolve inside containers.
|
|
116
|
+
if (fs.existsSync(dotGit) && fs.statSync(dotGit).isDirectory()) {
|
|
117
|
+
// Clone exists — fetch latest from GitHub and reset to clean state
|
|
118
|
+
logger.info(
|
|
119
|
+
{ groupFolder, cloneDir },
|
|
120
|
+
'Fetching latest from GitHub and resetting to origin/main',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
execFileSync('git', ['fetch', 'origin'], {
|
|
125
|
+
...GIT_OPTS,
|
|
126
|
+
cwd: cloneDir,
|
|
127
|
+
timeout: 120_000,
|
|
128
|
+
env: execEnv,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
execFileSync('git', ['checkout', '--detach', `origin/${defaultBranch}`], {
|
|
132
|
+
...GIT_OPTS,
|
|
133
|
+
cwd: cloneDir,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
execFileSync('git', ['clean', '-fd'], {
|
|
137
|
+
...GIT_OPTS,
|
|
138
|
+
cwd: cloneDir,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
execFileSync('git', ['checkout', '--', '.'], {
|
|
142
|
+
...GIT_OPTS,
|
|
143
|
+
cwd: cloneDir,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
logger.info({ groupFolder, cloneDir }, 'Clone reset to latest origin/main');
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.warn(
|
|
149
|
+
{ groupFolder, cloneDir, err },
|
|
150
|
+
'Failed to reset clone, recreating',
|
|
151
|
+
);
|
|
152
|
+
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
153
|
+
return createClone(sourceRepoPath, token, cloneDir, defaultBranch, groupFolder, execEnv);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// If cloneDir exists but .git is a file (stale worktree) or missing, wipe and recreate
|
|
157
|
+
if (fs.existsSync(cloneDir)) {
|
|
158
|
+
logger.warn({ groupFolder, cloneDir }, 'Removing stale clone directory');
|
|
159
|
+
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
return createClone(sourceRepoPath, token, cloneDir, defaultBranch, groupFolder, execEnv);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return cloneDir;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createClone(
|
|
168
|
+
sourceRepoPath: string,
|
|
169
|
+
token: string,
|
|
170
|
+
cloneDir: string,
|
|
171
|
+
defaultBranch: string,
|
|
172
|
+
groupFolder: string,
|
|
173
|
+
execEnv: Record<string, string | undefined>,
|
|
174
|
+
): string {
|
|
175
|
+
logger.info(
|
|
176
|
+
{ groupFolder, cloneDir, defaultBranch },
|
|
177
|
+
'Creating fresh clone from GitHub',
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
fs.mkdirSync(path.dirname(cloneDir), { recursive: true });
|
|
181
|
+
|
|
182
|
+
if (fs.existsSync(cloneDir)) {
|
|
183
|
+
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Determine the GitHub URL to clone from.
|
|
187
|
+
// Prefer GITHUB_REPO config (works on any machine, including the VM).
|
|
188
|
+
// Fall back to reading the remote URL from the local repo if present.
|
|
189
|
+
const cloneUrl = buildGitHubUrl(token) ?? getRemoteUrl(sourceRepoPath);
|
|
190
|
+
if (!cloneUrl) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Cannot determine GitHub URL for clone of ${sourceRepoPath}. ` +
|
|
193
|
+
'Set GITHUB_REPO in .env or ensure the source repo has an origin remote.',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Shallow clone of latest main — fast and gives agents exactly what's in production.
|
|
198
|
+
execFileSync('git', ['clone', '--depth', '1', '--branch', defaultBranch, cloneUrl, cloneDir], {
|
|
199
|
+
...GIT_OPTS,
|
|
200
|
+
timeout: 120_000,
|
|
201
|
+
env: execEnv,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
logger.info({ groupFolder, cloneDir }, 'Clone created at latest origin/main');
|
|
205
|
+
return cloneDir;
|
|
206
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|