cloudhouse-admin-cli 0.3.3

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 (96) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +63 -0
  4. package/SECURITY.md +11 -0
  5. package/THIRD_PARTY_NOTICES.md +7 -0
  6. package/bin/ch.mjs +11 -0
  7. package/docs/00-overview.md +71 -0
  8. package/docs/01-command-reference.md +1206 -0
  9. package/docs/02-agent-contract.md +118 -0
  10. package/docs/03-agent-setup.md +69 -0
  11. package/docs/04-audit.md +3 -0
  12. package/docs/05-release.md +5 -0
  13. package/docs/06-coverage.md +3 -0
  14. package/docs/07-data-analysis.md +105 -0
  15. package/docs/08-installation.md +36 -0
  16. package/docs/security-review.md +26 -0
  17. package/integrations/codex/.agents/plugins/marketplace.json +20 -0
  18. package/integrations/codex/.codex-plugin/plugin.json +10 -0
  19. package/integrations/codex/LICENSE +21 -0
  20. package/integrations/codex/README.md +5 -0
  21. package/integrations/codex/package.json +7 -0
  22. package/integrations/codex/plugin.json +11 -0
  23. package/integrations/codex/skills/cloudhouse-admin/SKILL.md +32 -0
  24. package/integrations/codex/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  25. package/integrations/codex/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  26. package/integrations/dsh/LICENSE +21 -0
  27. package/integrations/dsh/README.md +14 -0
  28. package/integrations/dsh/index.mjs +7 -0
  29. package/integrations/dsh/package.json +12 -0
  30. package/integrations/dsh/skills/cloudhouse-admin/SKILL.md +32 -0
  31. package/integrations/dsh/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  32. package/integrations/dsh/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  33. package/integrations/openclaw/LICENSE +21 -0
  34. package/integrations/openclaw/README.md +5 -0
  35. package/integrations/openclaw/package.json +7 -0
  36. package/integrations/openclaw/skills/cloudhouse-admin/SKILL.md +32 -0
  37. package/integrations/openclaw/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  38. package/integrations/openclaw/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  39. package/integrations/opencode/LICENSE +21 -0
  40. package/integrations/opencode/README.md +5 -0
  41. package/integrations/opencode/package.json +7 -0
  42. package/integrations/opencode/skills/cloudhouse-admin/SKILL.md +32 -0
  43. package/integrations/opencode/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  44. package/integrations/opencode/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  45. package/integrations/pi/LICENSE +21 -0
  46. package/integrations/pi/README.md +5 -0
  47. package/integrations/pi/package.json +15 -0
  48. package/integrations/pi/skills/cloudhouse-admin/SKILL.md +32 -0
  49. package/integrations/pi/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  50. package/integrations/pi/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  51. package/package.json +44 -0
  52. package/skills/cloudhouse-admin/SKILL.md +32 -0
  53. package/skills/cloudhouse-admin/references/01-command-reference.md +1206 -0
  54. package/skills/cloudhouse-admin/references/02-agent-contract.md +118 -0
  55. package/src/agent-install.mjs +50 -0
  56. package/src/analysis-resources.json +1207 -0
  57. package/src/argv.mjs +88 -0
  58. package/src/capabilities.mjs +18 -0
  59. package/src/cli.mjs +180 -0
  60. package/src/client.mjs +169 -0
  61. package/src/commands/accounts.mjs +115 -0
  62. package/src/commands/agent.mjs +13 -0
  63. package/src/commands/ai.mjs +420 -0
  64. package/src/commands/analysis.mjs +124 -0
  65. package/src/commands/api.mjs +63 -0
  66. package/src/commands/applies.mjs +89 -0
  67. package/src/commands/assistant.mjs +53 -0
  68. package/src/commands/auth.mjs +109 -0
  69. package/src/commands/candidates.mjs +252 -0
  70. package/src/commands/checkin.mjs +41 -0
  71. package/src/commands/dashboard.mjs +11 -0
  72. package/src/commands/evaluations.mjs +27 -0
  73. package/src/commands/extended.mjs +96 -0
  74. package/src/commands/feishu.mjs +31 -0
  75. package/src/commands/groups.mjs +66 -0
  76. package/src/commands/index.mjs +101 -0
  77. package/src/commands/interviews.mjs +151 -0
  78. package/src/commands/load.mjs +27 -0
  79. package/src/commands/notifications.mjs +131 -0
  80. package/src/commands/plans.mjs +90 -0
  81. package/src/commands/profile.mjs +43 -0
  82. package/src/commands/slots.mjs +95 -0
  83. package/src/commands/uploads.mjs +169 -0
  84. package/src/config.mjs +68 -0
  85. package/src/errors.mjs +156 -0
  86. package/src/flags.mjs +114 -0
  87. package/src/idempotent.mjs +75 -0
  88. package/src/output.mjs +138 -0
  89. package/src/path.mjs +40 -0
  90. package/src/payload.mjs +137 -0
  91. package/src/prompt.mjs +47 -0
  92. package/src/query.mjs +44 -0
  93. package/src/sensitive.mjs +50 -0
  94. package/src/session.mjs +94 -0
  95. package/src/time.mjs +87 -0
  96. package/src/version.mjs +1 -0
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs'
2
+ import crypto from 'node:crypto'
3
+ import path from 'node:path'
4
+ import { configDir } from './config.mjs'
5
+
6
+ const SESSION_FILE = 'session.json'
7
+
8
+ function sessionFilePath(env = process.env) {
9
+ return path.join(configDir(env), SESSION_FILE)
10
+ }
11
+
12
+ /** 读取磁盘会话;文件不存在/损坏/无 token 时返回 null */
13
+ export function loadSession(env = process.env) {
14
+ try {
15
+ const parsed = JSON.parse(fs.readFileSync(sessionFilePath(env), 'utf8'))
16
+ if (!parsed || typeof parsed.token !== 'string' || !parsed.token) return null
17
+ return parsed
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 区分「没有会话」与「会话文件读不出来」。
25
+ * loadSession 一律返回 null 是有意的(调用方要能继续走未登录分支,也有测试钉住它),
26
+ * 但对用户来说「会话文件坏了」和「从没登录过」是完全不同的处置方式,
27
+ * 所以坏掉时必须能问出来——否则表现是让人重新登录,而登录又会覆盖坏文件。
28
+ * @returns {null | {file:string, reason:string}}
29
+ */
30
+ export function describeSessionDamage(env = process.env) {
31
+ const file = sessionFilePath(env)
32
+ let raw
33
+ try {
34
+ raw = fs.readFileSync(file, 'utf8')
35
+ } catch (err) {
36
+ return err.code === 'ENOENT' ? null : { file, reason: `无法读取(${err.code || err.message})` }
37
+ }
38
+ try {
39
+ const parsed = JSON.parse(raw)
40
+ if (!parsed || typeof parsed.token !== 'string' || !parsed.token) return { file, reason: '文件里没有 token' }
41
+ return null
42
+ } catch (err) {
43
+ return { file, reason: `不是合法 JSON(${err.message})` }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 写入会话。
49
+ * token 是敏感凭据:目录 0700、文件 0600,且必须先落临时文件再 rename——
50
+ * 直接 writeFileSync 覆盖中途崩溃会留下截断的 JSON,而 loadSession 把截断读成 null,
51
+ * 用户看到的是「未登录」而不是「会话坏了」。
52
+ * fs.writeFileSync 的 mode 只在创建时生效,所以已存在 0644 的历史文件必须显式 chmod 收紧。
53
+ */
54
+ export function saveSession(env, session) {
55
+ const file = sessionFilePath(env)
56
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
57
+ try {
58
+ fs.chmodSync(path.dirname(file), 0o700)
59
+ } catch {
60
+ /* 目录属主不是自己时保持原样,不阻断命令 */
61
+ }
62
+ const tmp = `${file}.${crypto.randomUUID()}.tmp`
63
+ fs.writeFileSync(tmp, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
64
+ try { fs.renameSync(tmp, file) }
65
+ finally { try { fs.unlinkSync(tmp) } catch {} }
66
+ return file
67
+ }
68
+
69
+ export function clearSession(env = process.env) {
70
+ try {
71
+ fs.unlinkSync(sessionFilePath(env))
72
+ return true
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 吸收后端滚动续期下发的新 Token(X-Renewed-Token / X-Token-Expires-At)。
80
+ * 并发安全:仅当磁盘会话的 token 仍是本次请求发出的 token 时才换存,
81
+ * 迟到的旧响应不能用旧续期结果覆盖更新的会话(与 admin-web absorbSessionRenewal 同语义)。
82
+ * @returns {boolean} 是否发生了换存
83
+ */
84
+ export function absorbRenewal(env, sentToken, renewedToken, expiresAtMs) {
85
+ if (!sentToken || !renewedToken || renewedToken === sentToken) return false
86
+ const current = loadSession(env)
87
+ if (!current || current.token !== sentToken) return false
88
+ const next = { ...current, token: renewedToken }
89
+ if (Number.isFinite(expiresAtMs)) next.expiresAt = expiresAtMs
90
+ saveSession(env, next)
91
+ return true
92
+ }
93
+
94
+ export { sessionFilePath }
package/src/time.mjs ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 时间处理。
3
+ * - 传统接口:接受 `YYYY-MM-DD HH:mm:ss` 或 ISO,统一输出 `YYYY-MM-DDTHH:mm:ss`(Asia/Shanghai 无偏移,前端原样展示)
4
+ * - AI Coding 接口:统一输出 `YYYY-MM-DDTHH:mm:ss+08:00`(对齐 admin-web toShanghaiIso)
5
+ * Asia/Shanghai 自 1991 年起无夏令时,固定 UTC+8。
6
+ */
7
+ import { usageError } from './errors.mjs'
8
+
9
+ const SHANGHAI_OFFSET_MINUTES = 8 * 60
10
+
11
+ function pad2(value) {
12
+ return String(value).padStart(2, '0')
13
+ }
14
+
15
+ /**
16
+ * Date.UTC 会把 13 月、45 日、99 时静默进位成「另一个合法时间」。
17
+ * 归一化后把六个字段回读逐项比对,对不上就报用法错误——否则排出来的场次
18
+ * 和调用方写的完全不是一回事,而且全程 exit 0。
19
+ */
20
+ function assertWallClock(date, expected, input) {
21
+ const p = shanghaiParts(date)
22
+ const actual = [p.year, p.month, p.day, p.hour, p.minute, p.second]
23
+ if (expected.some((want, i) => Number(actual[i]) !== Number(want))) {
24
+ throw usageError(
25
+ `时间字段超出范围:${input}(会被归一化成 ${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second})`,
26
+ '月份 01-12、日 01-31、时 00-23、分与秒 00-59',
27
+ )
28
+ }
29
+ return date
30
+ }
31
+
32
+ function parseAny(input) {
33
+ if (input instanceof Date) {
34
+ if (Number.isNaN(input.getTime())) throw usageError('无效时间')
35
+ return input
36
+ }
37
+ if (typeof input !== 'string') throw usageError('无效时间')
38
+ const text = input.trim()
39
+ if (!text) throw usageError('无效时间')
40
+ // `YYYY-MM-DD HH:mm:ss`(无时区)按 Asia/Shanghai 解释
41
+ const naive = text.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/)
42
+ if (naive) {
43
+ const [, y, mo, d, h, mi, s = '0'] = naive
44
+ const date = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h) - SHANGHAI_OFFSET_MINUTES / 60, Number(mi), Number(s)))
45
+ return assertWallClock(date, [y, mo, d, h, mi, s], text)
46
+ }
47
+ const dateOnly = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
48
+ if (dateOnly) {
49
+ const [, y, mo, d] = dateOnly
50
+ const date = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), -SHANGHAI_OFFSET_MINUTES / 60))
51
+ return assertWallClock(date, [y, mo, d, '0', '0', '0'], text)
52
+ }
53
+ const parsed = new Date(text)
54
+ if (Number.isNaN(parsed.getTime())) throw usageError(`无效时间:${input}`)
55
+ return parsed
56
+ }
57
+
58
+ function shanghaiParts(date) {
59
+ const shifted = new Date(date.getTime() + SHANGHAI_OFFSET_MINUTES * 60 * 1000)
60
+ return {
61
+ year: shifted.getUTCFullYear(),
62
+ month: pad2(shifted.getUTCMonth() + 1),
63
+ day: pad2(shifted.getUTCDate()),
64
+ hour: pad2(shifted.getUTCHours()),
65
+ minute: pad2(shifted.getUTCMinutes()),
66
+ second: pad2(shifted.getUTCSeconds()),
67
+ }
68
+ }
69
+
70
+ /** 传统接口时间:`YYYY-MM-DDTHH:mm:ss` */
71
+ export function normalizeLocalTime(input) {
72
+ const date = parseAny(input)
73
+ const p = shanghaiParts(date)
74
+ return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}`
75
+ }
76
+
77
+ /** AI Coding 接口时间:`YYYY-MM-DDTHH:mm:ss+08:00` */
78
+ export function toShanghaiOffset(input) {
79
+ const date = parseAny(input)
80
+ const p = shanghaiParts(date)
81
+ return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}+08:00`
82
+ }
83
+
84
+ /** 解析 `YYYY-MM-DDTHH:mm:ss+08:00` 或本地无偏移时间为 epoch 毫秒(比较用) */
85
+ export function parseToEpochMillis(input) {
86
+ return parseAny(input).getTime()
87
+ }
@@ -0,0 +1 @@
1
+ export const VERSION = '0.3.3'