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 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
+ });
@@ -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
+ ```