tsic-ainode 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 +74 -0
- package/bin/index.js +299 -0
- package/locales/en.json +26 -0
- package/locales/zh-TW.json +26 -0
- package/package.json +33 -0
- package/templates/antigravity/AGENTS.md +80 -0
- package/templates/claude-code/SKILL.md +139 -0
- package/templates/codex/AGENTS.md +80 -0
- package/templates/cursor/tsic-ainode.mdc +58 -0
- package/templates/gemini-cli/GEMINI.md +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# TSIC AINode Skill Installer
|
|
2
|
+
|
|
3
|
+
為你的 AI 工具安裝 **AINode Dongle SSH 連線技能**,讓 Claude Code、Cursor、Codex、Gemini CLI、Antigravity 等 AI Agent 能自動偵測 AINODE USB Dongle 並安全連線到 AINode miniPC。
|
|
4
|
+
|
|
5
|
+
## 安裝
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx tsic-ainode@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
互動式安裝器會:
|
|
12
|
+
1. 偵測你已安裝的 AI 工具
|
|
13
|
+
2. 詢問語言(繁中 / English)
|
|
14
|
+
3. 詢問安裝範圍(全域 / 目前專案)
|
|
15
|
+
4. 安裝對應的 Skill 設定檔
|
|
16
|
+
|
|
17
|
+
## 支援的 AI 工具
|
|
18
|
+
|
|
19
|
+
| 工具 | 安裝位置(全域) | 安裝位置(專案) |
|
|
20
|
+
|------|----------------|----------------|
|
|
21
|
+
| Claude Code | `~/.claude/skills/tsic-ainode/SKILL.md` | 同左 |
|
|
22
|
+
| Cursor | `~/.cursor/rules/tsic-ainode.mdc` | `.cursor/rules/tsic-ainode.mdc` |
|
|
23
|
+
| OpenAI Codex CLI | `~/.codex/AGENTS.md` | `./AGENTS.md` |
|
|
24
|
+
| Gemini CLI | `~/.gemini/GEMINI.md` | `./GEMINI.md` |
|
|
25
|
+
| Antigravity | `~/.antigravity/AGENTS.md` | `./.antigravity/AGENTS.md` |
|
|
26
|
+
|
|
27
|
+
## 使用方式
|
|
28
|
+
|
|
29
|
+
安裝後,插入 **AINODE USB Dongle**,對你的 AI 工具說:
|
|
30
|
+
|
|
31
|
+
- 「部署到 AINode」
|
|
32
|
+
- 「連線 AINode」
|
|
33
|
+
- 「把服務跑在 AINode 上」
|
|
34
|
+
- "deploy to AINode"
|
|
35
|
+
- "connect to AINode"
|
|
36
|
+
|
|
37
|
+
AI Agent 會自動:
|
|
38
|
+
1. 偵測 AINODE 磁碟(Windows: `E:\`、macOS: `/Volumes/AINODE`、Linux: `blkid -L AINODE`)
|
|
39
|
+
2. 使用 Dongle 上的私鑰(`ID_ED255`)透過 `SSHCFG` 設定連線
|
|
40
|
+
3. 執行部署、查詢日誌、管理 Docker 服務等操作
|
|
41
|
+
|
|
42
|
+
## AINode Dongle 安全設計
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Dongle Flash (加密)
|
|
46
|
+
[AES-128-CBC 加密私鑰] ← 開機時解密到 RAM
|
|
47
|
+
[公鑰] ← 首次配對時寫入 AINode authorized_keys
|
|
48
|
+
|
|
49
|
+
Dongle 磁碟 (AINODE, FAT12 64KB)
|
|
50
|
+
SSHCFG ← SSH 設定(Host / HostName / Port)
|
|
51
|
+
ID_ED255 ← 私鑰(RAM,拔除即消失)
|
|
52
|
+
ID_ED255.PUB ← 公鑰
|
|
53
|
+
README.TXT
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
私鑰只存在 Dongle 的 RAM 中,拔除即無法連線 — 這是設計行為。
|
|
57
|
+
|
|
58
|
+
## 即將推出
|
|
59
|
+
|
|
60
|
+
- **MCP Server**:AINode 將提供 MCP 接入口,AI Agent 可直接查詢服務狀態、觸發部署
|
|
61
|
+
- **AI 優化 REST API**:`http://192.168.1.100:8080/api/v1/` 結構化端點
|
|
62
|
+
- **Skill 自動更新**:重新執行 `npx tsic-ainode@latest` 即可更新
|
|
63
|
+
|
|
64
|
+
## 手動安裝(不用 npx)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/TSIC-tech/TSIC-AINode.git
|
|
68
|
+
cd TSIC-AINode
|
|
69
|
+
node bin/index.js
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TSIC AINode Skill Installer
|
|
4
|
+
* Usage: npx tsic-ainode@latest
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Paths
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
19
|
+
const LOCALES_DIR = path.join(PKG_ROOT, 'locales');
|
|
20
|
+
const TMPL_DIR = path.join(PKG_ROOT, 'templates');
|
|
21
|
+
const HOME = os.homedir();
|
|
22
|
+
const CWD = process.cwd();
|
|
23
|
+
const IS_WIN = process.platform === 'win32';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// i18n
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
let T = {};
|
|
29
|
+
function loadLocale(lang) {
|
|
30
|
+
const file = path.join(LOCALES_DIR, `${lang}.json`);
|
|
31
|
+
const fallback = path.join(LOCALES_DIR, 'en.json');
|
|
32
|
+
try {
|
|
33
|
+
T = JSON.parse(fs.readFileSync(fs.existsSync(file) ? file : fallback, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
T = {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function t(key, fallback = key) {
|
|
40
|
+
return T[key] || fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Detect OS locale
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
function detectLocale() {
|
|
47
|
+
const env = process.env.LANG || process.env.LC_ALL || process.env.LANGUAGE || '';
|
|
48
|
+
if (env.toLowerCase().includes('zh')) return 'zh-TW';
|
|
49
|
+
return 'en';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Tool definitions
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
const TOOLS = [
|
|
56
|
+
{
|
|
57
|
+
id: 'claude',
|
|
58
|
+
nameKey: 'tool_claude',
|
|
59
|
+
detect: () => fs.existsSync(path.join(HOME, '.claude')),
|
|
60
|
+
install: installClaude,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'cursor',
|
|
64
|
+
nameKey: 'tool_cursor',
|
|
65
|
+
detect: () => fs.existsSync(path.join(HOME, '.cursor')) ||
|
|
66
|
+
fs.existsSync(path.join(CWD, '.cursor')),
|
|
67
|
+
install: installCursor,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'codex',
|
|
71
|
+
nameKey: 'tool_codex',
|
|
72
|
+
detect: () => cmdExists('codex') || fs.existsSync(path.join(HOME, '.codex')),
|
|
73
|
+
install: installCodex,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'gemini',
|
|
77
|
+
nameKey: 'tool_gemini',
|
|
78
|
+
detect: () => cmdExists('gemini') || fs.existsSync(path.join(HOME, '.gemini')),
|
|
79
|
+
install: installGemini,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'antigravity',
|
|
83
|
+
nameKey: 'tool_antigravity',
|
|
84
|
+
detect: () => cmdExists('antigravity') ||
|
|
85
|
+
fs.existsSync(path.join(HOME, '.antigravity')),
|
|
86
|
+
install: installAntigravity,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function cmdExists(cmd) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(IS_WIN ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Install functions
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
function copyTemplate(src, dest) {
|
|
103
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
104
|
+
fs.copyFileSync(src, dest);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function appendToFile(src, dest) {
|
|
108
|
+
const tag = '<!-- tsic-ainode -->';
|
|
109
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
110
|
+
if (fs.existsSync(dest)) {
|
|
111
|
+
const existing = fs.readFileSync(dest, 'utf8');
|
|
112
|
+
if (existing.includes(tag)) return; // already installed
|
|
113
|
+
fs.appendFileSync(dest, `\n\n${tag}\n${content}`);
|
|
114
|
+
} else {
|
|
115
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
116
|
+
fs.writeFileSync(dest, content);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function installClaude(scope) {
|
|
121
|
+
const dest = path.join(HOME, '.claude', 'skills', 'tsic-ainode', 'SKILL.md');
|
|
122
|
+
copyTemplate(path.join(TMPL_DIR, 'claude-code', 'SKILL.md'), dest);
|
|
123
|
+
return dest;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function installCursor(scope) {
|
|
127
|
+
const rulesDir = scope === 'global'
|
|
128
|
+
? path.join(HOME, '.cursor', 'rules')
|
|
129
|
+
: path.join(CWD, '.cursor', 'rules');
|
|
130
|
+
const dest = path.join(rulesDir, 'tsic-ainode.mdc');
|
|
131
|
+
copyTemplate(path.join(TMPL_DIR, 'cursor', 'tsic-ainode.mdc'), dest);
|
|
132
|
+
return dest;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function installCodex(scope) {
|
|
136
|
+
const dest = scope === 'global'
|
|
137
|
+
? path.join(HOME, '.codex', 'AGENTS.md')
|
|
138
|
+
: path.join(CWD, 'AGENTS.md');
|
|
139
|
+
appendToFile(path.join(TMPL_DIR, 'codex', 'AGENTS.md'), dest);
|
|
140
|
+
return dest;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installGemini(scope) {
|
|
144
|
+
const dest = scope === 'global'
|
|
145
|
+
? path.join(HOME, '.gemini', 'GEMINI.md')
|
|
146
|
+
: path.join(CWD, 'GEMINI.md');
|
|
147
|
+
appendToFile(path.join(TMPL_DIR, 'gemini-cli', 'GEMINI.md'), dest);
|
|
148
|
+
return dest;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function installAntigravity(scope) {
|
|
152
|
+
const dest = scope === 'global'
|
|
153
|
+
? path.join(HOME, '.antigravity', 'AGENTS.md')
|
|
154
|
+
: path.join(CWD, '.antigravity', 'AGENTS.md');
|
|
155
|
+
appendToFile(path.join(TMPL_DIR, 'antigravity', 'AGENTS.md'), dest);
|
|
156
|
+
return dest;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Readline helpers
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
function createRL() {
|
|
163
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ask(rl, question) {
|
|
167
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function askChoice(rl, question, choices) {
|
|
171
|
+
return new Promise(resolve => {
|
|
172
|
+
const opts = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
|
|
173
|
+
rl.question(`${question}\n${opts}\n> `, ans => {
|
|
174
|
+
const idx = parseInt(ans.trim(), 10) - 1;
|
|
175
|
+
resolve(choices[Math.max(0, Math.min(idx, choices.length - 1))]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function askCheckbox(rl, question, items) {
|
|
181
|
+
console.log(`\n${question}`);
|
|
182
|
+
const selected = new Set(items.filter(i => i.detected).map(i => i.id));
|
|
183
|
+
items.forEach((item, i) => {
|
|
184
|
+
const check = selected.has(item.id) ? '◉' : '○';
|
|
185
|
+
const hint = item.detected ? ' (detected)' : '';
|
|
186
|
+
console.log(` ${i + 1}. ${check} ${item.label}${hint}`);
|
|
187
|
+
});
|
|
188
|
+
console.log(' (Enter numbers to toggle, e.g. "1 3", or Enter to confirm)');
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const ans = await ask(rl, '> ');
|
|
192
|
+
if (ans.trim() === '') break;
|
|
193
|
+
ans.trim().split(/\s+/).forEach(n => {
|
|
194
|
+
const idx = parseInt(n, 10) - 1;
|
|
195
|
+
if (idx >= 0 && idx < items.length) {
|
|
196
|
+
const id = items[idx].id;
|
|
197
|
+
selected.has(id) ? selected.delete(id) : selected.add(id);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// redraw
|
|
201
|
+
items.forEach((item, i) => {
|
|
202
|
+
const check = selected.has(item.id) ? '◉' : '○';
|
|
203
|
+
process.stdout.write(` ${i + 1}. ${check} ${item.label}\n`);
|
|
204
|
+
});
|
|
205
|
+
console.log(' (Enter to confirm, or toggle more)');
|
|
206
|
+
}
|
|
207
|
+
return items.filter(i => selected.has(i.id));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Main
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
async function main() {
|
|
214
|
+
// Load default locale first
|
|
215
|
+
let locale = detectLocale();
|
|
216
|
+
loadLocale(locale);
|
|
217
|
+
|
|
218
|
+
const rl = createRL();
|
|
219
|
+
|
|
220
|
+
// Banner
|
|
221
|
+
console.log('\n' + '─'.repeat(50));
|
|
222
|
+
console.log(` ${t('welcome')}`);
|
|
223
|
+
console.log(` ${t('welcome_sub')}`);
|
|
224
|
+
console.log('─'.repeat(50));
|
|
225
|
+
|
|
226
|
+
// Language selection
|
|
227
|
+
const langChoice = await askChoice(rl, `\n${t('lang_prompt')}`, [
|
|
228
|
+
'繁體中文 (zh-TW)',
|
|
229
|
+
'English (en)',
|
|
230
|
+
]);
|
|
231
|
+
locale = langChoice.startsWith('繁') ? 'zh-TW' : 'en';
|
|
232
|
+
loadLocale(locale);
|
|
233
|
+
|
|
234
|
+
// Detect tools
|
|
235
|
+
const detectedIds = TOOLS.filter(t => t.detect()).map(t => t.id);
|
|
236
|
+
if (detectedIds.length > 0) {
|
|
237
|
+
console.log(`\n${T.detect_title}`);
|
|
238
|
+
detectedIds.forEach(id => {
|
|
239
|
+
const tool = TOOLS.find(t => t.id === id);
|
|
240
|
+
console.log(` ✓ ${T[tool.nameKey] || tool.id}`);
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`\n${T.detect_none}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Choose tools
|
|
247
|
+
const toolItems = TOOLS.map(tool => ({
|
|
248
|
+
id: tool.id,
|
|
249
|
+
label: T[tool.nameKey] || tool.id,
|
|
250
|
+
detected: detectedIds.includes(tool.id),
|
|
251
|
+
install: tool.install,
|
|
252
|
+
}));
|
|
253
|
+
const chosen = await askCheckbox(rl, T.tools_prompt, toolItems);
|
|
254
|
+
|
|
255
|
+
if (chosen.length === 0) {
|
|
256
|
+
console.log('\nNo tools selected. Exiting.');
|
|
257
|
+
rl.close();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Scope
|
|
262
|
+
const scopeChoice = await askChoice(rl, `\n${T.scope_prompt}`, [
|
|
263
|
+
T.scope_global,
|
|
264
|
+
T.scope_project,
|
|
265
|
+
]);
|
|
266
|
+
const scope = scopeChoice === T.scope_global ? 'global' : 'project';
|
|
267
|
+
|
|
268
|
+
// Install
|
|
269
|
+
console.log(`\n${T.installing}`);
|
|
270
|
+
const results = [];
|
|
271
|
+
for (const tool of chosen) {
|
|
272
|
+
try {
|
|
273
|
+
const dest = tool.install(scope);
|
|
274
|
+
console.log(` ${T.installed_ok}: ${tool.label}`);
|
|
275
|
+
console.log(` → ${dest}`);
|
|
276
|
+
results.push({ tool, dest, ok: true });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error(` ✗ ${T.err_write}: ${tool.label} — ${err.message}`);
|
|
279
|
+
results.push({ tool, ok: false });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Done
|
|
284
|
+
console.log('\n' + '─'.repeat(50));
|
|
285
|
+
console.log(` ${T.done_title}`);
|
|
286
|
+
console.log('─'.repeat(50));
|
|
287
|
+
console.log(`\n${T.done_usage}`);
|
|
288
|
+
console.log(` ${T.done_example_zh}`);
|
|
289
|
+
console.log(` ${T.done_example_en}`);
|
|
290
|
+
console.log(`\n${T.done_docs} https://github.com/TSIC-tech/TSIC-AINode`);
|
|
291
|
+
console.log();
|
|
292
|
+
|
|
293
|
+
rl.close();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
main().catch(err => {
|
|
297
|
+
console.error('Error:', err.message);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
package/locales/en.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": "🔐 TSIC AINode Skill Installer",
|
|
3
|
+
"welcome_sub": "Install AINode Dongle connection skills for your AI tools",
|
|
4
|
+
"lang_prompt": "Language / 語言:",
|
|
5
|
+
"detect_title": "Detected AI tools:",
|
|
6
|
+
"detect_none": "(No tools detected — showing all options)",
|
|
7
|
+
"tools_prompt": "Select tools to install (space to toggle, Enter to confirm):",
|
|
8
|
+
"scope_prompt": "Installation scope:",
|
|
9
|
+
"scope_global": "Global (~/.claude/skills, ~/.cursor/rules, etc.)",
|
|
10
|
+
"scope_project": "Current project folder (.cursor/rules, AGENTS.md, etc.)",
|
|
11
|
+
"installing": "Installing...",
|
|
12
|
+
"installed_ok": "✓ Installed",
|
|
13
|
+
"installed_skip": "- Skipped",
|
|
14
|
+
"done_title": "Installation complete!",
|
|
15
|
+
"done_usage": "Usage: insert the AINODE Dongle, then tell your AI tool:",
|
|
16
|
+
"done_example_zh": "「部署到 AINode」「連線 AINode」",
|
|
17
|
+
"done_example_en": "\"deploy to AINode\", \"connect to AINode\", \"run service on AINode\"",
|
|
18
|
+
"done_docs": "Docs:",
|
|
19
|
+
"err_node": "Node.js 16+ required",
|
|
20
|
+
"err_write": "Write failed",
|
|
21
|
+
"tool_claude": "Claude Code",
|
|
22
|
+
"tool_cursor": "Cursor",
|
|
23
|
+
"tool_codex": "OpenAI Codex CLI",
|
|
24
|
+
"tool_gemini": "Gemini CLI",
|
|
25
|
+
"tool_antigravity": "Antigravity"
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": "🔐 TSIC AINode Skill 安裝器",
|
|
3
|
+
"welcome_sub": "為你的 AI 工具安裝 AINode Dongle 連線技能",
|
|
4
|
+
"lang_prompt": "語言 / Language:",
|
|
5
|
+
"detect_title": "偵測到以下 AI 工具:",
|
|
6
|
+
"detect_none": "(未偵測到任何工具,將顯示所有選項)",
|
|
7
|
+
"tools_prompt": "選擇要安裝的工具(空白鍵切換,Enter 確認):",
|
|
8
|
+
"scope_prompt": "安裝範圍:",
|
|
9
|
+
"scope_global": "全域(~/.claude/skills、~/.cursor/rules 等)",
|
|
10
|
+
"scope_project": "目前專案資料夾(.cursor/rules、AGENTS.md 等)",
|
|
11
|
+
"installing": "安裝中...",
|
|
12
|
+
"installed_ok": "✓ 已安裝",
|
|
13
|
+
"installed_skip": "- 略過",
|
|
14
|
+
"done_title": "安裝完成!",
|
|
15
|
+
"done_usage": "使用方式:插入 AINODE Dongle 後,在你的 AI 工具中說:",
|
|
16
|
+
"done_example_zh": "「部署到 AINode」、「連線 AINode」、「把服務跑在 AINode 上」",
|
|
17
|
+
"done_example_en": "\"deploy to AINode\", \"connect to AINode\", \"run service on AINode\"",
|
|
18
|
+
"done_docs": "詳細說明:",
|
|
19
|
+
"err_node": "需要 Node.js 16 以上版本",
|
|
20
|
+
"err_write": "寫入失敗",
|
|
21
|
+
"tool_claude": "Claude Code",
|
|
22
|
+
"tool_cursor": "Cursor",
|
|
23
|
+
"tool_codex": "OpenAI Codex CLI",
|
|
24
|
+
"tool_gemini": "Gemini CLI",
|
|
25
|
+
"tool_antigravity": "Antigravity"
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tsic-ainode",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TSIC AINode Skill installer — adds AINode SSH Dongle support to Claude Code, Cursor, Codex, Gemini CLI, and Antigravity",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tsic-ainode": "bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ainode",
|
|
13
|
+
"tsic",
|
|
14
|
+
"ssh",
|
|
15
|
+
"dongle",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"cursor",
|
|
18
|
+
"codex",
|
|
19
|
+
"gemini",
|
|
20
|
+
"skill",
|
|
21
|
+
"mcp"
|
|
22
|
+
],
|
|
23
|
+
"author": "TSIC",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"templates/",
|
|
31
|
+
"locales/"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# TSIC AINode — Antigravity Agent Instructions
|
|
2
|
+
|
|
3
|
+
## AINode Connection Context
|
|
4
|
+
|
|
5
|
+
When working with this project, you may need to connect to a **TSIC AINode miniPC**
|
|
6
|
+
(`192.168.1.100`) — a local edge server running Docker and Cloudflare Tunnel.
|
|
7
|
+
|
|
8
|
+
Authentication is hardware-based: the **AINODE USB Security Dongle** contains the
|
|
9
|
+
SSH private key in AES-128-CBC encrypted Flash. The key decrypts to RAM at boot;
|
|
10
|
+
removing the Dongle immediately revokes access.
|
|
11
|
+
|
|
12
|
+
## Detecting the Dongle
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Windows (PowerShell)
|
|
16
|
+
(Get-WmiObject Win32_LogicalDisk | Where-Object { $_.VolumeName -eq "AINODE" }).DeviceID
|
|
17
|
+
|
|
18
|
+
# macOS
|
|
19
|
+
DONGLE=$(ls /Volumes | grep -x AINODE && echo /Volumes/AINODE || echo "")
|
|
20
|
+
|
|
21
|
+
# Linux
|
|
22
|
+
DONGLE=$(blkid -L AINODE 2>/dev/null | xargs -I{} findmnt -n -o TARGET {})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Connecting
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# With Dongle
|
|
29
|
+
ssh -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" AINode
|
|
30
|
+
|
|
31
|
+
# Fallback (no Dongle)
|
|
32
|
+
ssh AINode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Service Deployment
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 1. Build archive
|
|
39
|
+
tar -czf /tmp/service.tar.gz \
|
|
40
|
+
--exclude='.git' --exclude='node_modules' \
|
|
41
|
+
--exclude='__pycache__' --exclude='.env' .
|
|
42
|
+
|
|
43
|
+
# 2. Transfer
|
|
44
|
+
scp -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" \
|
|
45
|
+
/tmp/service.tar.gz AINode:~/services/$(basename $PWD)/
|
|
46
|
+
|
|
47
|
+
# 3. Deploy
|
|
48
|
+
ssh -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" AINode \
|
|
49
|
+
"cd ~/services/$(basename $PWD) && \
|
|
50
|
+
tar -xzf *.tar.gz && \
|
|
51
|
+
docker compose up -d --build && \
|
|
52
|
+
docker ps"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Key Rules
|
|
56
|
+
|
|
57
|
+
- Always detect Dongle **before** attempting SSH operations
|
|
58
|
+
- Never copy or persist `ID_ED255` outside the Dongle
|
|
59
|
+
- Ask the user before uploading `.env` files
|
|
60
|
+
- If connection fails: check Dongle is mounted, not just plugged in
|
|
61
|
+
|
|
62
|
+
## Future: MCP & AI Endpoints (Coming Soon)
|
|
63
|
+
|
|
64
|
+
AINode is being extended with AI-native access interfaces:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
MCP Server
|
|
68
|
+
Transport : SSH (no extra network exposure)
|
|
69
|
+
Auth : AINODE Dongle key
|
|
70
|
+
Tools : list_services, get_logs, deploy, get_metrics
|
|
71
|
+
|
|
72
|
+
REST API (http://192.168.1.100:8080/api/v1/)
|
|
73
|
+
GET /status
|
|
74
|
+
GET /services
|
|
75
|
+
POST /deploy { service, image, env }
|
|
76
|
+
GET /logs/{service}?tail=100
|
|
77
|
+
WS /logs/{service}/stream
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
These will be documented in future versions of this skill file.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tsic-ainode
|
|
3
|
+
description: 連線到 TSIC AINode miniPC 並管理服務。偵測 AINODE USB Security Dongle,透過 SSH 部署 Docker 服務、查詢日誌、設定 Cloudflare Tunnel。未來支援 MCP 端點與 AI 優化查詢接入口。觸發詞:「連線 AINode」「部署到 AINode」「AINode 上跑」「AINode ssh」「deploy to AINode」。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TSIC AINode Skill
|
|
7
|
+
|
|
8
|
+
透過 **AINODE USB Security Dongle** 安全連線到 AINode miniPC(192.168.1.100)。
|
|
9
|
+
私鑰儲存在 Dongle 的加密 Flash 中,拔除 Dongle 即無法連線。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Step 0:偵測 AINODE Dongle
|
|
14
|
+
|
|
15
|
+
### Windows(PowerShell)
|
|
16
|
+
```powershell
|
|
17
|
+
Get-WmiObject Win32_LogicalDisk | Where-Object { $_.VolumeName -eq "AINODE" } | Select-Object DeviceID
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### macOS
|
|
21
|
+
```bash
|
|
22
|
+
ls /Volumes/AINODE 2>/dev/null && echo found || echo not_found
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Linux
|
|
26
|
+
```bash
|
|
27
|
+
blkid -L AINODE 2>/dev/null || findmnt -l | grep AINODE | awk '{print $1}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**找到 Dongle(卷標 AINODE):**
|
|
31
|
+
```
|
|
32
|
+
DONGLE_PATH = <掛載路徑> # e.g. E:\ (Windows) / /Volumes/AINODE (macOS)
|
|
33
|
+
SSH_KEY = {DONGLE_PATH}/ID_ED255
|
|
34
|
+
SSH_CONFIG = {DONGLE_PATH}/SSHCFG
|
|
35
|
+
SSH_CMD = ssh -F {SSH_CONFIG} -i {SSH_KEY} AINode
|
|
36
|
+
SCP_CMD = scp -F {SSH_CONFIG} -i {SSH_KEY}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**找不到 Dongle → Fallback:**
|
|
40
|
+
```
|
|
41
|
+
SSH_CMD = ssh AINode # 使用 ~/.ssh/config 中的 AINode alias
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Step 1:確認連線
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
{SSH_CMD} -o BatchMode=yes "echo ok && whoami && uptime"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
若失敗:
|
|
53
|
+
- 有 Dongle → 確認 Dongle 已掛載,`ID_ED255` 檔案存在
|
|
54
|
+
- 無 Dongle → 確認 `~/.ssh/config` 有 `Host AINode` 設定
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 2:部署 Docker 服務
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 打包並上傳專案
|
|
62
|
+
tar -czf /tmp/{service}.tar.gz --exclude='.git' --exclude='node_modules' --exclude='.env' .
|
|
63
|
+
{SCP_CMD} /tmp/{service}.tar.gz AINode:~/services/{service}/
|
|
64
|
+
|
|
65
|
+
# 解壓並啟動
|
|
66
|
+
{SSH_CMD} "cd ~/services/{service} && tar -xzf {service}.tar.gz && docker compose up -d --build"
|
|
67
|
+
|
|
68
|
+
# 確認狀態
|
|
69
|
+
{SSH_CMD} "docker ps | grep {service}"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Step 3:常用操作
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 查看容器 log
|
|
78
|
+
{SSH_CMD} "docker logs -f {service}"
|
|
79
|
+
|
|
80
|
+
# 重新部署
|
|
81
|
+
{SSH_CMD} "cd ~/services/{service} && docker compose up -d --build"
|
|
82
|
+
|
|
83
|
+
# 停止服務
|
|
84
|
+
{SSH_CMD} "docker compose -f ~/services/{service}/docker-compose.yml down"
|
|
85
|
+
|
|
86
|
+
# 系統資源
|
|
87
|
+
{SSH_CMD} "df -h && free -h && docker stats --no-stream"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Step 4:Cloudflare Tunnel(選用)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 快速臨時 URL(測試用)
|
|
96
|
+
{SSH_CMD} "nohup cloudflared tunnel --url http://localhost:{port} > ~/cf.log 2>&1 &"
|
|
97
|
+
{SSH_CMD} "sleep 3 && grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' ~/cf.log | head -1"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 【預留】AINode AI 接入口(即將推出)
|
|
103
|
+
|
|
104
|
+
> 以下功能正在開發中,未來版本會在此 Skill 更新。
|
|
105
|
+
|
|
106
|
+
### MCP Server
|
|
107
|
+
```
|
|
108
|
+
AINode 內建 MCP Server,AI Agent 可直接查詢:
|
|
109
|
+
- 服務狀態(Docker containers)
|
|
110
|
+
- 系統資源(CPU / RAM / Disk)
|
|
111
|
+
- 部署歷史
|
|
112
|
+
- 網路設定
|
|
113
|
+
|
|
114
|
+
連線方式(待更新):
|
|
115
|
+
mcp connect ainode --via-ssh --key {SSH_KEY}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### AI 優化 API 端點
|
|
119
|
+
```
|
|
120
|
+
未來將提供 REST / gRPC 端點,針對 AI Agent 查詢優化:
|
|
121
|
+
http://192.168.1.100:8080/api/v1/
|
|
122
|
+
- GET /status 系統狀態快照
|
|
123
|
+
- GET /services 所有 Docker 服務列表
|
|
124
|
+
- POST /deploy 觸發部署流程
|
|
125
|
+
- GET /logs/{service} 結構化 log 輸出
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## AINode 規格參考
|
|
131
|
+
|
|
132
|
+
| 項目 | 規格 |
|
|
133
|
+
|------|------|
|
|
134
|
+
| 主機 IP | 192.168.1.100 |
|
|
135
|
+
| 連線方式 | RJ45 直連(LAN2)|
|
|
136
|
+
| SSH Port | 22 |
|
|
137
|
+
| 預設使用者 | 見 Dongle SSHCFG |
|
|
138
|
+
| Docker | docker compose v2 |
|
|
139
|
+
| Tunnel | cloudflared |
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# TSIC AINode — OpenAI Codex Agent Instructions
|
|
2
|
+
|
|
3
|
+
## AINode Connection
|
|
4
|
+
|
|
5
|
+
This project may interact with a **TSIC AINode miniPC** (192.168.1.100) via SSH.
|
|
6
|
+
The AINode uses an **AINODE USB Security Dongle** for authentication — the private key
|
|
7
|
+
lives on the Dongle's encrypted Flash and never exists as a plain file on disk.
|
|
8
|
+
|
|
9
|
+
### Detect Dongle
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Windows
|
|
13
|
+
(Get-WmiObject Win32_LogicalDisk | Where-Object {$_.VolumeName -eq "AINODE"}).DeviceID
|
|
14
|
+
|
|
15
|
+
# macOS
|
|
16
|
+
ls /Volumes/AINODE 2>/dev/null && echo "found" || echo "not_found"
|
|
17
|
+
|
|
18
|
+
# Linux
|
|
19
|
+
blkid -L AINODE 2>/dev/null
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### SSH Commands
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# With Dongle (preferred)
|
|
26
|
+
DONGLE=/Volumes/AINODE # macOS example; adjust per OS
|
|
27
|
+
ssh -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" AINode "<command>"
|
|
28
|
+
|
|
29
|
+
# Without Dongle (fallback — requires ~/.ssh/config Host AINode entry)
|
|
30
|
+
ssh AINode "<command>"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Deploy a Service
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Package project
|
|
37
|
+
tar -czf /tmp/app.tar.gz --exclude='.git' --exclude='node_modules' --exclude='.env' .
|
|
38
|
+
|
|
39
|
+
# Upload
|
|
40
|
+
scp -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" /tmp/app.tar.gz AINode:~/services/app/
|
|
41
|
+
|
|
42
|
+
# Start with Docker Compose
|
|
43
|
+
ssh -F "$DONGLE/SSHCFG" -i "$DONGLE/ID_ED255" AINode \
|
|
44
|
+
"cd ~/services/app && tar -xzf app.tar.gz && docker compose up -d --build"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Common Operations
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Check running services
|
|
51
|
+
ssh [...] AINode "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
|
52
|
+
|
|
53
|
+
# Tail logs
|
|
54
|
+
ssh [...] AINode "docker logs -f <service> --tail 50"
|
|
55
|
+
|
|
56
|
+
# System health
|
|
57
|
+
ssh [...] AINode "df -h / && free -h"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Security Notes
|
|
61
|
+
|
|
62
|
+
- The private key (`ID_ED255`) is **only accessible when the Dongle is physically connected**.
|
|
63
|
+
- Never attempt to copy or cache the private key file.
|
|
64
|
+
- If the Dongle is not detected, fall back to `ssh AINode` (requires local SSH config).
|
|
65
|
+
|
|
66
|
+
## Upcoming: MCP & AI Endpoints
|
|
67
|
+
|
|
68
|
+
> These features are under development and will be available in a future AINode firmware update.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# MCP Server (coming soon)
|
|
72
|
+
# AINode will expose an MCP server over SSH for AI agent integration
|
|
73
|
+
# Capabilities: service status, deployment, log query, resource monitoring
|
|
74
|
+
|
|
75
|
+
# REST API (coming soon)
|
|
76
|
+
GET http://192.168.1.100:8080/api/v1/status
|
|
77
|
+
GET http://192.168.1.100:8080/api/v1/services
|
|
78
|
+
POST http://192.168.1.100:8080/api/v1/deploy
|
|
79
|
+
GET http://192.168.1.100:8080/api/v1/logs/{service}
|
|
80
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TSIC AINode miniPC 連線與服務部署規則。當工作涉及「AINode」「部署服務」「Docker 部署」「SSH AINode」「TSIC」時套用此規則。自動偵測 AINODE USB Dongle 並使用 Dongle 私鑰連線。
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TSIC AINode 連線規則
|
|
8
|
+
|
|
9
|
+
## Dongle 偵測
|
|
10
|
+
|
|
11
|
+
使用 AINode 前,先偵測 AINODE USB Dongle(FAT12 磁碟,卷標 `AINODE`):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Windows
|
|
15
|
+
(Get-WmiObject Win32_LogicalDisk | Where-Object {$_.VolumeName -eq "AINODE"}).DeviceID
|
|
16
|
+
|
|
17
|
+
# macOS / Linux
|
|
18
|
+
ls /Volumes/AINODE 2>/dev/null || blkid -L AINODE 2>/dev/null
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## SSH 連線
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 有 Dongle(優先)
|
|
25
|
+
ssh -F {DONGLE_PATH}/SSHCFG -i {DONGLE_PATH}/ID_ED255 AINode
|
|
26
|
+
|
|
27
|
+
# 無 Dongle(fallback)
|
|
28
|
+
ssh AINode # 需要 ~/.ssh/config 有 AINode alias
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**AINode IP**: `192.168.1.100`,需 RJ45 直連。
|
|
32
|
+
|
|
33
|
+
## 部署流程
|
|
34
|
+
|
|
35
|
+
1. 偵測 Dongle → 取得 SSH_CMD
|
|
36
|
+
2. 確認連線:`{SSH_CMD} "echo ok"`
|
|
37
|
+
3. 上傳專案(排除 `.git`, `node_modules`, `.env`)
|
|
38
|
+
4. 在 AINode 上執行 `docker compose up -d --build`
|
|
39
|
+
5. 確認服務:`{SSH_CMD} "docker ps"`
|
|
40
|
+
|
|
41
|
+
## 重要原則
|
|
42
|
+
|
|
43
|
+
- **私鑰只在 Dongle 上**,不要嘗試從其他路徑載入
|
|
44
|
+
- Dongle 拔除後無法連線,這是設計行為
|
|
45
|
+
- `.env` 檔案需單獨詢問使用者是否上傳
|
|
46
|
+
|
|
47
|
+
## 【預留】MCP 與 AI 接入口
|
|
48
|
+
|
|
49
|
+
> 開發中,待 AINode 韌體更新後啟用。
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
# MCP Server(即將推出)
|
|
53
|
+
mcp connect ainode --transport ssh --key {DONGLE_PATH}/ID_ED255
|
|
54
|
+
|
|
55
|
+
# AI 優化端點(即將推出)
|
|
56
|
+
http://192.168.1.100:8080/api/v1/status
|
|
57
|
+
http://192.168.1.100:8080/api/v1/services
|
|
58
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# TSIC AINode — Gemini CLI Context
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This workspace may connect to a **TSIC AINode miniPC** at `192.168.1.100`.
|
|
6
|
+
Authentication uses the **AINODE USB Security Dongle** — a hardware key that stores
|
|
7
|
+
the SSH private key in AES-encrypted Flash. The key is only readable while the Dongle
|
|
8
|
+
is physically attached.
|
|
9
|
+
|
|
10
|
+
## Dongle Detection
|
|
11
|
+
|
|
12
|
+
Before any AINode operation, detect the Dongle:
|
|
13
|
+
|
|
14
|
+
| OS | Command | Expected Output |
|
|
15
|
+
|----|---------|-----------------|
|
|
16
|
+
| Windows | `(Get-WmiObject Win32_LogicalDisk \| Where-Object {$_.VolumeName -eq "AINODE"}).DeviceID` | `E:` (or similar) |
|
|
17
|
+
| macOS | `ls /Volumes/AINODE 2>/dev/null && echo found` | `found` |
|
|
18
|
+
| Linux | `blkid -L AINODE 2>/dev/null` | `/dev/sdX` |
|
|
19
|
+
|
|
20
|
+
Set `DONGLE_PATH` to the mount point: `E:\` / `/Volumes/AINODE` / `/media/user/AINODE`.
|
|
21
|
+
|
|
22
|
+
## SSH Connection
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Dongle present
|
|
26
|
+
ssh -F "${DONGLE_PATH}/SSHCFG" -i "${DONGLE_PATH}/ID_ED255" AINode
|
|
27
|
+
|
|
28
|
+
# Dongle absent (fallback)
|
|
29
|
+
ssh AINode # requires Host AINode in ~/.ssh/config
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Deployment Workflow
|
|
33
|
+
|
|
34
|
+
1. Detect Dongle → set `DONGLE_PATH`
|
|
35
|
+
2. Test connectivity: `ssh -F ... AINode "echo ok"`
|
|
36
|
+
3. Archive project: `tar -czf /tmp/svc.tar.gz --exclude='.git' --exclude='node_modules' .`
|
|
37
|
+
4. Upload: `scp -F ... /tmp/svc.tar.gz AINode:~/services/<name>/`
|
|
38
|
+
5. Deploy: `ssh -F ... AINode "cd ~/services/<name> && tar -xzf svc.tar.gz && docker compose up -d --build"`
|
|
39
|
+
6. Verify: `ssh -F ... AINode "docker ps | grep <name>"`
|
|
40
|
+
|
|
41
|
+
## Useful Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Service logs
|
|
45
|
+
ssh [...] AINode "docker logs -f <service>"
|
|
46
|
+
|
|
47
|
+
# Resource usage
|
|
48
|
+
ssh [...] AINode "docker stats --no-stream"
|
|
49
|
+
|
|
50
|
+
# Disk space
|
|
51
|
+
ssh [...] AINode "df -h /"
|
|
52
|
+
|
|
53
|
+
# All running services
|
|
54
|
+
ssh [...] AINode "docker compose ls"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Upcoming Features
|
|
58
|
+
|
|
59
|
+
> Under development — will be enabled in a future AINode update.
|
|
60
|
+
|
|
61
|
+
### MCP Integration
|
|
62
|
+
AINode will provide an MCP (Model Context Protocol) server accessible over SSH,
|
|
63
|
+
allowing Gemini CLI and other AI agents to query AINode state directly:
|
|
64
|
+
```
|
|
65
|
+
# MCP server (coming soon)
|
|
66
|
+
# Transport: SSH tunnel
|
|
67
|
+
# Capabilities: service management, log streaming, resource monitoring, deployment
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### AI-Optimized REST API
|
|
71
|
+
```
|
|
72
|
+
Base URL: http://192.168.1.100:8080/api/v1/
|
|
73
|
+
|
|
74
|
+
GET /status — system snapshot (CPU, RAM, disk, uptime)
|
|
75
|
+
GET /services — list all Docker services with health status
|
|
76
|
+
POST /deploy — trigger deployment pipeline
|
|
77
|
+
GET /logs/{service} — structured, AI-friendly log output
|
|
78
|
+
GET /metrics — Prometheus-compatible metrics
|
|
79
|
+
```
|