localant 1.0.1 → 1.1.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.
Files changed (178) hide show
  1. package/README.ja.md +185 -0
  2. package/README.md +137 -20
  3. package/SECURITY.md +63 -8
  4. package/assets/hero.png +0 -0
  5. package/assets/localant-icon.png +0 -0
  6. package/examples/skills/article-publisher/README.md +41 -0
  7. package/examples/skills/article-publisher/package.json +9 -0
  8. package/examples/skills/article-publisher/skill.json +134 -0
  9. package/examples/skills/article-publisher/src/index.ts +186 -0
  10. package/examples/skills/article-publisher/tests/skill.test.ts +72 -0
  11. package/package.json +26 -6
  12. package/packages/cli/dist/autostart.d.ts +14 -0
  13. package/packages/cli/dist/autostart.d.ts.map +1 -0
  14. package/packages/cli/dist/autostart.js +98 -0
  15. package/packages/cli/dist/autostart.js.map +1 -0
  16. package/packages/cli/dist/bin.js +214 -2
  17. package/packages/cli/dist/bin.js.map +1 -1
  18. package/packages/cli/dist/runtime.d.ts.map +1 -1
  19. package/packages/cli/dist/runtime.js +56 -8
  20. package/packages/cli/dist/runtime.js.map +1 -1
  21. package/packages/cli/dist/serveo-setup.d.ts +37 -0
  22. package/packages/cli/dist/serveo-setup.d.ts.map +1 -0
  23. package/packages/cli/dist/serveo-setup.js +168 -0
  24. package/packages/cli/dist/serveo-setup.js.map +1 -0
  25. package/packages/cli/dist/util.d.ts +6 -0
  26. package/packages/cli/dist/util.d.ts.map +1 -1
  27. package/packages/cli/dist/util.js +20 -0
  28. package/packages/cli/dist/util.js.map +1 -1
  29. package/packages/cli/package.json +1 -1
  30. package/packages/dashboard/dist/index.d.ts +5 -4
  31. package/packages/dashboard/dist/index.d.ts.map +1 -1
  32. package/packages/dashboard/dist/index.js +781 -44
  33. package/packages/dashboard/dist/index.js.map +1 -1
  34. package/packages/gateway/dist/gateway.d.ts +14 -1
  35. package/packages/gateway/dist/gateway.d.ts.map +1 -1
  36. package/packages/gateway/dist/gateway.js +59 -6
  37. package/packages/gateway/dist/gateway.js.map +1 -1
  38. package/packages/gateway/dist/index.d.ts +3 -0
  39. package/packages/gateway/dist/index.d.ts.map +1 -1
  40. package/packages/gateway/dist/index.js +3 -0
  41. package/packages/gateway/dist/index.js.map +1 -1
  42. package/packages/gateway/dist/managers/coding-agent-manager.d.ts +14 -0
  43. package/packages/gateway/dist/managers/coding-agent-manager.d.ts.map +1 -1
  44. package/packages/gateway/dist/managers/coding-agent-manager.js +21 -2
  45. package/packages/gateway/dist/managers/coding-agent-manager.js.map +1 -1
  46. package/packages/gateway/dist/managers/fs-manager.d.ts +73 -0
  47. package/packages/gateway/dist/managers/fs-manager.d.ts.map +1 -1
  48. package/packages/gateway/dist/managers/fs-manager.js +290 -6
  49. package/packages/gateway/dist/managers/fs-manager.js.map +1 -1
  50. package/packages/gateway/dist/managers/git-manager.d.ts +6 -0
  51. package/packages/gateway/dist/managers/git-manager.d.ts.map +1 -1
  52. package/packages/gateway/dist/managers/git-manager.js +24 -0
  53. package/packages/gateway/dist/managers/git-manager.js.map +1 -1
  54. package/packages/gateway/dist/managers/lsp-service.d.ts +88 -0
  55. package/packages/gateway/dist/managers/lsp-service.d.ts.map +1 -0
  56. package/packages/gateway/dist/managers/lsp-service.js +249 -0
  57. package/packages/gateway/dist/managers/lsp-service.js.map +1 -0
  58. package/packages/gateway/dist/managers/mcp-bridge.d.ts +2 -1
  59. package/packages/gateway/dist/managers/mcp-bridge.d.ts.map +1 -1
  60. package/packages/gateway/dist/managers/mcp-bridge.js +23 -2
  61. package/packages/gateway/dist/managers/mcp-bridge.js.map +1 -1
  62. package/packages/gateway/dist/managers/shell-manager.d.ts +19 -0
  63. package/packages/gateway/dist/managers/shell-manager.d.ts.map +1 -1
  64. package/packages/gateway/dist/managers/shell-manager.js +28 -0
  65. package/packages/gateway/dist/managers/shell-manager.js.map +1 -1
  66. package/packages/gateway/dist/managers/skill-runtime.d.ts +8 -0
  67. package/packages/gateway/dist/managers/skill-runtime.d.ts.map +1 -1
  68. package/packages/gateway/dist/managers/skill-runtime.js +15 -0
  69. package/packages/gateway/dist/managers/skill-runtime.js.map +1 -1
  70. package/packages/gateway/dist/managers/tunnel-manager.d.ts +19 -1
  71. package/packages/gateway/dist/managers/tunnel-manager.d.ts.map +1 -1
  72. package/packages/gateway/dist/managers/tunnel-manager.js +289 -8
  73. package/packages/gateway/dist/managers/tunnel-manager.js.map +1 -1
  74. package/packages/gateway/dist/security/command-guard.d.ts +3 -0
  75. package/packages/gateway/dist/security/command-guard.d.ts.map +1 -1
  76. package/packages/gateway/dist/security/command-guard.js +15 -7
  77. package/packages/gateway/dist/security/command-guard.js.map +1 -1
  78. package/packages/gateway/dist/security/path-guard.d.ts +3 -0
  79. package/packages/gateway/dist/security/path-guard.d.ts.map +1 -1
  80. package/packages/gateway/dist/security/path-guard.js +8 -2
  81. package/packages/gateway/dist/security/path-guard.js.map +1 -1
  82. package/packages/gateway/dist/stores/config-store.d.ts +10 -0
  83. package/packages/gateway/dist/stores/config-store.d.ts.map +1 -1
  84. package/packages/gateway/dist/stores/config-store.js +47 -3
  85. package/packages/gateway/dist/stores/config-store.js.map +1 -1
  86. package/packages/gateway/dist/stores/secret-vault.d.ts +19 -3
  87. package/packages/gateway/dist/stores/secret-vault.d.ts.map +1 -1
  88. package/packages/gateway/dist/stores/secret-vault.js +47 -6
  89. package/packages/gateway/dist/stores/secret-vault.js.map +1 -1
  90. package/packages/gateway/dist/tools/adapters.d.ts.map +1 -1
  91. package/packages/gateway/dist/tools/adapters.js +198 -7
  92. package/packages/gateway/dist/tools/adapters.js.map +1 -1
  93. package/packages/gateway/dist/tools/adb.d.ts.map +1 -1
  94. package/packages/gateway/dist/tools/adb.js +42 -0
  95. package/packages/gateway/dist/tools/adb.js.map +1 -1
  96. package/packages/gateway/dist/tools/agent.d.ts +10 -0
  97. package/packages/gateway/dist/tools/agent.d.ts.map +1 -0
  98. package/packages/gateway/dist/tools/agent.js +35 -0
  99. package/packages/gateway/dist/tools/agent.js.map +1 -0
  100. package/packages/gateway/dist/tools/aliases.d.ts +7 -0
  101. package/packages/gateway/dist/tools/aliases.d.ts.map +1 -0
  102. package/packages/gateway/dist/tools/aliases.js +64 -0
  103. package/packages/gateway/dist/tools/aliases.js.map +1 -0
  104. package/packages/gateway/dist/tools/bash.d.ts +10 -0
  105. package/packages/gateway/dist/tools/bash.d.ts.map +1 -0
  106. package/packages/gateway/dist/tools/bash.js +67 -0
  107. package/packages/gateway/dist/tools/bash.js.map +1 -0
  108. package/packages/gateway/dist/tools/browser.d.ts.map +1 -1
  109. package/packages/gateway/dist/tools/browser.js +9 -0
  110. package/packages/gateway/dist/tools/browser.js.map +1 -1
  111. package/packages/gateway/dist/tools/control.d.ts +8 -0
  112. package/packages/gateway/dist/tools/control.d.ts.map +1 -0
  113. package/packages/gateway/dist/tools/control.js +134 -0
  114. package/packages/gateway/dist/tools/control.js.map +1 -0
  115. package/packages/gateway/dist/tools/editing.d.ts +8 -0
  116. package/packages/gateway/dist/tools/editing.d.ts.map +1 -0
  117. package/packages/gateway/dist/tools/editing.js +102 -0
  118. package/packages/gateway/dist/tools/editing.js.map +1 -0
  119. package/packages/gateway/dist/tools/git.d.ts.map +1 -1
  120. package/packages/gateway/dist/tools/git.js +67 -0
  121. package/packages/gateway/dist/tools/git.js.map +1 -1
  122. package/packages/gateway/dist/tools/index.d.ts.map +1 -1
  123. package/packages/gateway/dist/tools/index.js +17 -2
  124. package/packages/gateway/dist/tools/index.js.map +1 -1
  125. package/packages/gateway/dist/tools/lsp.d.ts +10 -0
  126. package/packages/gateway/dist/tools/lsp.d.ts.map +1 -0
  127. package/packages/gateway/dist/tools/lsp.js +111 -0
  128. package/packages/gateway/dist/tools/lsp.js.map +1 -0
  129. package/packages/gateway/dist/tools/question.d.ts +10 -0
  130. package/packages/gateway/dist/tools/question.d.ts.map +1 -0
  131. package/packages/gateway/dist/tools/question.js +30 -0
  132. package/packages/gateway/dist/tools/question.js.map +1 -0
  133. package/packages/gateway/dist/tools/shell.d.ts +1 -1
  134. package/packages/gateway/dist/tools/shell.d.ts.map +1 -1
  135. package/packages/gateway/dist/tools/shell.js +15 -0
  136. package/packages/gateway/dist/tools/shell.js.map +1 -1
  137. package/packages/gateway/dist/tools/skill.d.ts.map +1 -1
  138. package/packages/gateway/dist/tools/skill.js +2 -7
  139. package/packages/gateway/dist/tools/skill.js.map +1 -1
  140. package/packages/gateway/dist/tools/system.js +2 -2
  141. package/packages/gateway/dist/tools/system.js.map +1 -1
  142. package/packages/gateway/dist/tools/validation.d.ts +3 -0
  143. package/packages/gateway/dist/tools/validation.d.ts.map +1 -0
  144. package/packages/gateway/dist/tools/validation.js +120 -0
  145. package/packages/gateway/dist/tools/validation.js.map +1 -0
  146. package/packages/mcp/dist/http-server.d.ts +1 -1
  147. package/packages/mcp/dist/http-server.d.ts.map +1 -1
  148. package/packages/mcp/dist/http-server.js +544 -20
  149. package/packages/mcp/dist/http-server.js.map +1 -1
  150. package/packages/mcp/dist/mcp-server.d.ts.map +1 -1
  151. package/packages/mcp/dist/mcp-server.js +5 -1
  152. package/packages/mcp/dist/mcp-server.js.map +1 -1
  153. package/packages/shared/dist/config.d.ts +146 -16
  154. package/packages/shared/dist/config.d.ts.map +1 -1
  155. package/packages/shared/dist/config.js +93 -7
  156. package/packages/shared/dist/config.js.map +1 -1
  157. package/packages/shared/dist/index.d.ts +2 -0
  158. package/packages/shared/dist/index.d.ts.map +1 -1
  159. package/packages/shared/dist/index.js +2 -0
  160. package/packages/shared/dist/index.js.map +1 -1
  161. package/packages/shared/dist/paths.d.ts +19 -2
  162. package/packages/shared/dist/paths.d.ts.map +1 -1
  163. package/packages/shared/dist/paths.js +50 -3
  164. package/packages/shared/dist/paths.js.map +1 -1
  165. package/packages/shared/dist/tool-profiles.d.ts +34 -0
  166. package/packages/shared/dist/tool-profiles.d.ts.map +1 -0
  167. package/packages/shared/dist/tool-profiles.js +188 -0
  168. package/packages/shared/dist/tool-profiles.js.map +1 -0
  169. package/packages/shared/dist/version.d.ts +9 -0
  170. package/packages/shared/dist/version.d.ts.map +1 -0
  171. package/packages/shared/dist/version.js +9 -0
  172. package/packages/shared/dist/version.js.map +1 -0
  173. package/scripts/postinstall.mjs +56 -0
  174. package/assets/icon.svg +0 -25
  175. package/packages/gateway/dist/tools/article.d.ts +0 -3
  176. package/packages/gateway/dist/tools/article.d.ts.map +0 -1
  177. package/packages/gateway/dist/tools/article.js +0 -230
  178. package/packages/gateway/dist/tools/article.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ {
2
+ "name": "article-publisher",
3
+ "displayName": "Article Publisher",
4
+ "version": "0.1.0",
5
+ "description": "Draft and publish articles to Zenn (git repo) and Qiita (API), plus local note/generic drafts.",
6
+ "author": "LocalAnt",
7
+ "license": "MIT",
8
+ "entry": "src/index.ts",
9
+ "riskLevel": 3,
10
+ "permissions": {
11
+ "filesystem": { "mode": "write", "allowedDirectories": [] },
12
+ "shell": { "mode": "allowed", "allowedCommands": ["git"] },
13
+ "network": { "mode": "allowlist", "allowedHosts": ["qiita.com"] },
14
+ "secrets": ["QIITA_TOKEN"],
15
+ "browser": "none",
16
+ "adb": "none",
17
+ "git": "write",
18
+ "agent": "none"
19
+ },
20
+ "tools": [
21
+ {
22
+ "name": "article_create",
23
+ "description": "Create a generic Markdown article draft in the skill workspace.",
24
+ "riskLevel": 1,
25
+ "inputSchema": {
26
+ "type": "object",
27
+ "properties": {
28
+ "title": { "type": "string" },
29
+ "body": { "type": "string" },
30
+ "tags": { "type": "array", "items": { "type": "string" } }
31
+ },
32
+ "required": ["title", "body"]
33
+ }
34
+ },
35
+ {
36
+ "name": "zenn_create_article",
37
+ "description": "Create a Zenn article markdown file (published:false draft) under <repoPath>/articles.",
38
+ "riskLevel": 2,
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "repoPath": { "type": "string" },
43
+ "slug": { "type": "string" },
44
+ "title": { "type": "string" },
45
+ "emoji": { "type": "string" },
46
+ "type": { "type": "string", "enum": ["tech", "idea"] },
47
+ "topics": { "type": "array", "items": { "type": "string" } },
48
+ "body": { "type": "string" }
49
+ },
50
+ "required": ["repoPath", "title", "body"]
51
+ }
52
+ },
53
+ {
54
+ "name": "zenn_list_articles",
55
+ "description": "List Zenn article files under <repoPath>/articles.",
56
+ "riskLevel": 0,
57
+ "inputSchema": {
58
+ "type": "object",
59
+ "properties": { "repoPath": { "type": "string" } },
60
+ "required": ["repoPath"]
61
+ }
62
+ },
63
+ {
64
+ "name": "zenn_publish_article",
65
+ "description": "Flip a Zenn article to published:true. Commit & push the repo to actually publish.",
66
+ "riskLevel": 4,
67
+ "inputSchema": {
68
+ "type": "object",
69
+ "properties": {
70
+ "repoPath": { "type": "string" },
71
+ "slug": { "type": "string" }
72
+ },
73
+ "required": ["repoPath", "slug"]
74
+ }
75
+ },
76
+ {
77
+ "name": "zenn_create_pr",
78
+ "description": "Commit Zenn changes on a new branch (ready to push and open a PR).",
79
+ "riskLevel": 3,
80
+ "inputSchema": {
81
+ "type": "object",
82
+ "properties": {
83
+ "repoPath": { "type": "string" },
84
+ "branch": { "type": "string" },
85
+ "message": { "type": "string" }
86
+ },
87
+ "required": ["repoPath", "branch", "message"]
88
+ }
89
+ },
90
+ {
91
+ "name": "qiita_create_private_article",
92
+ "description": "Create a PRIVATE Qiita article via the official API (reads QIITA_TOKEN secret).",
93
+ "riskLevel": 3,
94
+ "inputSchema": {
95
+ "type": "object",
96
+ "properties": {
97
+ "title": { "type": "string" },
98
+ "body": { "type": "string" },
99
+ "tags": { "type": "array", "items": { "type": "string" } }
100
+ },
101
+ "required": ["title", "body"]
102
+ }
103
+ },
104
+ {
105
+ "name": "qiita_list_articles",
106
+ "description": "List your authenticated Qiita articles (reads QIITA_TOKEN secret).",
107
+ "riskLevel": 3,
108
+ "inputSchema": { "type": "object", "properties": {} }
109
+ },
110
+ {
111
+ "name": "qiita_publish_article",
112
+ "description": "Flip a Qiita article to public (reads QIITA_TOKEN secret).",
113
+ "riskLevel": 4,
114
+ "inputSchema": {
115
+ "type": "object",
116
+ "properties": { "id": { "type": "string" } },
117
+ "required": ["id"]
118
+ }
119
+ },
120
+ {
121
+ "name": "note_create_draft",
122
+ "description": "Create a local note draft in the skill workspace (note has no official public write API).",
123
+ "riskLevel": 1,
124
+ "inputSchema": {
125
+ "type": "object",
126
+ "properties": {
127
+ "title": { "type": "string" },
128
+ "body": { "type": "string" }
129
+ },
130
+ "required": ["title", "body"]
131
+ }
132
+ }
133
+ ]
134
+ }
@@ -0,0 +1,186 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { defineSkill, z } from "@localant/skill-sdk";
6
+
7
+ const exec = promisify(execFile);
8
+
9
+ function slugify(s: string): string {
10
+ return (
11
+ s
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]+/g, "-")
14
+ .replace(/^-|-$/g, "")
15
+ .slice(0, 50) || `article-${Date.now()}`
16
+ );
17
+ }
18
+
19
+ /** Run git in a repo without a shell (args array, never interpolated). */
20
+ async function git(repo: string, args: string[]): Promise<string> {
21
+ const { stdout } = await exec("git", ["-C", repo, ...args], { maxBuffer: 10_000_000 });
22
+ return stdout.trim();
23
+ }
24
+
25
+ const QIITA_API = "https://qiita.com/api/v2";
26
+
27
+ async function qiitaToken(ctx: { getSecret: (n: string) => Promise<string | undefined> }): Promise<string> {
28
+ const token = await ctx.getSecret("QIITA_TOKEN");
29
+ if (!token) {
30
+ throw new Error(
31
+ "QIITA_TOKEN is not available. Store it via the dashboard (Secrets) or `localant secrets set QIITA_TOKEN`, " +
32
+ "and ensure this skill's permissions.secrets includes QIITA_TOKEN.",
33
+ );
34
+ }
35
+ return token;
36
+ }
37
+
38
+ export default defineSkill({
39
+ name: "article-publisher",
40
+ displayName: "Article Publisher",
41
+ description: "Draft and publish articles to Zenn (git repo) and Qiita (API), plus local note/generic drafts.",
42
+ version: "0.1.0",
43
+ tools: {
44
+ // ---- generic ----
45
+ article_create: {
46
+ description: "Create a generic Markdown article draft in the skill workspace.",
47
+ riskLevel: 1,
48
+ inputSchema: z.object({
49
+ title: z.string(),
50
+ body: z.string(),
51
+ tags: z.array(z.string()).default([]),
52
+ }),
53
+ handler: ({ title, body, tags }, ctx) => {
54
+ const file = path.join(ctx.workspaceDir, `${slugify(title)}.md`);
55
+ const fm = `---\ntitle: "${title}"\ntags: [${tags.map((t) => `"${t}"`).join(", ")}]\n---\n\n`;
56
+ fs.writeFileSync(file, fm + body);
57
+ return { path: file };
58
+ },
59
+ },
60
+
61
+ // ---- Zenn (GitHub repo method) ----
62
+ zenn_create_article: {
63
+ description: "Create a Zenn article markdown file (published:false draft) under <repoPath>/articles.",
64
+ riskLevel: 2,
65
+ inputSchema: z.object({
66
+ repoPath: z.string(),
67
+ slug: z.string().optional(),
68
+ title: z.string(),
69
+ emoji: z.string().default("📝"),
70
+ type: z.enum(["tech", "idea"]).default("tech"),
71
+ topics: z.array(z.string()).default([]),
72
+ body: z.string(),
73
+ }),
74
+ handler: ({ repoPath, slug, title, emoji, type, topics, body }) => {
75
+ const finalSlug = slug ?? slugify(title);
76
+ const dir = path.join(repoPath, "articles");
77
+ fs.mkdirSync(dir, { recursive: true });
78
+ const file = path.join(dir, `${finalSlug}.md`);
79
+ const fm = `---\ntitle: "${title}"\nemoji: "${emoji}"\ntype: "${type}"\ntopics: [${topics
80
+ .map((t) => `"${t}"`)
81
+ .join(", ")}]\npublished: false\n---\n\n`;
82
+ fs.writeFileSync(file, fm + body);
83
+ return { path: file, slug: finalSlug, published: false };
84
+ },
85
+ },
86
+ zenn_list_articles: {
87
+ description: "List Zenn article files under <repoPath>/articles.",
88
+ riskLevel: 0,
89
+ inputSchema: z.object({ repoPath: z.string() }),
90
+ handler: ({ repoPath }) => {
91
+ const dir = path.join(repoPath, "articles");
92
+ return { articles: fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith(".md")) : [] };
93
+ },
94
+ },
95
+ zenn_publish_article: {
96
+ description: "Flip a Zenn article to published:true. Commit & push the repo to actually publish.",
97
+ riskLevel: 4,
98
+ inputSchema: z.object({ repoPath: z.string(), slug: z.string() }),
99
+ handler: ({ repoPath, slug }) => {
100
+ const file = path.join(repoPath, "articles", `${slug}.md`);
101
+ const content = fs.readFileSync(file, "utf8").replace(/published:\s*false/, "published: true");
102
+ fs.writeFileSync(file, content);
103
+ return { path: file, published: true, note: "Commit & push the repo to publish on Zenn." };
104
+ },
105
+ },
106
+ zenn_create_pr: {
107
+ description: "Commit Zenn changes on a new branch (ready to push and open a PR).",
108
+ riskLevel: 3,
109
+ inputSchema: z.object({ repoPath: z.string(), branch: z.string(), message: z.string() }),
110
+ handler: async ({ repoPath, branch, message }) => {
111
+ await git(repoPath, ["checkout", "-b", branch]);
112
+ await git(repoPath, ["add", "-A"]);
113
+ await git(repoPath, ["commit", "-m", message]);
114
+ return { branch, note: `Push with: git -C ${repoPath} push -u origin ${branch}` };
115
+ },
116
+ },
117
+
118
+ // ---- Qiita (official API) ----
119
+ qiita_create_private_article: {
120
+ description: "Create a PRIVATE Qiita article via the official API (reads QIITA_TOKEN secret).",
121
+ riskLevel: 3,
122
+ inputSchema: z.object({
123
+ title: z.string(),
124
+ body: z.string(),
125
+ tags: z.array(z.string()).default([]),
126
+ }),
127
+ handler: async ({ title, body, tags }, ctx) => {
128
+ const token = await qiitaToken(ctx);
129
+ const res = await fetch(`${QIITA_API}/items`, {
130
+ method: "POST",
131
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
132
+ body: JSON.stringify({
133
+ title,
134
+ body,
135
+ private: true,
136
+ tags: (tags.length ? tags : ["draft"]).map((name) => ({ name })),
137
+ }),
138
+ });
139
+ if (!res.ok) throw new Error(`Qiita API error ${res.status}: ${await res.text()}`);
140
+ const json = (await res.json()) as { id: string; url: string };
141
+ return { id: json.id, url: json.url, private: true };
142
+ },
143
+ },
144
+ qiita_list_articles: {
145
+ description: "List your authenticated Qiita articles (reads QIITA_TOKEN secret).",
146
+ riskLevel: 3,
147
+ inputSchema: z.object({}),
148
+ handler: async (_input, ctx) => {
149
+ const token = await qiitaToken(ctx);
150
+ const res = await fetch(`${QIITA_API}/authenticated_user/items?per_page=20`, {
151
+ headers: { authorization: `Bearer ${token}` },
152
+ });
153
+ if (!res.ok) throw new Error(`Qiita API error ${res.status}`);
154
+ const items = (await res.json()) as { id: string; title: string; private: boolean; url: string }[];
155
+ return { items: items.map((it) => ({ id: it.id, title: it.title, private: it.private, url: it.url })) };
156
+ },
157
+ },
158
+ qiita_publish_article: {
159
+ description: "Flip a Qiita article to public (reads QIITA_TOKEN secret).",
160
+ riskLevel: 4,
161
+ inputSchema: z.object({ id: z.string() }),
162
+ handler: async ({ id }, ctx) => {
163
+ const token = await qiitaToken(ctx);
164
+ const res = await fetch(`${QIITA_API}/items/${id}`, {
165
+ method: "PATCH",
166
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
167
+ body: JSON.stringify({ private: false }),
168
+ });
169
+ if (!res.ok) throw new Error(`Qiita API error ${res.status}: ${await res.text()}`);
170
+ return { id, private: false };
171
+ },
172
+ },
173
+
174
+ // ---- note (local draft; note has no official public write API) ----
175
+ note_create_draft: {
176
+ description: "Create a local note draft in the skill workspace (note has no official public write API).",
177
+ riskLevel: 1,
178
+ inputSchema: z.object({ title: z.string(), body: z.string() }),
179
+ handler: ({ title, body }, ctx) => {
180
+ const file = path.join(ctx.workspaceDir, `note-${slugify(title)}.md`);
181
+ fs.writeFileSync(file, `# ${title}\n\n${body}`);
182
+ return { path: file, note: "note has no official write API; publish manually." };
183
+ },
184
+ },
185
+ },
186
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import skill from "../src/index";
6
+
7
+ const ctxBase = { getSecret: async () => undefined, log: () => {} };
8
+ let ws: string;
9
+
10
+ beforeEach(() => {
11
+ ws = fs.mkdtempSync(path.join(os.tmpdir(), "article-skill-"));
12
+ });
13
+ afterEach(() => fs.rmSync(ws, { recursive: true, force: true }));
14
+
15
+ describe("article-publisher skill", () => {
16
+ it("declares the expected tools", () => {
17
+ expect(skill.name).toBe("article-publisher");
18
+ for (const t of [
19
+ "article_create",
20
+ "zenn_create_article",
21
+ "zenn_list_articles",
22
+ "zenn_publish_article",
23
+ "zenn_create_pr",
24
+ "qiita_create_private_article",
25
+ "qiita_list_articles",
26
+ "qiita_publish_article",
27
+ "note_create_draft",
28
+ ]) {
29
+ expect(skill.tools[t], `missing tool ${t}`).toBeDefined();
30
+ }
31
+ });
32
+
33
+ it("writes a generic draft into the workspace", async () => {
34
+ const out = (await skill.tools.article_create!.handler(
35
+ { title: "Hello World", body: "content", tags: ["a", "b"] },
36
+ { ...ctxBase, workspaceDir: ws },
37
+ )) as { path: string };
38
+ expect(fs.existsSync(out.path)).toBe(true);
39
+ expect(fs.readFileSync(out.path, "utf8")).toContain('title: "Hello World"');
40
+ });
41
+
42
+ it("creates a Zenn draft with published:false then flips it", async () => {
43
+ const created = (await skill.tools.zenn_create_article!.handler(
44
+ { repoPath: ws, title: "My Post", body: "body", emoji: "📝", type: "tech", topics: ["ts"] },
45
+ { ...ctxBase, workspaceDir: ws },
46
+ )) as { path: string; slug: string };
47
+ expect(fs.readFileSync(created.path, "utf8")).toContain("published: false");
48
+
49
+ const listed = (await skill.tools.zenn_list_articles!.handler(
50
+ { repoPath: ws },
51
+ { ...ctxBase, workspaceDir: ws },
52
+ )) as { articles: string[] };
53
+ expect(listed.articles).toContain(`${created.slug}.md`);
54
+
55
+ const published = (await skill.tools.zenn_publish_article!.handler(
56
+ { repoPath: ws, slug: created.slug },
57
+ { ...ctxBase, workspaceDir: ws },
58
+ )) as { published: boolean };
59
+ expect(published.published).toBe(true);
60
+ expect(fs.readFileSync(created.path, "utf8")).toContain("published: true");
61
+ });
62
+
63
+ it("fails Qiita calls when the secret is missing", async () => {
64
+ await expect(
65
+ skill.tools.qiita_list_articles!.handler({}, { ...ctxBase, workspaceDir: ws }),
66
+ ).rejects.toThrow(/QIITA_TOKEN/);
67
+ });
68
+
69
+ it("validates input schemas", () => {
70
+ expect(() => skill.tools.zenn_list_articles!.inputSchema.parse({})).toThrow();
71
+ });
72
+ });
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "localant",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "LocalAnt — Use ChatGPT as the brain and your local computer as the hands. A safe, permissioned local MCP Gateway for ChatGPT.",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yuga-hashimoto/localant.git"
10
+ },
11
+ "homepage": "https://github.com/yuga-hashimoto/localant#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/yuga-hashimoto/localant/issues"
14
+ },
7
15
  "type": "module",
