harvask-cli 0.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/api-base.js +8 -0
  4. package/dist/api-base.js.map +1 -0
  5. package/dist/api-client.js +221 -0
  6. package/dist/api-client.js.map +1 -0
  7. package/dist/cli.js +154 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/_exec.js +63 -0
  10. package/dist/commands/_exec.js.map +1 -0
  11. package/dist/commands/api/index.js +88 -0
  12. package/dist/commands/api/index.js.map +1 -0
  13. package/dist/commands/attachment/index.js +54 -0
  14. package/dist/commands/attachment/index.js.map +1 -0
  15. package/dist/commands/auth/index.js +238 -0
  16. package/dist/commands/auth/index.js.map +1 -0
  17. package/dist/commands/calendar/index.js +167 -0
  18. package/dist/commands/calendar/index.js.map +1 -0
  19. package/dist/commands/chat/index.js +97 -0
  20. package/dist/commands/chat/index.js.map +1 -0
  21. package/dist/commands/file/index.js +42 -0
  22. package/dist/commands/file/index.js.map +1 -0
  23. package/dist/commands/global-wiki/index.js +196 -0
  24. package/dist/commands/global-wiki/index.js.map +1 -0
  25. package/dist/commands/issue/builds.js +160 -0
  26. package/dist/commands/issue/builds.js.map +1 -0
  27. package/dist/commands/issue/index.js +169 -0
  28. package/dist/commands/issue/index.js.map +1 -0
  29. package/dist/commands/issue/sub.js +149 -0
  30. package/dist/commands/issue/sub.js.map +1 -0
  31. package/dist/commands/issue/subbuilds.js +174 -0
  32. package/dist/commands/issue/subbuilds.js.map +1 -0
  33. package/dist/commands/milestone/index.js +104 -0
  34. package/dist/commands/milestone/index.js.map +1 -0
  35. package/dist/commands/note/builds.js +68 -0
  36. package/dist/commands/note/builds.js.map +1 -0
  37. package/dist/commands/note/index.js +122 -0
  38. package/dist/commands/note/index.js.map +1 -0
  39. package/dist/commands/preview-config/index.js +87 -0
  40. package/dist/commands/preview-config/index.js.map +1 -0
  41. package/dist/commands/project/builds.js +78 -0
  42. package/dist/commands/project/builds.js.map +1 -0
  43. package/dist/commands/project/index.js +103 -0
  44. package/dist/commands/project/index.js.map +1 -0
  45. package/dist/commands/scheduled-task/index.js +196 -0
  46. package/dist/commands/scheduled-task/index.js.map +1 -0
  47. package/dist/commands/site-credential/index.js +23 -0
  48. package/dist/commands/site-credential/index.js.map +1 -0
  49. package/dist/commands/skill/index.js +121 -0
  50. package/dist/commands/skill/index.js.map +1 -0
  51. package/dist/commands/storage/index.js +103 -0
  52. package/dist/commands/storage/index.js.map +1 -0
  53. package/dist/commands/user/index.js +55 -0
  54. package/dist/commands/user/index.js.map +1 -0
  55. package/dist/commands/wiki/builds.js +135 -0
  56. package/dist/commands/wiki/builds.js.map +1 -0
  57. package/dist/commands/wiki/index.js +177 -0
  58. package/dist/commands/wiki/index.js.map +1 -0
  59. package/dist/commands/workflow/index.js +111 -0
  60. package/dist/commands/workflow/index.js.map +1 -0
  61. package/dist/config.js +123 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/context.js +61 -0
  64. package/dist/context.js.map +1 -0
  65. package/dist/errors/hint-map.js +81 -0
  66. package/dist/errors/hint-map.js.map +1 -0
  67. package/dist/errors/index.js +132 -0
  68. package/dist/errors/index.js.map +1 -0
  69. package/dist/index.js +17 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/output/format.js +108 -0
  72. package/dist/output/format.js.map +1 -0
  73. package/dist/output/index.js +3 -0
  74. package/dist/output/index.js.map +1 -0
  75. package/dist/output/tty.js +22 -0
  76. package/dist/output/tty.js.map +1 -0
  77. package/dist/registry.js +47 -0
  78. package/dist/registry.js.map +1 -0
  79. package/dist/token-store.js +92 -0
  80. package/dist/token-store.js.map +1 -0
  81. package/dist/types/api.js +8 -0
  82. package/dist/types/api.js.map +1 -0
  83. package/dist/utils/bulk.js +29 -0
  84. package/dist/utils/bulk.js.map +1 -0
  85. package/dist/utils/download.js +36 -0
  86. package/dist/utils/download.js.map +1 -0
  87. package/dist/utils/file-utils.js +58 -0
  88. package/dist/utils/file-utils.js.map +1 -0
  89. package/dist/utils/input.js +66 -0
  90. package/dist/utils/input.js.map +1 -0
  91. package/dist/utils/issue-resolver.js +20 -0
  92. package/dist/utils/issue-resolver.js.map +1 -0
  93. package/dist/utils/loopback.js +86 -0
  94. package/dist/utils/loopback.js.map +1 -0
  95. package/dist/utils/pkce.js +20 -0
  96. package/dist/utils/pkce.js.map +1 -0
  97. package/dist/utils/project-resolver.js +63 -0
  98. package/dist/utils/project-resolver.js.map +1 -0
  99. package/dist/utils/prompt.js +17 -0
  100. package/dist/utils/prompt.js.map +1 -0
  101. package/dist/utils/upload.js +32 -0
  102. package/dist/utils/upload.js.map +1 -0
  103. package/package.json +44 -0
  104. package/skills/harvask/AGENTS.md +131 -0
  105. package/skills/harvask/GEMINI.md +131 -0
  106. package/skills/harvask/SKILL.md +134 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ファイル関連ユーティリティ(kairon utils/file-utils.ts から流用)。
