@alphafox/cli 0.1.5 β†’ 0.3.2

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 (104) hide show
  1. package/README.md +42 -4
  2. package/dist/auth/browser-login.d.ts +30 -0
  3. package/dist/auth/browser-login.js +193 -0
  4. package/dist/auth/loopback-callback.d.ts +32 -0
  5. package/dist/auth/loopback-callback.js +175 -0
  6. package/dist/auth/open-browser.d.ts +15 -0
  7. package/dist/auth/open-browser.js +54 -0
  8. package/dist/auth/refresh.d.ts +27 -2
  9. package/dist/auth/refresh.js +52 -15
  10. package/dist/catalog/allowlist.d.ts +20 -4
  11. package/dist/catalog/allowlist.js +126 -25
  12. package/dist/catalog/command-tree.d.ts +35 -0
  13. package/dist/catalog/command-tree.js +119 -0
  14. package/dist/catalog/compatibility.d.ts +23 -0
  15. package/dist/catalog/compatibility.js +58 -0
  16. package/dist/catalog/generated/registry.json +6137 -0
  17. package/dist/catalog/generated/schemas.json +31036 -0
  18. package/dist/catalog/operations.d.ts +76 -3
  19. package/dist/catalog/operations.js +87 -213
  20. package/dist/catalog/validate-body.d.ts +22 -0
  21. package/dist/catalog/validate-body.js +98 -0
  22. package/dist/commands/request-body.d.ts +21 -0
  23. package/dist/commands/request-body.js +92 -0
  24. package/dist/commands/run.js +283 -162
  25. package/dist/config/profiles.js +3 -3
  26. package/dist/engine-backtest/errors.d.ts +18 -0
  27. package/dist/engine-backtest/errors.js +26 -0
  28. package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
  29. package/dist/engine-backtest/fetch-runtime.js +170 -0
  30. package/dist/engine-backtest/native-import.d.ts +1 -0
  31. package/dist/engine-backtest/native-import.js +10 -0
  32. package/dist/engine-backtest/parse-args.d.ts +6 -0
  33. package/dist/engine-backtest/parse-args.js +281 -0
  34. package/dist/engine-backtest/persist.d.ts +32 -0
  35. package/dist/engine-backtest/persist.js +94 -0
  36. package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
  37. package/dist/engine-backtest/replay-timeframe.js +48 -0
  38. package/dist/engine-backtest/resolve-packages.d.ts +27 -0
  39. package/dist/engine-backtest/resolve-packages.js +288 -0
  40. package/dist/engine-backtest/run-command.d.ts +38 -0
  41. package/dist/engine-backtest/run-command.js +540 -0
  42. package/dist/engine-backtest/types.d.ts +249 -0
  43. package/dist/engine-backtest/types.js +2 -0
  44. package/dist/envelope.d.ts +3 -0
  45. package/dist/envelope.js +43 -3
  46. package/dist/http/client.js +18 -15
  47. package/dist/index.d.ts +16 -5
  48. package/dist/index.js +47 -1
  49. package/dist/install/exec.d.ts +13 -0
  50. package/dist/install/exec.js +73 -0
  51. package/dist/install/package-root.d.ts +4 -0
  52. package/dist/install/package-root.js +59 -0
  53. package/dist/install/types.d.ts +69 -0
  54. package/dist/install/types.js +26 -0
  55. package/dist/install/wizard.d.ts +21 -0
  56. package/dist/install/wizard.js +332 -0
  57. package/dist/keychain/linux-secret-service.d.ts +11 -0
  58. package/dist/keychain/linux-secret-service.js +93 -0
  59. package/dist/keychain/store.d.ts +20 -1
  60. package/dist/keychain/store.js +94 -6
  61. package/dist/keychain/windows-credential.d.ts +13 -0
  62. package/dist/keychain/windows-credential.js +176 -0
  63. package/dist/safety/confirmation.d.ts +10 -3
  64. package/dist/safety/confirmation.js +27 -4
  65. package/dist/version.d.ts +2 -2
  66. package/dist/version.js +3 -2
  67. package/docs/.nojekyll +0 -0
  68. package/docs/agents/domain.md +51 -0
  69. package/docs/agents/issue-tracker.md +156 -0
  70. package/docs/agents/triage-labels.md +18 -0
  71. package/docs/alphafox-cli-installation-guide.md +105 -0
  72. package/docs/e2e-staging.md +76 -6
  73. package/docs/favicon.svg +4 -0
  74. package/docs/index.html +748 -0
  75. package/docs/logo/alphafox-mark.svg +4 -0
  76. package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
  77. package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
  78. package/docs/release-supply-chain.md +107 -26
  79. package/package.json +10 -3
  80. package/skills/account/SKILL.md +8 -6
  81. package/skills/admin/SKILL.md +9 -4
  82. package/skills/alphafox-shared/SKILL.md +44 -14
  83. package/skills/auth/SKILL.md +21 -6
  84. package/skills/engine-backtest/SKILL.md +71 -0
  85. package/skills/exchange/SKILL.md +10 -3
  86. package/skills/market/SKILL.md +9 -4
  87. package/skills/notification/SKILL.md +8 -3
  88. package/skills/strategy/SKILL.md +23 -9
  89. package/skills/trading/SKILL.md +18 -5
  90. package/vendor/backtest-runner/NOTICE.md +1 -0
  91. package/vendor/backtest-runner/README.md +97 -0
  92. package/vendor/backtest-runner/index.d.ts +423 -0
  93. package/vendor/backtest-runner/index.mjs +59 -0
  94. package/vendor/backtest-runner/lib/abortable.mjs +23 -0
  95. package/vendor/backtest-runner/lib/cache.mjs +159 -0
  96. package/vendor/backtest-runner/lib/coverage.mjs +238 -0
  97. package/vendor/backtest-runner/lib/encode.mjs +28 -0
  98. package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
  99. package/vendor/backtest-runner/lib/proxy.mjs +78 -0
  100. package/vendor/backtest-runner/lib/scenario.mjs +66 -0
  101. package/vendor/backtest-runner/lib/series.mjs +370 -0
  102. package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
  103. package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
  104. package/vendor/backtest-runner/package.json +13 -0
