@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.
- package/README.md +42 -4
- package/dist/auth/browser-login.d.ts +30 -0
- package/dist/auth/browser-login.js +193 -0
- package/dist/auth/loopback-callback.d.ts +32 -0
- package/dist/auth/loopback-callback.js +175 -0
- package/dist/auth/open-browser.d.ts +15 -0
- package/dist/auth/open-browser.js +54 -0
- package/dist/auth/refresh.d.ts +27 -2
- package/dist/auth/refresh.js +52 -15
- package/dist/catalog/allowlist.d.ts +20 -4
- package/dist/catalog/allowlist.js +126 -25
- package/dist/catalog/command-tree.d.ts +35 -0
- package/dist/catalog/command-tree.js +119 -0
- package/dist/catalog/compatibility.d.ts +23 -0
- package/dist/catalog/compatibility.js +58 -0
- package/dist/catalog/generated/registry.json +6137 -0
- package/dist/catalog/generated/schemas.json +31036 -0
- package/dist/catalog/operations.d.ts +76 -3
- package/dist/catalog/operations.js +87 -213
- package/dist/catalog/validate-body.d.ts +22 -0
- package/dist/catalog/validate-body.js +98 -0
- package/dist/commands/request-body.d.ts +21 -0
- package/dist/commands/request-body.js +92 -0
- package/dist/commands/run.js +283 -162
- package/dist/config/profiles.js +3 -3
- package/dist/engine-backtest/errors.d.ts +18 -0
- package/dist/engine-backtest/errors.js +26 -0
- package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
- package/dist/engine-backtest/fetch-runtime.js +170 -0
- package/dist/engine-backtest/native-import.d.ts +1 -0
- package/dist/engine-backtest/native-import.js +10 -0
- package/dist/engine-backtest/parse-args.d.ts +6 -0
- package/dist/engine-backtest/parse-args.js +281 -0
- package/dist/engine-backtest/persist.d.ts +32 -0
- package/dist/engine-backtest/persist.js +94 -0
- package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
- package/dist/engine-backtest/replay-timeframe.js +48 -0
- package/dist/engine-backtest/resolve-packages.d.ts +27 -0
- package/dist/engine-backtest/resolve-packages.js +288 -0
- package/dist/engine-backtest/run-command.d.ts +38 -0
- package/dist/engine-backtest/run-command.js +540 -0
- package/dist/engine-backtest/types.d.ts +249 -0
- package/dist/engine-backtest/types.js +2 -0
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +43 -3
- package/dist/http/client.js +18 -15
- package/dist/index.d.ts +16 -5
- package/dist/index.js +47 -1
- package/dist/install/exec.d.ts +13 -0
- package/dist/install/exec.js +73 -0
- package/dist/install/package-root.d.ts +4 -0
- package/dist/install/package-root.js +59 -0
- package/dist/install/types.d.ts +69 -0
- package/dist/install/types.js +26 -0
- package/dist/install/wizard.d.ts +21 -0
- package/dist/install/wizard.js +332 -0
- package/dist/keychain/linux-secret-service.d.ts +11 -0
- package/dist/keychain/linux-secret-service.js +93 -0
- package/dist/keychain/store.d.ts +20 -1
- package/dist/keychain/store.js +94 -6
- package/dist/keychain/windows-credential.d.ts +13 -0
- package/dist/keychain/windows-credential.js +176 -0
- package/dist/safety/confirmation.d.ts +10 -3
- package/dist/safety/confirmation.js +27 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +3 -2
- package/docs/.nojekyll +0 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +156 -0
- package/docs/agents/triage-labels.md +18 -0
- package/docs/alphafox-cli-installation-guide.md +105 -0
- package/docs/e2e-staging.md +76 -6
- package/docs/favicon.svg +4 -0
- package/docs/index.html +748 -0
- package/docs/logo/alphafox-mark.svg +4 -0
- package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
- package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
- package/docs/release-supply-chain.md +107 -26
- package/package.json +10 -3
- package/skills/account/SKILL.md +8 -6
- package/skills/admin/SKILL.md +9 -4
- package/skills/alphafox-shared/SKILL.md +44 -14
- package/skills/auth/SKILL.md +21 -6
- package/skills/engine-backtest/SKILL.md +71 -0
- package/skills/exchange/SKILL.md +10 -3
- package/skills/market/SKILL.md +9 -4
- package/skills/notification/SKILL.md +8 -3
- package/skills/strategy/SKILL.md +23 -9
- package/skills/trading/SKILL.md +18 -5
- package/vendor/backtest-runner/NOTICE.md +1 -0
- package/vendor/backtest-runner/README.md +97 -0
- package/vendor/backtest-runner/index.d.ts +423 -0
- package/vendor/backtest-runner/index.mjs +59 -0
- package/vendor/backtest-runner/lib/abortable.mjs +23 -0
- package/vendor/backtest-runner/lib/cache.mjs +159 -0
- package/vendor/backtest-runner/lib/coverage.mjs +238 -0
- package/vendor/backtest-runner/lib/encode.mjs +28 -0
- package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
- package/vendor/backtest-runner/lib/proxy.mjs +78 -0
- package/vendor/backtest-runner/lib/scenario.mjs +66 -0
- package/vendor/backtest-runner/lib/series.mjs +370 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
- package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
- package/vendor/backtest-runner/package.json +13 -0
package/dist/keychain/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
25
|
-
|
|
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.
|
|
4
|
-
export
|
|
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.
|
|
7
|
-
|
|
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.
|