3
+ */
4
+ /**
5
+ * 拡張子から MIME タイプを判定する。
6
+ */
7
+ export function getMimeType(filename) {
8
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
9
+ const mimeTypes = {
10
+ png: 'image/png',
11
+ jpg: 'image/jpeg',
12
+ jpeg: 'image/jpeg',
13
+ gif: 'image/gif',
14
+ webp: 'image/webp',
15
+ svg: 'image/svg+xml',
16
+ ico: 'image/x-icon',
17
+ bmp: 'image/bmp',
18
+ pdf: 'application/pdf',
19
+ doc: 'application/msword',
20
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
21
+ xls: 'application/vnd.ms-excel',
22
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
23
+ ppt: 'application/vnd.ms-powerpoint',
24
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
25
+ txt: 'text/plain',
26
+ csv: 'text/csv',
27
+ md: 'text/markdown',
28
+ html: 'text/html',
29
+ css: 'text/css',
30
+ js: 'application/javascript',
31
+ ts: 'application/typescript',
32
+ json: 'application/json',
33
+ xml: 'application/xml',
34
+ yaml: 'application/x-yaml',
35
+ yml: 'application/x-yaml',
36
+ zip: 'application/zip',
37
+ gz: 'application/gzip',
38
+ tar: 'application/x-tar',
39
+ mp3: 'audio/mpeg',
40
+ mp4: 'video/mp4',
41
+ wav: 'audio/wav',
42
+ };
43
+ return mimeTypes[ext] || 'application/octet-stream';
44
+ }
45
+ /**
46
+ * バイト数を人間可読形式に変換する。
47
+ */
48
+ export function formatFileSize(bytes) {
49
+ if (bytes === 0) {
50
+ return '0 B';
51
+ }
52
+ const units = ['B', 'KB', 'MB', 'GB'];
53
+ const k = 1024;
54
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
55
+ const size = bytes / Math.pow(k, i);
56
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
57
+ }
58
+ //# sourceMappingURL=file-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-utils.js","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC3D,MAAM,SAAS,GAA2B;QACxC,GAAG,EAAE,WAAW;QAChB,GAAG,EAAE,YAAY;QACjB,IAAI,EAAE,YAAY;QAClB,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,YAAY;QAClB,GAAG,EAAE,eAAe;QACpB,GAAG,EAAE,cAAc;QACnB,GAAG,EAAE,WAAW;QAChB,GAAG,EAAE,iBAAiB;QACtB,GAAG,EAAE,oBAAoB;QACzB,IAAI,EAAE,yEAAyE;QAC/E,GAAG,EAAE,0BAA0B;QAC/B,IAAI,EAAE,mEAAmE;QACzE,GAAG,EAAE,+BAA+B;QACpC,IAAI,EAAE,2EAA2E;QACjF,GAAG,EAAE,YAAY;QACjB,GAAG,EAAE,UAAU;QACf,EAAE,EAAE,eAAe;QACnB,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,UAAU;QACf,EAAE,EAAE,wBAAwB;QAC5B,EAAE,EAAE,wBAAwB;QAC5B,IAAI,EAAE,kBAAkB;QACxB,GAAG,EAAE,iBAAiB;QACtB,IAAI,EAAE,oBAAoB;QAC1B,GAAG,EAAE,oBAAoB;QACzB,GAAG,EAAE,iBAAiB;QACtB,EAAE,EAAE,kBAAkB;QACtB,GAAG,EAAE,mBAAmB;QACxB,GAAG,EAAE,YAAY;QACjB,GAAG,EAAE,WAAW;QAChB,GAAG,EAAE,WAAW;KACjB,CAAC;IACF,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;AACtD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 入力ユーティリティ
3
+ *
4
+ * 長文 Markdown / JSON を、リテラル・@file・-(stdin)から読み取る。
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { ValidationError } from '../errors/index.js';
8
+ /**
9
+ * 値を解決する: '-' は stdin、先頭 '@' はファイル、それ以外はリテラル。
10
+ */
11
+ export function readContentArg(value) {
12
+ if (value === '-') {
13
+ return readFileSync(0, 'utf8');
14
+ }
15
+ if (value.startsWith('@')) {
16
+ const path = value.slice(1);
17
+ try {
18
+ return readFileSync(path, 'utf8');
19
+ }
20
+ catch (e) {
21
+ throw new ValidationError(`File not found or unreadable: ${path} (${e.message})`);
22
+ }
23
+ }
24
+ return value;
25
+ }
26
+ /**
27
+ * JSON 引数を解決する(リテラル / @file / -)。
28
+ */
29
+ export function parseJsonArg(value) {
30
+ const raw = readContentArg(value);
31
+ try {
32
+ return JSON.parse(raw);
33
+ }
34
+ catch (e) {
35
+ throw new ValidationError(`Invalid JSON: ${e.message}`);
36
+ }
37
+ }
38
+ /**
39
+ * "1,2,3" のようなカンマ区切りを number[] に変換する。
40
+ */
41
+ export function parseNumberList(value) {
42
+ return value
43
+ .split(',')
44
+ .map((s) => s.trim())
45
+ .filter((s) => s !== '')
46
+ .map((s) => {
47
+ if (!INTEGER_RE.test(s)) {
48
+ throw new ValidationError(`Invalid number in list: "${s}"`);
49
+ }
50
+ return Number(s);
51
+ });
52
+ }
53
+ /** 10進整数のみを許可する('', '0x10', '1e3', '3.0', ' 5 ' などは拒否)。 */
54
+ const INTEGER_RE = /^-?\d+$/;
55
+ /**
56
+ * 文字列を整数に変換する(commander の引数パーサ向け)。
57
+ * Number() の緩い変換(16進・指数・空文字を id/page として受理してしまう)を避け、
58
+ * 10進整数表記のみを受け付ける。
59
+ */
60
+ export function parseIntArg(value) {
61
+ if (!INTEGER_RE.test(value)) {
62
+ throw new ValidationError(`Expected an integer, got "${value}"`);
63
+ }
64
+ return Number(value);
65
+ }
66
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/utils/input.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,eAAe,CAAC,iCAAiC,IAAI,KAAM,CAAW,CAAC,OAAO,GAAG,CAAC,CAAC;QAC/F,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,eAAe,CAAC,iBAAkB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;SACvB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,eAAe,CAAC,4BAA4B,CAAC,GAAG,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;AACP,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,eAAe,CAAC,6BAA6B,KAAK,GAAG,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * issue 識別子の解決(数値idはそのまま、キーは search で id に解決)。
3
+ * issue 本体・子リソース(comment/todo/memory/dependency)で共有。
4
+ */
5
+ import { API_NS } from '../api-base.js';
6
+ import { resolveProjectId } from './project-resolver.js';
7
+ import { ValidationError } from '../errors/index.js';
8
+ export async function resolveIssueId(client, projectArg, value) {
9
+ if (/^\d+$/.test(value)) {
10
+ return parseInt(value, 10);
11
+ }
12
+ const pid = await resolveProjectId(client, projectArg ?? {});
13
+ const res = await client.get(`${API_NS}/projects/${pid}/issues/search?query=${encodeURIComponent(value)}&limit=50`);
14
+ const match = (res.results || []).find((r) => String(r.key).toUpperCase() === value.toUpperCase());
15
+ if (!match) {
16
+ throw new ValidationError(`Issue "${value}" not found. Pass a numeric issue id, or a valid key (see \`harvask issue search\`).`);
17
+ }
18
+ return match.id;
19
+ }
20
+ //# sourceMappingURL=issue-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-resolver.js","sourceRoot":"","sources":["../../src/utils/issue-resolver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAmB,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAwB,EACxB,UAAkC,EAClC,KAAa;IAEb,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAC1B,GAAG,MAAM,aAAa,GAAG,wBAAwB,kBAAkB,CAAC,KAAK,CAAC,WAAW,CACtF,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CACpC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,WAAW,EAAE,CAC3D,CAAC;IACF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,eAAe,CACvB,UAAU,KAAK,sFAAsF,CACtG,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC,EAAE,CAAC;AAClB,CAAC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ループバック受け口
3
+ *
4
+ * `127.0.0.1:<random>` に一時 HTTP サーバを立て、ブラウザ認可後のリダイレクト
5
+ * `/callback?request_id=...&code=...` を受け取る。
6
+ */
7
+ import http from 'node:http';
8
+ /**
9
+ * コールバックパス(/callback?request_id&code)を解析する純粋関数。
10
+ */
11
+ export function parseCallback(reqUrl, base = 'http://127.0.0.1') {
12
+ let u;
13
+ try {
14
+ u = new URL(reqUrl, base);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ if (u.pathname !== '/callback') {
20
+ return null;
21
+ }
22
+ const code = u.searchParams.get('code') ?? '';
23
+ if (!code) {
24
+ return null;
25
+ }
26
+ return { requestId: u.searchParams.get('request_id') ?? '', code };
27
+ }
28
+ const DONE_HTML = `<!doctype html><html lang="ja"><head><meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width,initial-scale=1"><title>harvask</title></head>
30
+ <body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f1f5f9;font-family:system-ui,-apple-system,'Segoe UI',sans-serif;color:#0f172a">
31
+ <div style="text-align:center;background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:40px 48px;box-shadow:0 10px 30px rgba(15,23,42,.06);max-width:360px">
32
+ <div style="font-size:40px;line-height:1">&#9989;</div>
33
+ <h1 style="font-size:1.125rem;margin:12px 0 6px;font-weight:600">ログインが完了しました</h1>
34
+ <p style="color:#64748b;margin:0;font-size:.875rem">ターミナルに戻ってください。このタブは閉じて構いません。</p>
35
+ </div></body></html>`;
36
+ /**
37
+ * ループバックサーバを起動し、redirectUri と callback 待受を返す。
38
+ */
39
+ export async function startLoopback() {
40
+ const server = http.createServer();
41
+ await new Promise((resolve, reject) => {
42
+ server.once('error', reject);
43
+ server.listen(0, '127.0.0.1', () => resolve());
44
+ });
45
+ const port = server.address().port;
46
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
47
+ let resolveResult;
48
+ const result = new Promise((res) => {
49
+ resolveResult = res;
50
+ });
51
+ // 期待する request_id(/cli/auth/start の戻り値)。CSRF 的な偽コールバック対策。
52
+ let expectedRequestId;
53
+ function expectRequestId(id) {
54
+ expectedRequestId = id;
55
+ }
56
+ server.on('request', (req, res) => {
57
+ const parsed = req.url ? parseCallback(req.url, redirectUri) : null;
58
+ // このログイン試行の request_id と完全一致しないコールバックは無視する(空・欠落も拒否)。
59
+ // 正規サーバのリダイレクトは常に request_id を含むため、ローカルの悪意あるページ等が
60
+ // code を投げ込んでも正しい request_id を推測できず弾かれる(ログインCSRF対策)。
61
+ const mismatched = parsed !== null &&
62
+ expectedRequestId !== undefined &&
63
+ parsed.requestId !== expectedRequestId;
64
+ if (!parsed || mismatched) {
65
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
66
+ res.end('not found');
67
+ return;
68
+ }
69
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
70
+ res.end(DONE_HTML);
71
+ resolveResult(parsed);
72
+ });
73
+ function wait(timeoutMs = 5 * 60 * 1000) {
74
+ return Promise.race([
75
+ result,
76
+ new Promise((_, rej) => setTimeout(() => rej(new Error('Timed out waiting for browser authorization')), timeoutMs)),
77
+ ]);
78
+ }
79
+ function close() {
80
+ // keep-alive 接続が残るとプロセスが終了しないため、明示的に全接続を閉じる
81
+ server.closeAllConnections?.();
82
+ server.close();
83
+ }
84
+ return { redirectUri, expectRequestId, wait, close };
85
+ }
86
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../src/utils/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PKCE (RFC 7636) — S256
3
+ */
4
+ import { randomBytes, createHash } from 'node:crypto';
5
+ export function base64url(buf) {
6
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
7
+ }
8
+ /**
9
+ * code_verifier を生成する(base64url, 64文字程度)。
10
+ */
11
+ export function generateVerifier() {
12
+ return base64url(randomBytes(48));
13
+ }
14
+ /**
15
+ * code_challenge = base64url(sha256(verifier))(S256)。純粋関数。
16
+ */
17
+ export function challengeFor(verifier) {
18
+ return base64url(createHash('sha256').update(verifier).digest());
19
+ }
20
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/utils/pkce.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Project Resolver
3
+ *
4
+ * project_key から project_id を解決するユーティリティ(セッション内キャッシュ付き)。
5
+ * kairon MCP (mcp-servers/kairon/src/utils/project-resolver.ts) から流用。
6
+ */
7
+ import { ApiClientError } from '../api-client.js';
8
+ import { API_NS } from '../api-base.js';
9
+ const projectKeyCache = new Map();
10
+ /**
11
+ * project_id または project_key からプロジェクトIDを解決する。
12
+ *
13
+ * @throws ProjectIdentifierRequiredError どちらも未指定の場合
14
+ * @throws ProjectKeyNotFoundError キーが見つからない場合
15
+ */
16
+ export async function resolveProjectId(apiClient, arg) {
17
+ const projectId = arg.id;
18
+ const projectKey = arg.key;
19
+ // 1. project_id 優先
20
+ if (projectId !== undefined && projectId !== null) {
21
+ return projectId;
22
+ }
23
+ // 2. project_key を API で解決(キャッシュあり)
24
+ if (projectKey !== undefined && projectKey !== null && projectKey.trim() !== '') {
25
+ const cacheKey = projectKey.toUpperCase();
26
+ const cached = projectKeyCache.get(cacheKey);
27
+ if (cached !== undefined) {
28
+ return cached;
29
+ }
30
+ try {
31
+ const response = await apiClient.get(`${API_NS}/projects/resolve?key=${encodeURIComponent(projectKey)}`);
32
+ projectKeyCache.set(cacheKey, response.project_id);
33
+ return response.project_id;
34
+ }
35
+ catch (error) {
36
+ if (error instanceof ApiClientError && error.statusCode === 404) {
37
+ throw new ProjectKeyNotFoundError(`プロジェクトキー "${projectKey}" が見つかりませんでした。`);
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ // 3. どちらも未指定
43
+ throw new ProjectIdentifierRequiredError('プロジェクトを指定してください(-p, --project に key または id)。');
44
+ }
45
+ /**
46
+ * キャッシュをクリア(テスト用)
47
+ */
48
+ export function clearProjectKeyCache() {
49
+ projectKeyCache.clear();
50
+ }
51
+ export class ProjectKeyNotFoundError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = 'ProjectKeyNotFoundError';
55
+ }
56
+ }
57
+ export class ProjectIdentifierRequiredError extends Error {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = 'ProjectIdentifierRequiredError';
61
+ }
62
+ }
63
+ //# sourceMappingURL=project-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-resolver.js","sourceRoot":"","sources":["../../src/utils/project-resolver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,MAAM,eAAe,GAAwB,IAAI,GAAG,EAAE,CAAC;AAOvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAA2B,EAC3B,GAAe;IAEf,MAAM,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC;IAE3B,mBAAmB;IACnB,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QAClD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,oCAAoC;IACpC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAChF,MAAM,QAAQ,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,CAClC,GAAG,MAAM,yBAAyB,kBAAkB,CAAC,UAAU,CAAC,EAAE,CACnE,CAAC;YACF,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;YACnD,OAAO,QAAQ,CAAC,UAAU,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,cAAc,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;gBAChE,MAAM,IAAI,uBAAuB,CAAC,aAAa,UAAU,gBAAgB,CAAC,CAAC;YAC7E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,aAAa;IACb,MAAM,IAAI,8BAA8B,CACtC,8CAA8C,CAC/C,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,eAAe,CAAC,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,8BAA+B,SAAQ,KAAK;IACvD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gCAAgC,CAAC;IAC/C,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 対話プロンプト(TTY)
3
+ */
4
+ import readline from 'node:readline';
5
+ /**
6
+ * 通常の入力プロンプト。
7
+ */
8
+ export function prompt(question) {
9
+ return new Promise((resolve) => {
10
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
11
+ rl.question(question, (answer) => {
12
+ rl.close();
13
+ resolve(answer);
14
+ });
15
+ });
16
+ }
17
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/utils/prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,QAAQ,MAAM,eAAe,CAAC;AAErC;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACtF,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;YAC/B,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * マルチパートアップロード用 FormData 構築。
3
+ */
4
+ import { readFileSync } from 'node:fs';
5
+ import { basename } from 'node:path';
6
+ import { ValidationError } from '../errors/index.js';
7
+ import { getMimeType } from './file-utils.js';
8
+ /**
9
+ * ローカルファイルと追加フィールドから FormData を作る。
10
+ * フィールド名 `file` でファイルを添付する(API 仕様)。
11
+ */
12
+ export function buildFileFormData(filePath, fields = {}) {
13
+ let buffer;
14
+ try {
15
+ buffer = readFileSync(filePath);
16
+ }
17
+ catch (e) {
18
+ throw new ValidationError(`File not found or unreadable: ${filePath} (${e.message})`);
19
+ }
20
+ const name = basename(filePath);
21
+ const form = new FormData();
22
+ // undici の FormData は Blob + filename を受け付ける(Buffer を Uint8Array ビューに包む)。
23
+ const blob = new Blob([new Uint8Array(buffer)], { type: getMimeType(name) });
24
+ form.append('file', blob, name);
25
+ for (const [key, value] of Object.entries(fields)) {
26
+ if (value !== undefined && value !== null) {
27
+ form.append(key, String(value));
28
+ }
29
+ }
30
+ return form;
31
+ }
32
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/utils/upload.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,SAAsD,EAAE;IAExD,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,eAAe,CAAC,iCAAiC,QAAQ,KAAM,CAAW,CAAC,OAAO,GAAG,CAAC,CAAC;IACnG,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "harvask-cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line client for a harvask tenant, built for coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "harvask": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skills",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "scripts": {
18
+ "clean": "rm -rf dist",
19
+ "gen:agents": "node scripts/gen-agent-docs.mjs",
20
+ "build": "npm run clean && npm run gen:agents && tsc -p tsconfig.build.json && chmod +x dist/index.js",
21
+ "build:test": "npm run clean && tsc && chmod +x dist/index.js",
22
+ "dev": "tsc -p tsconfig.build.json --watch",
23
+ "test": "npm run build:test && node --test \"dist/__tests__/**/*.test.js\"",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "cli",
28
+ "harvask",
29
+ "tenant",
30
+ "ai-worker",
31
+ "coding-agent"
32
+ ],
33
+ "author": "wown",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "commander": "^12.1.0",
37
+ "env-paths": "^3.0.0",
38
+ "open": "^10.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.10.0",
42
+ "typescript": "^5.6.0"
43
+ }
44
+ }
@@ -0,0 +1,131 @@
1
+ <!-- Generated from skills/harvask/SKILL.md by scripts/gen-agent-docs.mjs. Edit that file, then run `npm run gen:agents`. Do not edit skills/harvask/AGENTS.md by hand. -->
2
+
3
+ # harvask CLI
4
+
5
+ `harvask` is a command-line client for a harvask tenant, designed for coding
6
+ agents: output is JSON by default and errors are structured as
7
+ `Error / Why / Hint`.
8
+
9
+ ## First: authenticate
10
+
11
+ Login is browser-based (no password typed into the CLI):
12
+
13
+ ```
14
+ harvask login --tenant <tenant-domain> # opens browser, approve, done (loopback)
15
+ harvask login --tenant <tenant-domain> --no-browser # print URL, approve, paste the code (remote/headless)
16
+ ```
17
+
18
+ You must be logged into the web app to approve. Tokens are stored per tenant
19
+ under your OS config dir (mode 0600). Check status with `harvask whoami`.
20
+ Log out with `harvask logout`.
21
+
22
+ If a command fails with an auth error (exit code 3), run `harvask login` again.
23
+
24
+ ## Core workflow: discover → fetch → act
25
+
26
+ 1. **discover** the target
27
+ ```
28
+ harvask project list
29
+ harvask issue search -p PRJ "login bug"
30
+ ```
31
+ 2. **fetch** details
32
+ ```
33
+ harvask issue show -p PRJ 1234 --comments
34
+ harvask wiki show -p PRJ --slug api-documentation
35
+ ```
36
+ 3. **act**
37
+ ```
38
+ harvask issue create -p PRJ --title "Fix login" --type bug --priority high
39
+ harvask issue status -p PRJ 1234 in_progress
40
+ harvask issue assign -p PRJ 1234 --assignee-id 42
41
+ ```
42
+
43
+ ## project key vs id
44
+
45
+ - Humans know the **key** (e.g. `PRJ`); the API uses a numeric **id**.
46
+ - Every project-scoped command accepts either via `-p, --project <key|id>`
47
+ (`-p PRJ` or `-p 5`). The CLI resolves keys to ids automatically.
48
+ - For `issue show/update/status/assign`, the issue argument accepts a numeric id
49
+ or a key like `PRJ-12` (resolved via search).
50
+ - New to a tenant? Start with `harvask project list`.
51
+
52
+ ## Reading output
53
+
54
+ - Default output is pretty JSON.
55
+ - `--data` prints only the payload (unwraps `{issues:[...]}` → the array).
56
+ - `--raw` prints the unformatted response.
57
+ - `--json` forces machine-readable JSON (no color), even in a terminal.
58
+ - Results go to stdout; errors go to stderr.
59
+
60
+ ## Long input (Markdown / JSON)
61
+
62
+ - `--content` accepts a literal string, `@file.md` (read a file), or `-` (stdin).
63
+ ```
64
+ harvask wiki create -p PRJ --title "Docs" --content @docs.md
65
+ harvask note create --content - # read from stdin
66
+ ```
67
+ - The `api` escape hatch accepts a JSON body the same way (`@body.json`, `-`).
68
+
69
+ ## Common errors and what to do
70
+
71
+ - **exit 3 / 401** — token invalid or expired → `harvask login --tenant <domain>`.
72
+ - **403 tenant_mismatch** — token is for another tenant → use `--profile <domain>`
73
+ or re-login against the correct tenant.
74
+ - **403 not_project_member** — you are not a member of that project → pick a
75
+ project from `harvask project list` or ask an admin to add you.
76
+ - **404** — wrong id/key → verify with `harvask project list` / `harvask issue search`.
77
+ - **422** — validation: read `Why:` for the offending field
78
+ (e.g. `issue_type`/`priority` enums, `title` ≤ 255, wiki/note content ≤ 500000).
79
+
80
+ ## Escape hatch (anything not wrapped)
81
+
82
+ `harvask api <method> <path> [json]` calls any tenant API endpoint directly,
83
+ for anything not covered by a dedicated command. Relative paths are namespaced
84
+ automatically; absolute URLs and `..` are rejected.
85
+
86
+ ```
87
+ harvask api GET projects
88
+ harvask api GET projects/5/issues?status=open
89
+ harvask api POST notes '{"content":"hi","is_pinned":true}'
90
+ harvask api POST projects/5/issues @issue.json
91
+ ```
92
+
93
+ ## Command summary
94
+
95
+ ```
96
+ harvask login | logout | whoami
97
+
98
+ # project-scoped (pass -p <key|id>)
99
+ harvask project list | show <key|id> | create | members list|add|remove <key|id>
100
+ harvask issue list | show | create | update | search | status | assign
101
+ harvask issue comment list | create
102
+ harvask issue todo list | show | create | update | delete
103
+ harvask issue memory list | show | create | update | delete
104
+ harvask issue dependency list | add | remove
105
+ harvask issue bulk-create | bulk-update (--items @file.json [--atomic])
106
+ harvask wiki list | show | create | update | search | history | delete | bulk-create | bulk-update
107
+ harvask milestone list | create | update
108
+ harvask workflow list | show | create | update
109
+ harvask scheduled-task list | show | create | update
110
+ harvask file search | stats
111
+ harvask storage search | stats | upload | folder | move | download | content
112
+ harvask preview-config show | update
113
+ harvask site-credential # list browser credentials
114
+
115
+ # personal / tenant-wide (no -p)
116
+ harvask note list | show | create | update | delete | search
117
+ harvask calendar list | show | create | update | delete
118
+ harvask global-wiki list | show | create | update | search | delete | bulk-create | bulk-update
119
+ harvask user list | search
120
+ harvask chat history <chatId> | search <q> | list | thread <kind> <uuid>
121
+ harvask attachment upload | download <id>
122
+
123
+ harvask api <method> <path> [json] # escape hatch
124
+ ```
125
+
126
+ Notes:
127
+ - `personal-wiki` is intentionally not available via the CLI.
128
+ - File/binary: `storage upload` / `attachment upload` take `--file <path>`;
129
+ `storage download` / `attachment download` save with `--out <path>`.
130
+
131
+ Use `harvask <command> --help` for options, examples, and prerequisites.