vps-work 2.4.1 → 2.5.1

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 (125) hide show
  1. package/.well-known/skills/index.json +11 -0
  2. package/README.md +10 -0
  3. package/dist/packages/vps-work/src/bin/vps-work.js +271 -0
  4. package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
  5. package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
  6. package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
  7. package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
  8. package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
  9. package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
  10. package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
  11. package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
  12. package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
  13. package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
  14. package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
  15. package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
  16. package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
  17. package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
  18. package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
  19. package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
  20. package/dist/packages/vps-work/src/commands/catalog.js +29 -0
  21. package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
  22. package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
  23. package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
  24. package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
  25. package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
  26. package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
  27. package/dist/packages/vps-work/src/commands/exec.js +46 -0
  28. package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
  29. package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
  30. package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
  31. package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
  32. package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
  33. package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
  34. package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
  35. package/dist/packages/vps-work/src/commands/im/index.js +7 -0
  36. package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
  37. package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
  38. package/dist/packages/vps-work/src/commands/login.js +52 -0
  39. package/dist/packages/vps-work/src/commands/logout.js +12 -0
  40. package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
  41. package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
  42. package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
  43. package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
  44. package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
  45. package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
  46. package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
  47. package/dist/packages/vps-work/src/commands/office/index.js +59 -0
  48. package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
  49. package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
  50. package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
  51. package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
  52. package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
  53. package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
  54. package/dist/packages/vps-work/src/commands/product/index.js +11 -0
  55. package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
  56. package/dist/packages/vps-work/src/commands/reauth.js +127 -0
  57. package/dist/packages/vps-work/src/commands/report/index.js +7 -0
  58. package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
  59. package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
  60. package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
  61. package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
  62. package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
  63. package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
  64. package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
  65. package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
  66. package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
  67. package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
  68. package/dist/packages/vps-work/src/commands/store/index.js +82 -0
  69. package/dist/packages/vps-work/src/commands/task/index.js +7 -0
  70. package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
  71. package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
  72. package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
  73. package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
  74. package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
  75. package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
  76. package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
  77. package/dist/packages/vps-work/src/core/auth.js +240 -0
  78. package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
  79. package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
  80. package/dist/packages/vps-work/src/core/client.js +158 -0
  81. package/dist/packages/vps-work/src/core/config.js +183 -0
  82. package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
  83. package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
  84. package/dist/packages/vps-work/src/core/documents-client.js +57 -0
  85. package/dist/packages/vps-work/src/core/endpoints.js +46 -0
  86. package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
  87. package/dist/packages/vps-work/src/core/errors.js +107 -0
  88. package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
  89. package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
  90. package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
  91. package/dist/packages/vps-work/src/core/im-client.js +64 -0
  92. package/dist/packages/vps-work/src/core/keychain.js +58 -0
  93. package/dist/packages/vps-work/src/core/mail-client.js +82 -0
  94. package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
  95. package/dist/packages/vps-work/src/core/office-client.js +25 -0
  96. package/dist/packages/vps-work/src/core/office-files.js +82 -0
  97. package/dist/packages/vps-work/src/core/paths.js +70 -0
  98. package/dist/packages/vps-work/src/core/public-result.js +43 -0
  99. package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
  100. package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
  101. package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
  102. package/dist/packages/vps-work/src/core/skill-key.js +19 -0
  103. package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
  104. package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
  105. package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
  106. package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
  107. package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
  108. package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
  109. package/dist/packages/vps-work/src/utils/at-file.js +34 -0
  110. package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
  111. package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
  112. package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
  113. package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
  114. package/dist/packages/vps-work/src/utils/prompt.js +46 -0
  115. package/dist/packages/vps-work/src/utils/read-text.js +44 -0
  116. package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
  117. package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
  118. package/dist/shared/im-org-tree/index.js +200 -0
  119. package/dist/vps-work.cjs +246 -44
  120. package/package.json +1 -1
  121. package/scripts/package-desktop-runtime.mjs +43 -0
  122. package/skills/README.md +1 -0
  123. package/skills/vertu-aisales/SKILL.md +18 -0
  124. package/skills/vertu-erp/SKILL.md +17 -0
  125. package/skills/vps-dealer-ops/SKILL.md +92 -0