8
16
  "bin": {
9
17
  "localant": "packages/cli/dist/bin.js"
@@ -13,12 +21,21 @@
13
21
  "packages/*/package.json",
14
22
  "examples/skills/**",
15
23
  "assets/**",
24
+ "scripts/postinstall.mjs",
16
25
  "README.md",
17
26
  "SECURITY.md",
18
27
  "LICENSE"
19
28
  ],
20
29
  "engines": {
21
- "node": ">=22"
30
+ "node": ">=20.10"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.12.0",
34
+ "commander": "^15.0.0",
35
+ "express": "^5.0.1",
36
+ "nanoid": "^5.0.9",
37
+ "tsx": "^4.19.2",
38
+ "zod": "^3.24.1"
22
39
  },
23
40
  "keywords": [
24
41
  "chatgpt",
@@ -38,15 +55,16 @@
38
55
  "@types/node": "^22.10.0",
39
56
  "@typescript-eslint/eslint-plugin": "^8.18.0",
40
57
  "@typescript-eslint/parser": "^8.18.0",
58
+ "@vitest/coverage-v8": "^2.1.9",
41
59
  "eslint": "^9.17.0",
42
60
  "rimraf": "^6.0.1",
43
61
  "tsx": "^4.19.2",
44
62
  "typescript": "^5.7.2",
45
63
  "vitest": "^2.1.8",
46
- "@localant/gateway": "1.0.0",
47
- "@localant/skill-sdk": "1.0.0",
64
+ "@localant/mcp": "1.0.0",
48
65
  "@localant/shared": "1.0.0",
49
- "@localant/mcp": "1.0.0"
66
+ "@localant/skill-sdk": "1.0.0",
67
+ "@localant/gateway": "1.0.0"
50
68
  },