@@ -5,6 +5,11 @@
5
5
  * (local unit tests only; not a production automation path β€” ADR 0004).
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.getLastTokenSaveResult = getLastTokenSaveResult;
9
+ exports.keychainServiceName = keychainServiceName;
10
+ exports.keychainAccountName = keychainAccountName;
11
+ exports.keychainPlatform = keychainPlatform;
12
+ exports.probeOsKeychain = probeOsKeychain;
8
13
  exports.saveTokens = saveTokens;
9
14
  exports.loadTokens = loadTokens;
10
15
  exports.deleteTokens = deleteTokens;
@@ -14,12 +19,52 @@ const node_fs_1 = require("node:fs");
14
19
  const node_os_1 = require("node:os");
15
20
  const node_path_1 = require("node:path");
16
21
  const node_child_process_1 = require("node:child_process");
17
- function serviceName(profile) {
22
+ const linux_secret_service_1 = require("./linux-secret-service");
23
+ const windows_credential_1 = require("./windows-credential");
24
+ /** Last save outcome β€” callers/tests can observe silent-fallback without stderr parsing. */
25
+ let lastSaveResult = null;
26
+ function getLastTokenSaveResult() {
27
+ return lastSaveResult;
28
+ }
29
+ function keychainServiceName(profile) {
18
30
  return `alphafox-cli.${profile}`;
19
31
  }
20
- function accountName() {
32
+ function keychainAccountName() {
21
33
  return "oauth-tokens";
22
34
  }
35
+ function serviceName(profile) {
36
+ return keychainServiceName(profile);
37
+ }
38
+ function accountName() {
39
+ return keychainAccountName();
40
+ }
41
+ /** Test-only override. Production code uses process.platform. */
42
+ function keychainPlatform(env = process.env) {
43
+ const raw = env.ALPHAFOX_KEYCHAIN_PLATFORM?.trim();
44
+ if (raw === "darwin" || raw === "linux" || raw === "win32") {
45
+ return raw;
46
+ }
47
+ return process.platform;
48
+ }
49
+ function probeOsKeychain(env = process.env) {
50
+ const platform = keychainPlatform(env);
51
+ if (platform === "darwin") {
52
+ return { kind: "macos-security", available: true };
53
+ }
54
+ if (platform === "linux") {
55
+ return {
56
+ kind: "linux-secret-service",
57
+ available: (0, linux_secret_service_1.linuxSecretServiceAvailable)(env),
58
+ };
59
+ }
60
+ if (platform === "win32") {
61
+ return {
62
+ kind: "windows-credential-manager",
63
+ available: (0, windows_credential_1.windowsCredentialAvailable)(env),
64
+ };
65
+ }
66
+ return { kind: "none", available: false };
67
+ }
23
68
  /** File fallback under secure mode 0600 when OS keychain is unavailable (CI/Linux headless). */
24
69
  function fileFallbackPath(profile, env) {
25
70
  const base = env.ALPHAFOX_KEYCHAIN_DIR?.trim() ||
@@ -29,11 +74,31 @@ function fileFallbackPath(profile, env) {
29
74
  function saveTokens(profile, tokens, env = process.env) {
30
75
  const payload = JSON.stringify(tokens);
31
76
  if (tryKeychainWrite(profile, payload, env)) {
32
- return;
77
+ lastSaveResult = {
78
+ backend: "keychain",
79
+ kind: probeOsKeychain(env).kind,
80
+ degraded: false,
81
+ };
82
+ return lastSaveResult;
33
83
  }
34
84
  const path = fileFallbackPath(profile, env);
35
85
  (0, node_fs_1.mkdirSync)((0, node_path_1.join)(path, ".."), { recursive: true });
36
86
  (0, node_fs_1.writeFileSync)(path, payload, { mode: 0o600 });
87
+ const intentionalFile = env.ALPHAFOX_FORCE_FILE_KEYCHAIN === "1";
88
+ lastSaveResult = {
89
+ backend: "file",
90
+ path,
91
+ // Intentional file mode is not a silent degrade.
92
+ degraded: !intentionalFile,
93
+ };
94
+ // Observable signal when OS keychain failed unexpectedly (not force-file).
95
+ if (!intentionalFile) {
96
+ process.emitWarning(`OS keychain unavailable for profile "${profile}"; tokens stored in file ${path} (mode 0600). Set ALPHAFOX_FORCE_FILE_KEYCHAIN=1 when file storage is intentional.`, {
97
+ code: "ALPHAFOX_KEYCHAIN_FILE_FALLBACK",
98
+ detail: path,
99
+ });
100
+ }
101
+ return lastSaveResult;
37
102
  }
38
103
  function loadTokens(profile, env = process.env) {
39
104
  // Controlled test injection β€” never document as prod automation.
@@ -77,7 +142,14 @@ function tryKeychainWrite(profile, payload, env) {
77
142
  if (env.ALPHAFOX_FORCE_FILE_KEYCHAIN === "1") {
78
143
  return false;
79
144
  }
80
- if (process.platform === "darwin") {
145
+ const platform = keychainPlatform(env);
146
+ if (platform === "linux") {
147
+ return (0, linux_secret_service_1.linuxSecretServiceWrite)(serviceName(profile), accountName(), payload, env);
148
+ }
149
+ if (platform === "win32") {
150
+ return (0, windows_credential_1.windowsCredentialWrite)(profile, payload, env);
151
+ }
152
+ if (platform === "darwin") {
81
153
  try {
82
154
  // delete existing silently
83
155
  try {
@@ -114,7 +186,14 @@ function tryKeychainRead(profile, env) {
114
186
  if (env.ALPHAFOX_FORCE_FILE_KEYCHAIN === "1") {
115
187
  return null;
116
188
  }
117
- if (process.platform === "darwin") {
189
+ const platform = keychainPlatform(env);
190
+ if (platform === "linux") {
191
+ return (0, linux_secret_service_1.linuxSecretServiceRead)(serviceName(profile), accountName(), env);
192
+ }
193
+ if (platform === "win32") {
194
+ return (0, windows_credential_1.windowsCredentialRead)(profile, env);
195
+ }
196
+ if (platform === "darwin") {
118
197
  try {
119
198
  const out = (0, node_child_process_1.execFileSync)("security", [
120
199
  "find-generic-password",
@@ -136,7 +215,16 @@ function tryKeychainDelete(profile, env) {
136
215
  if (env.ALPHAFOX_FORCE_FILE_KEYCHAIN === "1") {
137
216
  return;
138
217
  }
139
- if (process.platform === "darwin") {
218
+ const platform = keychainPlatform(env);
219
+ if (platform === "linux") {
220
+ (0, linux_secret_service_1.linuxSecretServiceDelete)(serviceName(profile), accountName(), env);
221
+ return;
222
+ }
223
+ if (platform === "win32") {
224
+ (0, windows_credential_1.windowsCredentialDelete)(profile, env);
225
+ return;
226
+ }
227
+ if (platform === "darwin") {
140
228
  try {
141
229
  (0, node_child_process_1.execFileSync)("security", [
142
230
  "delete-generic-password",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Windows Credential Manager via advapi32 CredWrite/CredRead/CredDelete.
3
+ * Payload is passed on stdin to PowerShell β€” never as argv.
4
+ */
5
+ /** CRED_MAX_CREDENTIAL_BLOB_SIZE is 5*512 = 2560. */
6
+ export declare const WINDOWS_CRED_MAX_BYTES = 2560;
7
+ export declare const WINDOWS_CRED_PS1 = "# Alphafox CLI \u2014 Windows Credential Manager helper (t101360)\nparam(\n [Parameter(Mandatory = $true)][ValidateSet('write','read','delete')][string]$Action,\n [Parameter(Mandatory = $true)][string]$Target\n)\n$ErrorActionPreference = 'Stop'\nAdd-Type -TypeDefinition @\"\nusing System;\nusing System.Runtime.InteropServices;\nnamespace AlphafoxCred {\n [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n public struct CREDENTIAL {\n public uint Flags;\n public uint Type;\n public string TargetName;\n public string Comment;\n public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;\n public uint CredentialBlobSize;\n public IntPtr CredentialBlob;\n public uint Persist;\n public uint AttributeCount;\n public IntPtr Attributes;\n public string TargetAlias;\n public string UserName;\n }\n public static class Native {\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n public static extern bool CredWrite(ref CREDENTIAL credential, uint flags);\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credentialPtr);\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)]\n public static extern bool CredDelete(string target, uint type, uint flags);\n [DllImport(\"advapi32.dll\", SetLastError = true)]\n public static extern void CredFree(IntPtr credential);\n }\n}\n\"@\n$CredTypeGeneric = 1\n$PersistLocalMachine = 2\nswitch ($Action) {\n 'write' {\n $payload = [Console]::In.ReadToEnd()\n $bytes = [Text.Encoding]::UTF8.GetBytes($payload)\n if ($bytes.Length -gt 2560) { throw \"credential blob too large\" }\n $blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)\n try {\n [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)\n $cred = New-Object AlphafoxCred.CREDENTIAL\n $cred.Type = $CredTypeGeneric\n $cred.TargetName = $Target\n $cred.UserName = \"alphafox-cli\"\n $cred.CredentialBlobSize = [uint32]$bytes.Length\n $cred.CredentialBlob = $blob\n $cred.Persist = $PersistLocalMachine\n $ok = [AlphafoxCred.Native]::CredWrite([ref]$cred, 0)\n if (-not $ok) {\n throw \"CredWrite failed Win32=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())\"\n }\n } finally {\n [Runtime.InteropServices.Marshal]::FreeHGlobal($blob)\n }\n }\n 'read' {\n $ptr = [IntPtr]::Zero\n $ok = [AlphafoxCred.Native]::CredRead($Target, $CredTypeGeneric, 0, [ref]$ptr)\n if (-not $ok) { exit 2 }\n try {\n $cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][AlphafoxCred.CREDENTIAL])\n $size = [int]$cred.CredentialBlobSize\n $bytes = New-Object byte[] $size\n [Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $size)\n [Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))\n } finally {\n [AlphafoxCred.Native]::CredFree($ptr)\n }\n }\n 'delete' {\n [void][AlphafoxCred.Native]::CredDelete($Target, $CredTypeGeneric, 0)\n }\n}\n";
8
+ export declare function windowsCredentialTarget(profile: string): string;
9
+ export declare function windowsPowershellBin(env?: NodeJS.ProcessEnv): string;
10
+ export declare function windowsCredentialWrite(profile: string, payload: string, env?: NodeJS.ProcessEnv): boolean;
11
+ export declare function windowsCredentialRead(profile: string, env?: NodeJS.ProcessEnv): string | null;
12
+ export declare function windowsCredentialDelete(profile: string, env?: NodeJS.ProcessEnv): void;
13
+ export declare function windowsCredentialAvailable(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ /**
3
+ * Windows Credential Manager via advapi32 CredWrite/CredRead/CredDelete.
4
+ * Payload is passed on stdin to PowerShell β€” never as argv.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.WINDOWS_CRED_PS1 = exports.WINDOWS_CRED_MAX_BYTES = void 0;
8
+ exports.windowsCredentialTarget = windowsCredentialTarget;
9
+ exports.windowsPowershellBin = windowsPowershellBin;
10
+ exports.windowsCredentialWrite = windowsCredentialWrite;
11
+ exports.windowsCredentialRead = windowsCredentialRead;
12
+ exports.windowsCredentialDelete = windowsCredentialDelete;
13
+ exports.windowsCredentialAvailable = windowsCredentialAvailable;
14
+ const node_child_process_1 = require("node:child_process");
15
+ const node_fs_1 = require("node:fs");
16
+ const node_os_1 = require("node:os");
17
+ const node_path_1 = require("node:path");
18
+ /** CRED_MAX_CREDENTIAL_BLOB_SIZE is 5*512 = 2560. */
19
+ exports.WINDOWS_CRED_MAX_BYTES = 2560;
20
+ exports.WINDOWS_CRED_PS1 = `# Alphafox CLI β€” Windows Credential Manager helper (t101360)
21
+ param(
22
+ [Parameter(Mandatory = $true)][ValidateSet('write','read','delete')][string]$Action,
23
+ [Parameter(Mandatory = $true)][string]$Target
24
+ )
25
+ $ErrorActionPreference = 'Stop'
26
+ Add-Type -TypeDefinition @"
27
+ using System;
28
+ using System.Runtime.InteropServices;
29
+ namespace AlphafoxCred {
30
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
31
+ public struct CREDENTIAL {
32
+ public uint Flags;
33
+ public uint Type;
34
+ public string TargetName;
35
+ public string Comment;
36
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
37
+ public uint CredentialBlobSize;
38
+ public IntPtr CredentialBlob;
39
+ public uint Persist;
40
+ public uint AttributeCount;
41
+ public IntPtr Attributes;
42
+ public string TargetAlias;
43
+ public string UserName;
44
+ }
45
+ public static class Native {
46
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
47
+ public static extern bool CredWrite(ref CREDENTIAL credential, uint flags);
48
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
49
+ public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credentialPtr);
50
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
51
+ public static extern bool CredDelete(string target, uint type, uint flags);
52
+ [DllImport("advapi32.dll", SetLastError = true)]
53
+ public static extern void CredFree(IntPtr credential);
54
+ }
55
+ }
56
+ "@
57
+ $CredTypeGeneric = 1
58
+ $PersistLocalMachine = 2
59
+ switch ($Action) {
60
+ 'write' {
61
+ $payload = [Console]::In.ReadToEnd()
62
+ $bytes = [Text.Encoding]::UTF8.GetBytes($payload)
63
+ if ($bytes.Length -gt ${exports.WINDOWS_CRED_MAX_BYTES}) { throw "credential blob too large" }
64
+ $blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
65
+ try {
66
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
67
+ $cred = New-Object AlphafoxCred.CREDENTIAL
68
+ $cred.Type = $CredTypeGeneric
69
+ $cred.TargetName = $Target
70
+ $cred.UserName = "alphafox-cli"
71
+ $cred.CredentialBlobSize = [uint32]$bytes.Length
72
+ $cred.CredentialBlob = $blob
73
+ $cred.Persist = $PersistLocalMachine
74
+ $ok = [AlphafoxCred.Native]::CredWrite([ref]$cred, 0)
75
+ if (-not $ok) {
76
+ throw "CredWrite failed Win32=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
77
+ }
78
+ } finally {
79
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
80
+ }
81
+ }
82
+ 'read' {
83
+ $ptr = [IntPtr]::Zero
84
+ $ok = [AlphafoxCred.Native]::CredRead($Target, $CredTypeGeneric, 0, [ref]$ptr)
85
+ if (-not $ok) { exit 2 }
86
+ try {
87
+ $cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][AlphafoxCred.CREDENTIAL])
88
+ $size = [int]$cred.CredentialBlobSize
89
+ $bytes = New-Object byte[] $size
90
+ [Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $size)
91
+ [Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
92
+ } finally {
93
+ [AlphafoxCred.Native]::CredFree($ptr)
94
+ }
95
+ }
96
+ 'delete' {
97
+ [void][AlphafoxCred.Native]::CredDelete($Target, $CredTypeGeneric, 0)
98
+ }
99
+ }
100
+ `;
101
+ function windowsCredentialTarget(profile) {
102
+ return `alphafox-cli/${profile}/oauth-tokens`;
103
+ }
104
+ function windowsPowershellBin(env = process.env) {
105
+ return env.ALPHAFOX_POWERSHELL?.trim() || "powershell.exe";
106
+ }
107
+ function scriptPath() {
108
+ const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "alphafox-cli");
109
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
110
+ const path = (0, node_path_1.join)(dir, "windows-cred.ps1");
111
+ (0, node_fs_1.writeFileSync)(path, exports.WINDOWS_CRED_PS1, { encoding: "utf8" });
112
+ return path;
113
+ }
114
+ function runCred(action, target, env, input) {
115
+ const result = (0, node_child_process_1.execFileSync)(windowsPowershellBin(env), [
116
+ "-NoProfile",
117
+ "-NonInteractive",
118
+ "-ExecutionPolicy",
119
+ "Bypass",
120
+ "-File",
121
+ scriptPath(),
122
+ action,
123
+ target,
124
+ ], {
125
+ input: input ?? "",
126
+ encoding: "utf8",
127
+ stdio: ["pipe", "pipe", "pipe"],
128
+ timeout: 15_000,
129
+ windowsHide: true,
130
+ env: { ...process.env, ...env },
131
+ });
132
+ return typeof result === "string" ? result : String(result);
133
+ }
134
+ function windowsCredentialWrite(profile, payload, env = process.env) {
135
+ if (Buffer.byteLength(payload, "utf8") > exports.WINDOWS_CRED_MAX_BYTES) {
136
+ return false;
137
+ }
138
+ try {
139
+ runCred("write", windowsCredentialTarget(profile), env, payload);
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ function windowsCredentialRead(profile, env = process.env) {
147
+ try {
148
+ const out = runCred("read", windowsCredentialTarget(profile), env);
149
+ return out.length > 0 ? out : null;
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ function windowsCredentialDelete(profile, env = process.env) {
156
+ try {
157
+ runCred("delete", windowsCredentialTarget(profile), env);
158
+ }
159
+ catch {
160
+ // none
161
+ }
162
+ }
163
+ function windowsCredentialAvailable(env = process.env) {
164
+ try {
165
+ (0, node_child_process_1.execFileSync)(windowsPowershellBin(env), ["-NoProfile", "-Command", "exit 0"], {
166
+ stdio: "ignore",
167
+ timeout: 5_000,
168
+ windowsHide: true,
169
+ env: { ...process.env, ...env },
170
+ });
171
+ return true;
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
@@ -1,4 +1,4 @@
1
- export type RiskLevel = "read" | "write" | "high-risk-write";
1
+ export type RiskLevel = "read" | "write" | "high-risk-write" | "unknown";
2
2
  export interface ConfirmationGateResult {
3
3
  readonly allowed: boolean;
4
4
  readonly error?: {
@@ -6,12 +6,19 @@ export interface ConfirmationGateResult {
6
6
  readonly subtype: "confirmation_required";
7
7
  readonly message: string;
8
8
  readonly hint: string;
9
- readonly risk: "high-risk-write";
9
+ readonly risk: "high-risk-write" | "unknown";
10
10
  readonly action: string;
11
11
  };
12
12
  }
13
13
  /**
14
- * High-risk writes require explicit --yes (or equivalent confirmation token).
14
+ * Infer risk for raw `api METHOD PATH` calls.
15
+ * Catalog-matched risks win; uncataloged mutations are treated as high-risk
16
+ * so they cannot skip the `--yes` gate.
17
+ */
18
+ export declare function inferRawApiRisk(method: string, catalogRisk: string | undefined): RiskLevel | string;
19
+ export declare function requiresHighRiskConfirmation(risk: RiskLevel | string): boolean;
20
+ /**
21
+ * High-risk writes (and uncataloged/unknown mutations) require explicit --yes.
15
22
  * Server still enforces scopes/roles; this is CLI UX only.
16
23
  */
17
24
  export declare function assertHighRiskConfirmation(input: {
@@ -1,12 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferRawApiRisk = inferRawApiRisk;
4
+ exports.requiresHighRiskConfirmation = requiresHighRiskConfirmation;
3
5
  exports.assertHighRiskConfirmation = assertHighRiskConfirmation;
6
+ const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
4
7
  /**
5
- * High-risk writes require explicit --yes (or equivalent confirmation token).
8
+ * Infer risk for raw `api METHOD PATH` calls.
9
+ * Catalog-matched risks win; uncataloged mutations are treated as high-risk
10
+ * so they cannot skip the `--yes` gate.
11
+ */
12
+ function inferRawApiRisk(method, catalogRisk) {
13
+ if (catalogRisk) {
14
+ return catalogRisk;
15
+ }
16
+ if (MUTATING_METHODS.has(method.toUpperCase())) {
17
+ return "unknown";
18
+ }
19
+ return "read";
20
+ }
21
+ function requiresHighRiskConfirmation(risk) {
22
+ return risk === "high-risk-write" || risk === "unknown";
23
+ }
24
+ /**
25
+ * High-risk writes (and uncataloged/unknown mutations) require explicit --yes.
6
26
  * Server still enforces scopes/roles; this is CLI UX only.
7
27
  */
8
28
  function assertHighRiskConfirmation(input) {
9
- if (input.risk !== "high-risk-write") {
29
+ if (!requiresHighRiskConfirmation(input.risk)) {
10
30
  return { allowed: true };
11
31
  }
12
32
  if (input.dryRun) {
@@ -15,14 +35,17 @@ function assertHighRiskConfirmation(input) {
15
35
  if (input.yes) {
16
36
  return { allowed: true };
17
37
  }
38
+ const unknown = input.risk === "unknown";
18
39
  return {
19
40
  allowed: false,
20
41
  error: {
21
42
  type: "confirmation",
22
43
  subtype: "confirmation_required",
23
44
  message: `${input.action} requires confirmation`,
24
- hint: "add --yes to confirm",
25
- risk: "high-risk-write",
45
+ hint: unknown
46
+ ? "uncataloged mutation treated as high-risk; add --yes to confirm"
47
+ : "add --yes to confirm",
48
+ risk: unknown ? "unknown" : "high-risk-write",
26
49
  action: input.action,
27
50
  },
28
51
  };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const CLI_NAME = "alphafox";
2
2
  export declare const CLI_PACKAGE = "@alphafox/cli";
3
- export declare const CLI_VERSION = "0.1.5";
4
- export declare const CLI_CONTRACT_VERSION = "2026-08-11";
3
+ export declare const CLI_VERSION = "0.3.2";
4
+ export { CATALOG_VERSION as CLI_CONTRACT_VERSION } from "./catalog/operations";
package/dist/version.js CHANGED
@@ -3,5 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CLI_CONTRACT_VERSION = exports.CLI_VERSION = exports.CLI_PACKAGE = exports.CLI_NAME = void 0;
4
4
  exports.CLI_NAME = "alphafox";
5
5
  exports.CLI_PACKAGE = "@alphafox/cli";
6
- exports.CLI_VERSION = "0.1.5";
7
- exports.CLI_CONTRACT_VERSION = "2026-08-11";
6
+ exports.CLI_VERSION = "0.3.2";
7
+ var operations_1 = require("./catalog/operations");
8
+ Object.defineProperty(exports, "CLI_CONTRACT_VERSION", { enumerable: true, get: function () { return operations_1.CATALOG_VERSION; } });
package/docs/.nojekyll ADDED
File without changes
@@ -0,0 +1,51 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`CONTEXT.md`** at the repo root, or
8
+ - **`CONTEXT-MAP.md`** at the repo root if it exists β€” it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
9
+ - **`docs/adr/`** β€” read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
12
+
13
+ ## File structure
14
+
15
+ Single-context repo (most repos):
16
+
17
+ ```
18
+ /
19
+ β”œβ”€β”€ CONTEXT.md
20
+ β”œβ”€β”€ docs/adr/
21
+ β”‚ β”œβ”€β”€ 0001-event-sourced-orders.md
22
+ β”‚ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
27
+
28
+ ```
29
+ /
30
+ β”œβ”€β”€ CONTEXT-MAP.md
31
+ β”œβ”€β”€ docs/adr/ ← system-wide decisions
32
+ └── src/
33
+ β”œβ”€β”€ ordering/
34
+ β”‚ β”œβ”€β”€ CONTEXT.md
35
+ β”‚ └── docs/adr/ ← context-specific decisions
36
+ └── billing/
37
+ β”œβ”€β”€ CONTEXT.md
38
+ └── docs/adr/
39
+ ```
40
+
41
+ ## Use the glossary's vocabulary
42
+
43
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
44
+
45
+ If the concept you need isn't in the glossary yet, that's a signal β€” either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
46
+
47
+ ## Flag ADR conflicts
48
+
49
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
50
+
51
+ > _Contradicts ADR-0007 (event-sourced orders) β€” but worth reopening because…_
@@ -0,0 +1,156 @@
1
+ # Issue tracker: Feishu Tasks
2
+
3
+ Issues and specs for this repo live as tasks in the **Alphafox-Issues** Feishu (Lark) tasklist. Use the `lark-cli` CLI ([`@larksuite/cli`](https://github.com/larksuite/cli)) for all operations, always with `--as user` β€” a task created by the bot identity is invisible in the human's task centre.
4
+
5
+ List: <https://applink.feishu.cn/client/todo/task_list?guid=6d628d79-cdfe-47cc-98f7-35561944494b>
6
+
7
+ ## Identifiers
8
+
9
+ An issue's identity is its **task GUID** β€” a UUID. There is no usable short number: the `t101420`-style id the Feishu UI shows (`task_id`, `suite_entity_num`) is **display-only** and every API rejects it, so a bare `#42` or `t101420` cannot be looked up directly.
10
+
11
+ - Prefer the task **URL** (`https://applink.feishu.cn/client/todo/detail?guid=<GUID>`) β€” the GUID is the `guid` query param. Every write command below also accepts the URL in place of the GUID.
12
+ - Given only a `t1014xx` number or a title fragment, resolve it with `lark-cli task +search --query "<title words>" --as user` and match on `summary`. Search spans **all** the user's tasks, not just this tasklist, so confirm the match before writing.
13
+ - When narrating to the human, refer to an issue by its **title**, not its GUID.
14
+
15
+ ## Setup values
16
+
17
+ - **Tasklist GUID**: `6d628d79-cdfe-47cc-98f7-35561944494b`
18
+ - **`Type` field GUID**: `227d69e7-e535-40f0-b64f-4e90247149e2`
19
+
20
+ Triage roles are **sections** β€” a task sits in exactly one, so the state machine can't be violated. `εΎ…εˆ†η±»` is the default section, so anything created without a section lands there as untriaged.
21
+
22
+ | Canonical role | Section | Section GUID |
23
+ | -------------- | ------- | ------------ |
24
+ | _(untriaged)_ | `εΎ…εˆ†η±»` (default) | `4ca8fd68-9a08-5794-1598-8e5a394e8638` |
25
+ | `needs-triage` | `εΎ…θ―„δΌ°` | `44e58692-1ced-462b-99bb-bf082a745ef3` |
26
+ | `needs-info` | `εΎ…θ‘₯充俑息` | `0cff4cf9-00b9-42bd-b482-5cd06a51b99b` |
27
+ | `ready-for-agent` | `可亀给 Agent` | `1ce586d5-7953-4d68-b41a-d4b2851f5806` |
28
+ | `ready-for-human` | `ιœ€δΊΊε·₯倄理` | `8713387c-e758-4730-9de8-0b30859674ff` |
29
+ | `wontfix` | `δΈδΊˆε€„η†` | `00d4b874-1a4a-43d8-b0cf-fcb52460f9bf` |
30
+ | _(wayfinder β€” outside the triage queue)_ | `ζŽ’θ·―ε›Ύ` | `bab88abf-0bf9-407d-806b-680901a86dd7` |
31
+
32
+ Every other label is an option on the single-select `Type` field. Address an option by its GUID, never by name β€” the skills' canonical labels use a colon (`wayfinder:map`) while the Feishu options are named with a hyphen (`wayfinder-map`), so this table is the only mapping between them:
33
+
34
+ | Label | Option GUID |
35
+ | ----- | ----------- |
36
+ | `bug` | `0eb51704-bead-4e8e-93be-dd8d02c1d20c` |
37
+ | `enhancement` | `ebd05bc8-ab6c-4a56-8628-6c3284d9f1f9` |
38
+ | `spec` | `399a640f-5d51-4dab-a4f3-da3df638ec8a` |
39
+ | `ticket` | `3bb7ca44-ac8c-4b75-8925-96fc457fb974` |
40
+ | `wayfinder:map` | `8cd13987-010e-46f0-808f-a8f289791cf5` |
41
+ | `wayfinder:research` | `f18b808a-d978-41cb-9bef-cd0708fecf0c` |
42
+ | `wayfinder:prototype` | `49126969-48dc-4297-96d4-36d176ffb8bb` |
43
+ | `wayfinder:grilling` | `265feb46-9ac8-4c03-afa8-9400296defe4` |
44
+ | `wayfinder:task` | `ed48b848-01d0-406b-ac7d-a96ce66cee0d` |
45
+
46
+ ## Conventions
47
+
48
+ - **Shared-list title** β€” prefix every issue/task title with `[alphafox-cli]` so the repository remains identifiable on the shared board.
49
+ - **Tracker boundary** β€” Matt Skills issues, specs, tickets, triage items, and wayfinder maps belong in `Alphafox-Issues`, not a quarterly execution list. If quarterly planning initiated the work, link its absolute Feishu task URL instead of duplicating workflow state.
50
+
51
+ - **Create an issue** β€” one call sets body, section (triage role) and `Type`:
52
+
53
+ ```bash
54
+ lark-cli task tasks create --as user --params '{"user_id_type":"open_id"}' --data '{
55
+ "summary": "<title>",
56
+ "description": "<body>",
57
+ "tasklists": [{"tasklist_guid": "6d628d79-cdfe-47cc-98f7-35561944494b", "section_guid": "<SECTION_GUID>"}],
58
+ "custom_fields": [{"guid": "227d69e7-e535-40f0-b64f-4e90247149e2", "single_select_value": "<OPTION_GUID>"}]
59
+ }'
60
+ ```
61
+
62
+ For an untriaged issue with no label, `lark-cli task +create --tasklist-id 6d628d79-cdfe-47cc-98f7-35561944494b --summary "..." --description "..." --as user` is enough β€” it lands in `εΎ…εˆ†η±»`.
63
+
64
+ - **Read an issue** β€” the body and the comments are two calls:
65
+
66
+ ```bash
67
+ lark-cli task tasks get --as user --params '{"task_guid": "<GUID>", "user_id_type": "open_id"}'
68
+ lark-cli api GET /open-apis/task/v2/comments --as user \
69
+ --params '{"resource_type": "task", "resource_id": "<GUID>", "page_size": 50, "user_id_type": "open_id"}'
70
+ ```
71
+
72
+ `tasks get` returns `description` (the body), `custom_fields` (the `Type` label), `tasklists[].section_guid` (the triage role), `members` (assignees), `status` (`todo`/`done`), `dependencies` (blockers) and `parent_task_guid`. Comments are **not** exposed by `lark-cli task`; the raw `api` call above is the only way to read them.
73
+
74
+ - **List a triage bucket** β€” one call per section, and this is the cheap path:
75
+
76
+ ```bash
77
+ lark-cli task sections tasks --as user --params '{"section_guid": "<SECTION_GUID>", "completed": false, "page_size": 100}'
78
+ ```
79
+
80
+ Returns a brief per task β€” `guid`, `summary`, `completed_at`, `subtask_count` β€” which is all a queue listing needs. Pass `created_from`/`created_to` to window by age; results come oldest-first. Reading a task's `Type` or assignees means a `tasks get` per task, so don't do it while building a queue listing.
81
+
82
+ - **List every issue**: `lark-cli task tasklists tasks --as user --params '{"tasklist_guid": "6d628d79-cdfe-47cc-98f7-35561944494b", "completed": false, "page_size": 100}'`.
83
+
84
+ - **Comment on an issue**: `lark-cli task +comment --task-id <GUID> --content "..." --as user`. Plain text β€” Feishu renders no markdown in comments, so keep formatting light.
85
+
86
+ - **Apply a triage role** β€” move the task to that role's section. Re-adding an existing task with a new `--section-guid` moves it; there is no separate "remove from old section" step:
87
+
88
+ ```bash
89
+ lark-cli task +tasklist-task-add --tasklist-id 6d628d79-cdfe-47cc-98f7-35561944494b --task-id <GUID> --section-guid <SECTION_GUID> --as user
90
+ ```
91
+
92
+ - **Apply a `Type` label** β€” patch the custom field. This replaces the previous value, since the field is single-select:
93
+
94
+ ```bash
95
+ lark-cli task tasks patch --as user --params '{"task_guid": "<GUID>"}' --data '{
96
+ "task": {"custom_fields": [{"guid": "227d69e7-e535-40f0-b64f-4e90247149e2", "single_select_value": "<OPTION_GUID>"}]},
97
+ "update_fields": ["custom_fields"]
98
+ }'
99
+ ```
100
+
101
+ - **Edit the body**: same shape, with `{"task": {"description": "..."}, "update_fields": ["description"]}`. A patch **replaces** the description, so read it first and re-send the whole text.
102
+
103
+ - **Every markdown link in a body needs a real absolute URL.** Feishu parses `[text](target)` in `description` and validates the target, rejecting the entire write with `Invalid Param 'description', url in description is invalid` when it isn't one. A placeholder `(link)`, an empty `()`, and a repo-relative `(./src/foo.ts)` all fail. Link to an absolute `https://` URL, or drop the link syntax and name the thing in plain text. Headings, lists, bold, bare URLs and task GUIDs are all fine.
104
+
105
+ - **Close**: `lark-cli task +complete --task-id <GUID> --as user`. It takes no closing comment, so post the explanation with `+comment` first, then complete. Reopen with `lark-cli task +reopen --task-id <GUID> --as user`.
106
+
107
+ - **Assign**: `lark-cli task +assign --task-id <GUID> --add <open_id> --as user` (`--remove` to unassign). Get your own `open_id` from `lark-cli auth status --jq '.identities.user.openId'`.
108
+
109
+ ## Pull requests as a triage surface
110
+
111
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
112
+
113
+ Feishu Tasks holds no code, so PRs live on the GitHub remote. When this flag is `yes`, triage reads a PR and its diff with `gh pr view` / `gh pr list` / `gh pr diff` while the roles and states stay here in the tasklist: a PR under triage gets a task in this list whose body links the PR. Numbering never collides, because a task GUID and a `#42` are different shapes.
114
+
115
+ ## When a skill says "publish to the issue tracker"
116
+
117
+ Create a task in this tasklist. Put the whole document in `description`, set `Type`, and place it in the section matching its triage role.
118
+
119
+ ## When a skill says "fetch the relevant ticket"
120
+
121
+ Run `tasks get` for the body plus the raw `comments` call for the history β€” an agent brief or a resolution lives in one or the other.
122
+
123
+ ## Wayfinding operations
124
+
125
+ Used by `/wayfinder`. The **map** is a task; its tickets are **subtasks** of it.
126
+
127
+ - **Map**: a task with `Type` = `wayfinder:map`, in `ζŽ’θ·―ε›Ύ`, holding the Destination / Notes / Decisions-so-far / Fog body in `description`.
128
+ - **Child ticket**: create the task, then parent it β€” `lark-cli task +set-ancestor --task-id <CHILD_GUID> --ancestor-id <MAP_GUID> --as user`. Parenting **keeps** the child's tasklist and section membership, so tickets stay visible in `ζŽ’θ·―ε›Ύ`. Label each with `Type` = `wayfinder:<type>`.
129
+ - **Blocking**: Feishu's **native task dependencies** β€” the canonical, UI-visible representation. `type: "prev"` means "blocks this task", so a child blocked by another records the blocker as `prev`:
130
+
131
+ ```bash
132
+ lark-cli api POST /open-apis/task/v2/tasks/<CHILD_GUID>/add_dependencies --as user \
133
+ --data '{"dependencies": [{"task_guid": "<BLOCKER_GUID>", "type": "prev"}]}'
134
+ ```
135
+
136
+ The reciprocal `next` edge appears on the blocker automatically. Drop an edge with `--data '{"dependencies": [{"task_guid": "<BLOCKER_GUID>"}]}'` against `.../remove_dependencies`. Dependencies are **not** settable through `tasks create` or `tasks patch` β€” the raw `api` call is the only route, so tickets must exist before they can be wired, which is why charting creates first and wires second.
137
+
138
+ - **Frontier query** β€” one call, because `subtasks list` returns *full* task objects rather than briefs:
139
+
140
+ ```bash
141
+ lark-cli task subtasks list --as user --params '{"task_guid": "<MAP_GUID>", "page_size": 100, "user_id_type": "open_id"}'
142
+ ```
143
+
144
+ Each child carries `status`, `members`, `dependencies` and `description`. Filter locally: keep `status` of `todo`, drop any with a `members` entry whose `role` is `assignee` (claimed), and drop any whose `prev` dependencies include a task still `todo` (blocked). Blockers that are siblings on the same map are already in this response; resolve any others with `tasks get`. First in map order wins.
145
+
146
+ - **Claim**: `lark-cli task +assign --task-id <GUID> --add <your open_id> --as user` β€” the session's first write.
147
+ - **Resolve**: `+comment` with the answer, then `+complete`, then patch the map's `description` to append a context pointer to Decisions-so-far. That pointer's link must be the ticket's full applink URL β€” a Decisions-so-far line written as `[title](link)` is rejected by the description's URL validation, taking the whole map update with it.
148
+
149
+ ## Re-reading the setup values
150
+
151
+ ```bash
152
+ lark-cli task sections list --as user --params '{"resource_type": "tasklist", "resource_id": "6d628d79-cdfe-47cc-98f7-35561944494b", "page_size": 50}'
153
+ lark-cli task custom_fields list --as user --params '{"resource_type": "tasklist", "resource_id": "6d628d79-cdfe-47cc-98f7-35561944494b", "page_size": 50}'
154
+ ```
155
+
156
+ `custom_fields list` returns each option's `guid` alongside its `name`, which is what the `Type` table above records.