@@ -0,0 +1,52 @@
1
+ import { VertuError } from '../core/errors.js';
2
+ export function resolveModelAlias(modelAlias, raw) {
3
+ if (!modelAlias) {
4
+ return;
5
+ }
6
+ const canonicalCamel = modelAlias.camelKey;
7
+ const canonicalFile = `${canonicalCamel}File`;
8
+ const aliasValue = raw['model'];
9
+ const aliasFile = raw['modelFile'];
10
+ if (aliasValue !== undefined && aliasFile !== undefined) {
11
+ throw new VertuError('--model and --model-file are mutually exclusive; pass only one.');
12
+ }
13
+ if (aliasValue !== undefined) {
14
+ if (raw[canonicalCamel] !== undefined) {
15
+ throw new VertuError(`--model and --${modelAlias.canonicalFlagName} are mutually exclusive; pass only one.`);
16
+ }
17
+ raw[canonicalCamel] = aliasValue;
18
+ delete raw['model'];
19
+ }
20
+ if (aliasFile !== undefined) {
21
+ if (raw[canonicalFile] !== undefined) {
22
+ throw new VertuError(`--model-file and --${modelAlias.canonicalFlagName}-file are mutually exclusive; pass only one.`);
23
+ }
24
+ raw[canonicalFile] = aliasFile;
25
+ delete raw['modelFile'];
26
+ }
27
+ }
28
+ export function pickPositionalOption(derived) {
29
+ const requiredStrings = derived.filter((o) => o.required && o.kind === 'string');
30
+ if (requiredStrings.length !== 1) {
31
+ return null;
32
+ }
33
+ return requiredStrings[0] ?? null;
34
+ }
35
+ export function pickModelAliasTarget(derived) {
36
+ const keys = new Set(derived.map((o) => o.key));
37
+ if (keys.has('model')) {
38
+ return null;
39
+ }
40
+ const hasModelName = keys.has('model_name');
41
+ const hasResModel = keys.has('res_model');
42
+ if (hasModelName && hasResModel) {
43
+ return null;
44
+ }
45
+ if (hasModelName) {
46
+ return { schemaKey: 'model_name', camelKey: 'modelName', canonicalFlagName: 'model-name' };
47
+ }
48
+ if (hasResModel) {
49
+ return { schemaKey: 'res_model', camelKey: 'resModel', canonicalFlagName: 'res-model' };
50
+ }
51
+ return null;
52
+ }
@@ -0,0 +1,26 @@
1
+ import YAML from 'yaml';
2
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
3
+ export function parseFrontmatter(source) {
4
+ const match = FRONTMATTER_RE.exec(source);
5
+ if (!match) {
6
+ return { data: {}, body: source, hadFrontmatter: false };
7
+ }
8
+ const yamlBlock = match[1] ?? '';
9
+ let data = {};
10
+ try {
11
+ const parsed = YAML.parse(yamlBlock);
12
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
13
+ data = parsed;
14
+ }
15
+ }
16
+ catch {
17
+ data = {};
18
+ }
19
+ const body = source.slice(match[0].length);
20
+ return { data, body, hadFrontmatter: true };
21
+ }
22
+ export function stringifyFrontmatter(data, body) {
23
+ const yaml = YAML.stringify(data, { lineWidth: 0 }).trimEnd();
24
+ const trimmedBody = body.startsWith('\n') ? body.slice(1) : body;
25
+ return `---\n${yaml}\n---\n\n${trimmedBody}`;
26
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Rewrite long-flag tokens that use snake_case (e.g. `--start_date`) into the
3
+ * kebab-case form (`--start-date`) that commander actually has registered.
4
+ *
5
+ * Why: server-side `parameters_schema` keys are snake_case (Python/Odoo
6
+ * convention), and we expose them as kebab-case CLI flags via
7
+ * `schema-to-options.toFlagName`. AI agents and humans reading the schema
8
+ * directly often type the snake_case form first; commander then rejects it
9
+ * with `unknown option '--start_date' (Did you mean --start-date?)`. Rather
10
+ * than burning a retry/round-trip on every such typo, normalize at the argv
11
+ * boundary so both spellings work transparently.
12
+ *
13
+ * Rules:
14
+ * - Only long flags are touched (`--name`, `--name=value`); short flags and
15
+ * positional values pass through unchanged.
16
+ * - Anything after the standard `--` end-of-options sentinel is preserved
17
+ * verbatim, so payload values like `--` `value_with_underscore` are safe.
18
+ * - The value half of `--flag=value` is preserved verbatim — only the flag
19
+ * name (left of the first `=`) is rewritten.
20
+ * - Tokens without an underscore in the flag name are returned by reference
21
+ * to keep the common path allocation-free.
22
+ */
23
+ export function normalizeSnakeFlags(argv) {
24
+ const out = [];
25
+ let passthrough = false;
26
+ for (const tok of argv) {
27
+ if (passthrough) {
28
+ out.push(tok);
29
+ continue;
30
+ }
31
+ if (tok === '--') {
32
+ passthrough = true;
33
+ out.push(tok);
34
+ continue;
35
+ }
36
+ out.push(rewriteLongFlag(tok));
37
+ }
38
+ return out;
39
+ }
40
+ function rewriteLongFlag(tok) {
41
+ // Long flags must start with exactly two dashes followed by an alnum char.
42
+ // We deliberately reject `---foo` and `--=value` to match commander's parser.
43
+ if (!tok.startsWith('--') || tok.length < 3)
44
+ return tok;
45
+ const eq = tok.indexOf('=');
46
+ const name = eq === -1 ? tok.slice(2) : tok.slice(2, eq);
47
+ if (!/^[A-Za-z0-9][\w-]*$/.test(name) || !name.includes('_'))
48
+ return tok;
49
+ const rewritten = `--${name.replace(/_/g, '-')}`;
50
+ return eq === -1 ? rewritten : `${rewritten}${tok.slice(eq)}`;
51
+ }
@@ -0,0 +1,85 @@
1
+ import { VertuError } from '../core/errors.js';
2
+ /**
3
+ * Parse a CLI-supplied JSON string and, on failure, surface a PowerShell-aware
4
+ * diagnostic that tells the user exactly which escape hatch to use.
5
+ *
6
+ * Why this exists:
7
+ * `--params '{"k":"v"}'` looks fine in bash, but Windows PowerShell 5.1
8
+ * strips the inner double quotes when forwarding to a native command, so
9
+ * Node receives `{k:v}` and `JSON.parse` throws
10
+ * `Expected property name or '}' in JSON at position 1`. Without a hint,
11
+ * users (and AI agents) read the message literally and retry the same shape.
12
+ *
13
+ * Heuristic: if the raw input starts with `{`/`[` *and* contains **no** `"`
14
+ * characters anywhere, we are nearly certain the shell stripped quotes. In
15
+ * that case we attach a strong hint that names `PowerShell 5.1` explicitly
16
+ * (so the keyword shows up in searches) and lists three battle-tested
17
+ * workarounds: a `--<flag>-file` companion, the inline `@<path>` shorthand,
18
+ * and `@-` stdin piping. Outside the heuristic we still emit a weak hint
19
+ * pointing at the same escape hatches, because malformed JSON has the same
20
+ * fixes regardless of which shell mangled it.
21
+ *
22
+ * We deliberately do **not** attempt to repair stripped-quote input:
23
+ * `{k:1}` could mean `{"k":1}` or `{"k":"1"}` and auto-guessing trains the
24
+ * wrong habit. An explicit error with concrete fixes is the right teach.
25
+ */
26
+ export function parseJsonArg(raw, label, opts = {}) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch (err) {
32
+ throw buildJsonParseError(raw, label, opts, err.message);
33
+ }
34
+ if (opts.requireObject) {
35
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
36
+ throw new VertuError(`${label} must decode to a JSON object (got ${describeValue(parsed)}).`);
37
+ }
38
+ }
39
+ return parsed;
40
+ }
41
+ function buildJsonParseError(raw, label, opts, jsonErrorMessage) {
42
+ const fileFlag = opts.fileFlagSuggestion ?? `${label}-file`;
43
+ const looksMangledByShell = detectStrippedQuotes(raw);
44
+ const exampleSkillKey = label === '--params' ? 'sales' : '<command>';
45
+ const inlineCmd = label === '--params'
46
+ ? `vps-work ${exampleSkillKey} +dept-breakdown ${label} @./payload.json`
47
+ : `vps-work … ${label} @./payload.json`;
48
+ const fileCmd = label === '--params'
49
+ ? `vps-work ${exampleSkillKey} +dept-breakdown ${fileFlag} ./payload.json`
50
+ : `vps-work … ${fileFlag} ./payload.json`;
51
+ const stdinCmd = label === '--params'
52
+ ? `Get-Content ./payload.json | vps-work ${exampleSkillKey} +dept-breakdown ${label} @-`
53
+ : `Get-Content ./payload.json | vps-work … ${label} @-`;
54
+ const lines = [];
55
+ lines.push(`Invalid ${label} JSON: ${jsonErrorMessage}`);
56
+ if (looksMangledByShell) {
57
+ lines.push('');
58
+ lines.push('Your shell likely stripped the inner double quotes (common on Windows PowerShell 5.1).');
59
+ }
60
+ lines.push('');
61
+ lines.push('Use any of the quoting-safe forms:');
62
+ lines.push(` ${fileCmd}`);
63
+ lines.push(` ${inlineCmd}`);
64
+ lines.push(` ${stdinCmd}`);
65
+ const hint = looksMangledByShell
66
+ ? `PowerShell strips quotes from native-command args; pass payloads via ${fileFlag} or @file instead.`
67
+ : `Pass payloads via ${fileFlag} or @file to bypass shell quoting.`;
68
+ return new VertuError(lines.join('\n'), { hint });
69
+ }
70
+ function detectStrippedQuotes(raw) {
71
+ const trimmed = raw.trim();
72
+ if (trimmed.length === 0)
73
+ return false;
74
+ const first = trimmed[0];
75
+ if (first !== '{' && first !== '[')
76
+ return false;
77
+ return !trimmed.includes('"');
78
+ }
79
+ function describeValue(v) {
80
+ if (v === null)
81
+ return 'null';
82
+ if (Array.isArray(v))
83
+ return 'array';
84
+ return typeof v;
85
+ }
@@ -0,0 +1,46 @@
1
+ import { checkbox, confirm, input, password } from '@inquirer/prompts';
2
+ export function isInteractive() {
3
+ return process.stdout.isTTY === true && process.stdin.isTTY === true;
4
+ }
5
+ export async function promptText(message, defaultValue) {
6
+ if (!isInteractive()) {
7
+ if (defaultValue !== undefined)
8
+ return defaultValue;
9
+ throw new Error(`Missing required value (${message}); stdin is not a TTY.`);
10
+ }
11
+ return input({ message, default: defaultValue });
12
+ }
13
+ export async function promptSecret(message) {
14
+ if (!isInteractive()) {
15
+ throw new Error(`Missing required secret (${message}); stdin is not a TTY.`);
16
+ }
17
+ return password({ message, mask: '*' });
18
+ }
19
+ export async function promptConfirm(message, defaultValue = true) {
20
+ if (!isInteractive())
21
+ return defaultValue;
22
+ return confirm({ message, default: defaultValue });
23
+ }
24
+ export async function promptCheckbox(message, choices, opts = {}) {
25
+ if (!isInteractive()) {
26
+ // 非交互场景:返回所有 checked === true 的选项作为兜底,调用方应自己保证有合理 default。
27
+ return choices.filter((c) => c.checked === true).map((c) => c.value);
28
+ }
29
+ return checkbox({
30
+ message,
31
+ choices,
32
+ pageSize: opts.pageSize ?? Math.min(20, Math.max(5, choices.length)),
33
+ required: opts.required ?? false,
34
+ loop: opts.loop ?? false,
35
+ });
36
+ }
37
+ export function normalizeBaseUrl(value) {
38
+ let trimmed = value.trim();
39
+ if (!trimmed)
40
+ throw new Error('base URL cannot be empty');
41
+ trimmed = trimmed.replace(/\/+$/, '');
42
+ if (!/^https?:\/\//i.test(trimmed)) {
43
+ throw new Error(`base URL must start with http(s)://: ${value}`);
44
+ }
45
+ return trimmed;
46
+ }
@@ -0,0 +1,44 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const UTF8_BOM = '\uFEFF';
3
+ /**
4
+ * Read a UTF-8 text file and strip a leading BOM if present.
5
+ *
6
+ * Why: Windows PowerShell's `Out-File`, `Set-Content`, and `>` redirection
7
+ * write UTF-8 with a BOM by default. `JSON.parse` then fails with
8
+ * `Unexpected token '\ufeff'`, forcing every PowerShell caller to remember the
9
+ * `[System.IO.File]::WriteAllBytes(..., [Text.Encoding]::UTF8.GetBytes(...))`
10
+ * incantation. Stripping the BOM at every file-input boundary removes that
11
+ * footgun without affecting bash/zsh callers (they don't write a BOM) or
12
+ * compliant payloads (a leading BOM is invalid in RFC 8259 JSON anyway).
13
+ *
14
+ * Use this helper instead of `fs.readFile(path, 'utf8')` for any file whose
15
+ * contents may originate from a user, agent, or external IDE.
16
+ */
17
+ export async function readUtf8File(path) {
18
+ const text = await readFile(path, 'utf8');
19
+ return stripBom(text);
20
+ }
21
+ export function stripBom(text) {
22
+ return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
23
+ }
24
+ /**
25
+ * Read process stdin to end as UTF-8 text (BOM stripped).
26
+ *
27
+ * Used by `@-` and `--<flag>-file -` to pipe payloads from upstream commands
28
+ * without persisting to a temp file — the cleanest way to bypass shell quoting
29
+ * (especially Windows PowerShell 5.1's native-command argument mangling).
30
+ *
31
+ * If the stream is a TTY we surface a clear error rather than blocking forever;
32
+ * callers should only request stdin when an upstream `|` is in play.
33
+ */
34
+ export async function readStdinText() {
35
+ const stdin = process.stdin;
36
+ if (stdin.isTTY) {
37
+ throw new Error('refused to read stdin: no data piped (stdin is a TTY)');
38
+ }
39
+ const chunks = [];
40
+ for await (const chunk of stdin) {
41
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : Buffer.from(chunk));
42
+ }
43
+ return stripBom(Buffer.concat(chunks).toString('utf8'));
44
+ }
@@ -0,0 +1,307 @@
1
+ import { parseJsonArg } from './parse-json-arg.js';
2
+ const PRIMITIVE_KINDS = new Set(['string', 'integer', 'number', 'boolean']);
3
+ function pickType(prop) {
4
+ const t = prop.type;
5
+ if (Array.isArray(t)) {
6
+ const firstStr = t.find((x) => x !== 'null');
7
+ return typeof firstStr === 'string' ? firstStr : 'json';
8
+ }
9
+ if (typeof t === 'string')
10
+ return t;
11
+ return 'json';
12
+ }
13
+ function toFlagName(key) {
14
+ return key.replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '');
15
+ }
16
+ function buildDescription(prop, kind, itemKind) {
17
+ const parts = [];
18
+ if (prop.description)
19
+ parts.push(prop.description);
20
+ if (prop.enum && prop.enum.length)
21
+ parts.push(`enum: ${JSON.stringify(prop.enum)}`);
22
+ if (kind === 'array') {
23
+ parts.push(itemKind && itemKind !== 'json'
24
+ ? '(JSON array, e.g. \'["a","b"]\'; or comma-separated "a,b"; or repeat the flag)'
25
+ : '(JSON array; pass a JSON-encoded value)');
26
+ }
27
+ else if (kind === 'object' || kind === 'json') {
28
+ parts.push('(JSON; pass a JSON-encoded value)');
29
+ }
30
+ if (prop.default !== undefined)
31
+ parts.push(`default: ${JSON.stringify(prop.default)}`);
32
+ return parts.join(' ').trim();
33
+ }
34
+ export function deriveOptions(schema) {
35
+ if (!schema || typeof schema !== 'object')
36
+ return [];
37
+ const props = schema.properties ?? {};
38
+ const required = new Set(schema.required ?? []);
39
+ const out = [];
40
+ for (const [key, raw] of Object.entries(props)) {
41
+ const prop = raw ?? {};
42
+ const type = pickType(prop);
43
+ let kind = 'string';
44
+ let itemKind;
45
+ let placeholder = '<value>';
46
+ if (PRIMITIVE_KINDS.has(type)) {
47
+ kind = type;
48
+ placeholder =
49
+ kind === 'integer'
50
+ ? '<int>'
51
+ : kind === 'number'
52
+ ? '<num>'
53
+ : kind === 'boolean'
54
+ ? ''
55
+ : '<string>';
56
+ }
57
+ else if (type === 'array') {
58
+ kind = 'array';
59
+ const itemType = prop.items ? pickType(prop.items) : 'json';
60
+ itemKind = PRIMITIVE_KINDS.has(itemType) ? itemType : 'json';
61
+ // Show a friendlier placeholder when items are primitives — users can then pass either
62
+ // JSON, a comma-separated list, or repeat the flag.
63
+ placeholder = itemKind === 'json' ? '<json>' : '<list>';
64
+ }
65
+ else if (type === 'object') {
66
+ kind = 'object';
67
+ placeholder = '<json>';
68
+ }
69
+ else {
70
+ kind = 'json';
71
+ placeholder = '<json>';
72
+ }
73
+ const flagName = toFlagName(key);
74
+ if (!flagName)
75
+ continue;
76
+ const flag = kind === 'boolean'
77
+ ? `--${flagName}`
78
+ : required.has(key)
79
+ ? `--${flagName} ${placeholder}`
80
+ : `--${flagName} [${placeholder.replace(/[<>]/g, '')}]`;
81
+ out.push({
82
+ flag,
83
+ description: buildDescription(prop, kind, itemKind),
84
+ required: required.has(key),
85
+ key,
86
+ kind,
87
+ itemKind,
88
+ defaultValue: prop.default,
89
+ enumValues: prop.enum,
90
+ });
91
+ }
92
+ return out;
93
+ }
94
+ export function parseOptionValues(options, raw) {
95
+ const values = {};
96
+ const errors = [];
97
+ for (const opt of options) {
98
+ const flagKey = opt.flag.split(' ')[0]?.replace(/^--/, '') ?? opt.key;
99
+ const camel = flagKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
100
+ const v = raw[camel];
101
+ if (v === undefined)
102
+ continue;
103
+ try {
104
+ values[opt.key] = coerceValue(opt, v);
105
+ }
106
+ catch (err) {
107
+ errors.push({ key: opt.key, message: err.message });
108
+ }
109
+ }
110
+ return { values, errors };
111
+ }
112
+ function coerceValue(opt, value) {
113
+ switch (opt.kind) {
114
+ case 'string':
115
+ return String(value);
116
+ case 'integer': {
117
+ const n = Number(value);
118
+ if (!Number.isFinite(n) || !Number.isInteger(n))
119
+ throw new Error(`Expected integer for --${opt.key}, got ${String(value)}`);
120
+ return n;
121
+ }
122
+ case 'number': {
123
+ const n = Number(value);
124
+ if (!Number.isFinite(n))
125
+ throw new Error(`Expected number for --${opt.key}, got ${String(value)}`);
126
+ return n;
127
+ }
128
+ case 'boolean':
129
+ if (typeof value === 'boolean')
130
+ return value;
131
+ if (value === 'true' || value === 1 || value === '1')
132
+ return true;
133
+ if (value === 'false' || value === 0 || value === '0')
134
+ return false;
135
+ throw new Error(`Expected boolean for --${opt.key}, got ${String(value)}`);
136
+ case 'array':
137
+ return coerceArray(opt, value);
138
+ case 'object':
139
+ case 'json':
140
+ if (typeof value !== 'string')
141
+ return value;
142
+ return parseJsonForSchemaField(toFlagName(opt.key), value);
143
+ }
144
+ }
145
+ /**
146
+ * Bridge `parseJsonArg` (throws VertuError carrying multi-line PowerShell hints)
147
+ * into the plain-`Error` contract that `parseOptionValues` expects when it
148
+ * aggregates per-field failures. We carry the full diagnostic message — the
149
+ * helpful 3-fix block stays visible to the user — but drop the `hint` channel
150
+ * because the upstream aggregator only inspects `.message`.
151
+ */
152
+ function parseJsonForSchemaField(flagName, value) {
153
+ try {
154
+ return parseJsonArg(value, `--${flagName}`, {
155
+ fileFlagSuggestion: `--${flagName}-file`,
156
+ });
157
+ }
158
+ catch (err) {
159
+ throw new Error(err.message);
160
+ }
161
+ }
162
+ /**
163
+ * Array coercion accepts three shell-friendly shapes:
164
+ * 1. JSON literal (preferred): --fields '["id","name"]'
165
+ * 2. Comma-separated shorthand: --fields id,name (only when items are primitives)
166
+ * 3. Repeated flag (variadic): --fields id --fields name
167
+ *
168
+ * `value` may already be an `unknown[]` when commander's variadic collector accumulated
169
+ * multiple --flag invocations; in that case each element is treated as a single token
170
+ * (or a comma-separated group when the items are primitives), so 2 and 3 compose.
171
+ *
172
+ * For arrays of objects / nested arrays (`itemKind === 'json'`) we keep strict JSON to
173
+ * avoid silently mangling structured payloads (e.g. an Odoo domain).
174
+ */
175
+ function coerceArray(opt, value) {
176
+ const itemKind = opt.itemKind ?? 'json';
177
+ if (Array.isArray(value)) {
178
+ // Single variadic call: defer to string handling so JSON / shorthand still works.
179
+ if (value.length === 1 && typeof value[0] === 'string') {
180
+ return parseArrayString(opt.key, itemKind, value[0]);
181
+ }
182
+ return value.flatMap((v) => splitArrayToken(itemKind, v));
183
+ }
184
+ if (typeof value !== 'string')
185
+ return value;
186
+ return parseArrayString(opt.key, itemKind, value);
187
+ }
188
+ /**
189
+ * Split a single repeated-flag token into typed items.
190
+ *
191
+ * Both commas and newlines act as separators for primitive item arrays — that
192
+ * way `--fields @list.txt` works whether the file lists names by row, by comma,
193
+ * or mixes both.
194
+ */
195
+ function splitArrayToken(itemKind, token) {
196
+ if (typeof token !== 'string')
197
+ return [token];
198
+ if (itemKind === 'json') {
199
+ // Each variadic token is expected to be a standalone JSON value when items aren't primitives.
200
+ // TODO: wrap with parseJsonArg for consistent PowerShell-aware diagnostics; this path
201
+ // currently leaks a raw `SyntaxError` on bad input (narrow blast radius — only triggers
202
+ // when repeating a json-item flag like `--filters '{...}' --filters '{...}'`).
203
+ return [JSON.parse(token)];
204
+ }
205
+ return token
206
+ .split(/[\n,]+/u)
207
+ .map((s) => s.trim())
208
+ .filter((s) => s.length > 0)
209
+ .map((s) => coerceArrayItem(itemKind, s));
210
+ }
211
+ function parseArrayString(key, itemKind, raw) {
212
+ const trimmed = raw.trim();
213
+ if (trimmed.length === 0)
214
+ return [];
215
+ const flagName = toFlagName(key);
216
+ const looksJson = trimmed.startsWith('[') || trimmed.startsWith('{');
217
+ if (looksJson) {
218
+ let parsed;
219
+ let parseFailed = false;
220
+ try {
221
+ parsed = JSON.parse(trimmed);
222
+ }
223
+ catch {
224
+ parseFailed = true;
225
+ }
226
+ if (!parseFailed) {
227
+ if (!Array.isArray(parsed)) {
228
+ throw new Error(`Expected JSON array for --${flagName} (got ${describeJsonValue(parsed)}).`);
229
+ }
230
+ return itemKind === 'json'
231
+ ? parsed
232
+ : parsed.map((v) => coerceArrayItem(itemKind, v));
233
+ }
234
+ // PowerShell strips inner double-quotes from `["id","name"]`, leaving `[id,name]`.
235
+ // For primitive item arrays we recover by stripping the outer brackets and splitting.
236
+ if (itemKind !== 'json') {
237
+ const inner = trimmed.replace(/^\[/, '').replace(/\]$/, '');
238
+ return inner
239
+ .split(/[\n,]+/u)
240
+ .map((s) => s.trim())
241
+ .filter((s) => s.length > 0)
242
+ .map((s) => coerceArrayItem(itemKind, s));
243
+ }
244
+ // No primitive recovery available for json-item arrays (e.g. Odoo domain
245
+ // `[["a","=",1]]`): surface the PowerShell-aware diagnostic so users learn
246
+ // the @file / --<flag>-file escape hatch instead of guessing at quoting.
247
+ return parseJsonForSchemaField(flagName, trimmed);
248
+ }
249
+ if (itemKind !== 'json') {
250
+ // Comma- *or* newline-separated list — convenient when the value comes from
251
+ // `--fields @list.txt`, where users write one name per row.
252
+ return trimmed
253
+ .split(/[\n,]+/u)
254
+ .map((s) => s.trim())
255
+ .filter((s) => s.length > 0)
256
+ .map((s) => coerceArrayItem(itemKind, s));
257
+ }
258
+ // Last resort for arrays of arbitrary structure: still require valid JSON.
259
+ // Route through parseJsonForSchemaField so the failure message stays consistent
260
+ // with the leading-`[` path (same `--<flag>-file` / `@file` / `@-` hints).
261
+ const result = parseJsonForSchemaField(flagName, trimmed);
262
+ if (!Array.isArray(result)) {
263
+ throw new Error(`Expected JSON array for --${flagName} (got ${describeJsonValue(result)}).`);
264
+ }
265
+ return result;
266
+ }
267
+ function describeJsonValue(v) {
268
+ if (v === null)
269
+ return 'null';
270
+ if (Array.isArray(v))
271
+ return 'array';
272
+ return typeof v;
273
+ }
274
+ function coerceArrayItem(itemKind, value) {
275
+ if (typeof value !== 'string')
276
+ return value;
277
+ switch (itemKind) {
278
+ case 'string':
279
+ return value;
280
+ case 'integer': {
281
+ const n = Number(value);
282
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
283
+ throw new Error(`expected integer item, got ${value}`);
284
+ }
285
+ return n;
286
+ }
287
+ case 'number': {
288
+ const n = Number(value);
289
+ if (!Number.isFinite(n))
290
+ throw new Error(`expected number item, got ${value}`);
291
+ return n;
292
+ }
293
+ case 'boolean':
294
+ if (value === 'true' || value === '1')
295
+ return true;
296
+ if (value === 'false' || value === '0')
297
+ return false;
298
+ throw new Error(`expected boolean item, got ${value}`);
299
+ case 'json':
300
+ try {
301
+ return JSON.parse(value);
302
+ }
303
+ catch {
304
+ return value;
305
+ }
306
+ }
307
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 服务端 ai_cli `/ai/cli/v1/execute` 与 ai_skills `/ai/skills/v1/execute`
3
+ * 在成功路径上恒返回 `{ "ok": true, "result": <真正数据> }`,失败时通过
4
+ * JSON-RPC `error` 通道抛出(不会以 `{ ok: false, ... }` 形式回到这里)。
5
+ *
6
+ * 该 envelope 因此不携带任何分支信息:`ok` 永远是 `true`、`result` 才是用户
7
+ * 真正想看的内容。再叠加 Odoo JSON-RPC 自身的一层 `result`,导致 CLI 用户每
8
+ * 次都在 stdout 看到双层包裹的 JSON,需要肉眼/jq 拆包。
9
+ *
10
+ * 这个工具按"严格匹配"剥掉这一层:
11
+ * - 只在对象正好是 `{ ok: true, result: ... }`(且**只有**这两个键)时拆开;
12
+ * - 任何其它形态(裸值、数组、含额外字段如 `warnings`、`ok=false`、缺少
13
+ * `result` 等)一律按原样返回,避免把未来可能加上的 envelope 元数据吃掉。
14
+ *
15
+ * 这样如果服务端将来去掉 envelope(直接返回业务对象),或者为了承载告警/部分
16
+ * 成功新增字段,本工具都会自然降级为原值穿透。
17
+ */
18
+ export function unwrapExecuteEnvelope(value) {
19
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
20
+ return value;
21
+ }
22
+ const obj = value;
23
+ const keys = Object.keys(obj);
24
+ if (keys.length !== 2)
25
+ return value;
26
+ if (!keys.includes('ok') || !keys.includes('result'))
27
+ return value;
28
+ if (obj.ok !== true)
29
+ return value;
30
+ return obj.result;
31
+ }