51
69
  "scripts": {
52
70
  "build": "tsc -b",
@@ -56,8 +74,10 @@
56
74
  "lint": "eslint .",
57
75
  "test": "vitest run",
58
76
  "test:watch": "vitest",
77
+ "test:coverage": "vitest run --coverage",
59
78
  "validate": "pnpm build && pnpm test",
60
79
  "start": "node packages/cli/dist/bin.js start",
61
- "setup": "node packages/cli/dist/bin.js setup"
80
+ "setup": "node packages/cli/dist/bin.js setup",
81
+ "postinstall": "node scripts/postinstall.mjs"
62
82
  }
63
83
  }
@@ -0,0 +1,14 @@
1
+ /** Auto-start on login is currently implemented for macOS (launchd) only. */
2
+ export declare function autostartSupported(): boolean;
3
+ /** True when the LaunchAgent plist is installed. */
4
+ export declare function isAutostartEnabled(): boolean;
5
+ /**
6
+ * Install the LaunchAgent so the gateway starts automatically on every login.
7
+ * The plist is written but not booted immediately — booting it now would spawn
8
+ * a second `start` that collides with the gateway setup is about to run on the
9
+ * same port. It takes effect on the next login/reboot. Returns the plist path.
10
+ */
11
+ export declare function enableAutostart(logDir: string): string;
12
+ /** Remove the LaunchAgent and stop any running launchd-managed instance. */
13
+ export declare function disableAutostart(): void;
14
+ //# sourceMappingURL=autostart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autostart.d.ts","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"AAgBA,6EAA6E;AAC7E,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAoDD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,IAAI,IAAI,CASvC"}
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ /** launchd label for the LocalAnt gateway LaunchAgent. */
6
+ const LABEL = "com.localant.gateway";
7
+ function launchAgentsDir() {
8
+ return path.join(os.homedir(), "Library", "LaunchAgents");
9
+ }
10
+ function plistPath() {
11
+ return path.join(launchAgentsDir(), `${LABEL}.plist`);
12
+ }
13
+ /** Auto-start on login is currently implemented for macOS (launchd) only. */
14
+ export function autostartSupported() {
15
+ return process.platform === "darwin";
16
+ }
17
+ /** True when the LaunchAgent plist is installed. */
18
+ export function isAutostartEnabled() {
19
+ return autostartSupported() && fs.existsSync(plistPath());
20
+ }
21
+ /**
22
+ * A PATH for the launchd job. launchd starts with a minimal PATH, but the
23
+ * gateway shells out to ssh / node / npx / cloudflared, so we seed it with the
24
+ * common locations plus whatever PATH setup itself was launched with.
25
+ */
26
+ function launchdPath() {
27
+ const base = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
28
+ const inherited = (process.env.PATH ?? "").split(":").filter(Boolean);
29
+ return [...new Set([...base, ...inherited])].join(":");
30
+ }
31
+ function buildPlist(logDir) {
32
+ const node = process.execPath;
33
+ const binJs = path.resolve(process.argv[1] ?? "");
34
+ const outLog = path.join(logDir, "autostart.out.log");
35
+ const errLog = path.join(logDir, "autostart.err.log");
36
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
37
+ return `<?xml version="1.0" encoding="UTF-8"?>
38
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39
+ <plist version="1.0">
40
+ <dict>
41
+ <key>Label</key>
42
+ <string>${LABEL}</string>
43
+ <key>ProgramArguments</key>
44
+ <array>
45
+ <string>${esc(node)}</string>
46
+ <string>${esc(binJs)}</string>
47
+ <string>start</string>
48
+ <string>--no-open</string>
49
+ </array>
50
+ <key>RunAtLoad</key>
51
+ <true/>
52
+ <key>KeepAlive</key>
53
+ <true/>
54
+ <key>WorkingDirectory</key>
55
+ <string>${esc(os.homedir())}</string>
56
+ <key>StandardOutPath</key>
57
+ <string>${esc(outLog)}</string>
58
+ <key>StandardErrorPath</key>
59
+ <string>${esc(errLog)}</string>
60
+ <key>EnvironmentVariables</key>
61
+ <dict>
62
+ <key>PATH</key>
63
+ <string>${esc(launchdPath())}</string>
64
+ </dict>
65
+ </dict>
66
+ </plist>
67
+ `;
68
+ }
69
+ /**
70
+ * Install the LaunchAgent so the gateway starts automatically on every login.
71
+ * The plist is written but not booted immediately — booting it now would spawn
72
+ * a second `start` that collides with the gateway setup is about to run on the
73
+ * same port. It takes effect on the next login/reboot. Returns the plist path.
74
+ */
75
+ export function enableAutostart(logDir) {
76
+ if (!autostartSupported()) {
77
+ throw new Error("Auto-start on login is only supported on macOS.");
78
+ }
79
+ fs.mkdirSync(launchAgentsDir(), { recursive: true });
80
+ fs.mkdirSync(logDir, { recursive: true });
81
+ fs.writeFileSync(plistPath(), buildPlist(logDir), { mode: 0o644 });
82
+ return plistPath();
83
+ }
84
+ /** Remove the LaunchAgent and stop any running launchd-managed instance. */
85
+ export function disableAutostart() {
86
+ if (!autostartSupported())
87
+ return;
88
+ const p = plistPath();
89
+ try {
90
+ execFileSync("launchctl", ["bootout", `gui/${process.getuid?.()}/${LABEL}`], { stdio: "ignore" });
91
+ }
92
+ catch {
93
+ // Not loaded — nothing to bootout.
94
+ }
95
+ if (fs.existsSync(p))
96
+ fs.rmSync(p, { force: true });
97
+ }
98
+ //# sourceMappingURL=autostart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autostart.js","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"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"}