@windsland52/maafw-live 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -2
  3. package/bin/maafw-live.mjs +25 -0
  4. package/docs/daemon-protocol.md +116 -0
  5. package/docs/keyframe-evidence-contract-v0.md +61 -0
  6. package/docs/keyframe-retention-contract-v0.md +236 -0
  7. package/lib/client/daemon.js +234 -0
  8. package/lib/client/daemon.js.map +1 -0
  9. package/lib/commands/annotate.js +172 -0
  10. package/lib/commands/annotate.js.map +1 -0
  11. package/lib/commands/calibrate.js +61 -0
  12. package/lib/commands/calibrate.js.map +1 -0
  13. package/lib/commands/color.js +73 -0
  14. package/lib/commands/color.js.map +1 -0
  15. package/lib/commands/crop.js +228 -0
  16. package/lib/commands/crop.js.map +1 -0
  17. package/lib/commands/env.js +215 -0
  18. package/lib/commands/env.js.map +1 -0
  19. package/lib/commands/index.js +64 -0
  20. package/lib/commands/index.js.map +1 -0
  21. package/lib/commands/input.js +219 -0
  22. package/lib/commands/input.js.map +1 -0
  23. package/lib/commands/kf.js +131 -0
  24. package/lib/commands/kf.js.map +1 -0
  25. package/lib/commands/reco.js +211 -0
  26. package/lib/commands/reco.js.map +1 -0
  27. package/lib/commands/repl.js +698 -0
  28. package/lib/commands/repl.js.map +1 -0
  29. package/lib/commands/runtime.js +514 -0
  30. package/lib/commands/runtime.js.map +1 -0
  31. package/lib/commands/skill.js +169 -0
  32. package/lib/commands/skill.js.map +1 -0
  33. package/lib/commands/timing.js +229 -0
  34. package/lib/commands/timing.js.map +1 -0
  35. package/lib/commands/version.js +66 -0
  36. package/lib/commands/version.js.map +1 -0
  37. package/lib/daemon/framed.mjs +2850 -0
  38. package/lib/daemon/reco_child.mjs +146 -0
  39. package/lib/exec.js +96 -0
  40. package/lib/exec.js.map +1 -0
  41. package/lib/flags.js +42 -0
  42. package/lib/flags.js.map +1 -0
  43. package/lib/image.js +90 -0
  44. package/lib/image.js.map +1 -0
  45. package/lib/index.js +139 -0
  46. package/lib/index.js.map +1 -0
  47. package/lib/interface/index.js +12 -0
  48. package/lib/interface/index.js.map +1 -0
  49. package/lib/interface/jsonc.js +95 -0
  50. package/lib/interface/jsonc.js.map +1 -0
  51. package/lib/interface/load.js +307 -0
  52. package/lib/interface/load.js.map +1 -0
  53. package/lib/interface/override.js +103 -0
  54. package/lib/interface/override.js.map +1 -0
  55. package/lib/interface/plan.js +188 -0
  56. package/lib/interface/plan.js.map +1 -0
  57. package/lib/pkg.js +18 -0
  58. package/lib/pkg.js.map +1 -0
  59. package/lib/protocol.js +90 -0
  60. package/lib/protocol.js.map +1 -0
  61. package/lib/runtime/actions.js +110 -0
  62. package/lib/runtime/actions.js.map +1 -0
  63. package/lib/runtime/keyframes.js +73 -0
  64. package/lib/runtime/keyframes.js.map +1 -0
  65. package/lib/runtime/session.js +86 -0
  66. package/lib/runtime/session.js.map +1 -0
  67. package/lib/types/client/daemon.d.ts +86 -0
  68. package/lib/types/commands/annotate.d.ts +3 -0
  69. package/lib/types/commands/calibrate.d.ts +7 -0
  70. package/lib/types/commands/color.d.ts +9 -0
  71. package/lib/types/commands/crop.d.ts +3 -0
  72. package/lib/types/commands/env.d.ts +2 -0
  73. package/lib/types/commands/index.d.ts +13 -0
  74. package/lib/types/commands/input.d.ts +17 -0
  75. package/lib/types/commands/kf.d.ts +10 -0
  76. package/lib/types/commands/reco.d.ts +23 -0
  77. package/lib/types/commands/repl.d.ts +2 -0
  78. package/lib/types/commands/runtime.d.ts +48 -0
  79. package/lib/types/commands/skill.d.ts +17 -0
  80. package/lib/types/commands/timing.d.ts +64 -0
  81. package/lib/types/commands/version.d.ts +3 -0
  82. package/lib/types/exec.d.ts +31 -0
  83. package/lib/types/flags.d.ts +58 -0
  84. package/lib/types/image.d.ts +27 -0
  85. package/lib/types/index.d.ts +1 -0
  86. package/lib/types/interface/index.d.ts +11 -0
  87. package/lib/types/interface/jsonc.d.ts +9 -0
  88. package/lib/types/interface/load.d.ts +113 -0
  89. package/lib/types/interface/override.d.ts +33 -0
  90. package/lib/types/interface/plan.d.ts +47 -0
  91. package/lib/types/pkg.d.ts +6 -0
  92. package/lib/types/protocol.d.ts +108 -0
  93. package/lib/types/runtime/actions.d.ts +166 -0
  94. package/lib/types/runtime/keyframes.d.ts +43 -0
  95. package/lib/types/runtime/session.d.ts +53 -0
  96. package/lib/types/update-check.d.ts +20 -0
  97. package/lib/types/usage.d.ts +3 -0
  98. package/lib/update-check.js +97 -0
  99. package/lib/update-check.js.map +1 -0
  100. package/lib/usage.js +50 -0
  101. package/lib/usage.js.map +1 -0
  102. package/package.json +81 -4
  103. package/skills/README.md +51 -0
  104. package/skills/maafw-live/SKILL.md +263 -0
  105. package/skills/maafw-live/references/examples.md +238 -0
  106. package/skills/maafw-live/references/pitfalls.md +118 -0
  107. package/skills/maafw-live/references/workflows.md +159 -0
@@ -0,0 +1,166 @@
1
+ import { resolveResourcePaths, type ControllerPlan, type DeviceLike, type LoadedInterface } from '../interface/index.js';
2
+ import { type DaemonClient, type SpawnOptions } from '../client/daemon.js';
3
+ /** 设备会话的连接参数(手动路径)。kind/target 之外全部来自 maafw 的控制器能力。 */
4
+ export interface ManualConnect {
5
+ kind: 'adb' | 'win32' | 'gamepad';
6
+ target?: string;
7
+ /** 截图缩放覆盖(会改变 Tasker 识别图像的坐标系,模板/roi 随之变化) */
8
+ shortSide?: number;
9
+ longSide?: number;
10
+ rawSize?: boolean;
11
+ screencap?: string;
12
+ mouse?: string;
13
+ keyboard?: string;
14
+ gamepadType?: string;
15
+ }
16
+ export interface ConnectSession {
17
+ ok: boolean;
18
+ error?: string;
19
+ session?: Record<string, unknown> | null;
20
+ /** 本次连接重建了控制器、因而停掉了上一轮的帧流(需要看帧就重新 stream_start) */
21
+ streamStopped?: boolean;
22
+ }
23
+ /** 起一个 daemon、跑 fn、无论如何都收掉子进程。一次性命令用它,REPL 用长活客户端。 */
24
+ export declare function withDaemon<T>(fn: (client: DaemonClient) => Promise<T>, options?: SpawnOptions): Promise<T>;
25
+ export declare const probe: (c: DaemonClient) => Promise<Record<string, unknown>>;
26
+ export declare const deviceList: (c: DaemonClient, kind?: "all" | "adb" | "win32") => Promise<DeviceLike[]>;
27
+ export declare const connect: (c: DaemonClient, args: Record<string, unknown>) => Promise<ConnectSession>;
28
+ export declare const disconnect: (c: DaemonClient) => Promise<{
29
+ ok: boolean;
30
+ }>;
31
+ export declare const screencap: (c: DaemonClient, out?: string) => Promise<Record<string, unknown>>;
32
+ /**
33
+ * 一次性命令的"先产生一次观测":截一帧进 daemon 的 L0,然后**把临时文件删掉**。
34
+ *
35
+ * 为什么需要这一帧:L0 是进程态,一次性命令(`crop` / `annotate` / `color`)每次 spawn 自己的
36
+ * daemon,不先截一帧就没有原图可用。为什么必须删:daemon 收帧时就把像素读进缓冲了,
37
+ * 落盘那份只为喂它一次——不删就是每跑一次在系统临时目录留一张 720p 截图(实测攒到 42 张 / 54MB)。
38
+ * 失败不抛:没有观测时命令自己会报"先 screencap / stream"这类可行动的错。
39
+ */
40
+ export declare function seedObservation(c: DaemonClient, prefix: string): Promise<void>;
41
+ export declare const streamStart: (c: DaemonClient, args?: {
42
+ fps?: number;
43
+ scale?: number;
44
+ maxFrames?: number;
45
+ l0Roll?: number;
46
+ l0Anchor?: number;
47
+ l0Bytes?: number;
48
+ blockThresh?: number;
49
+ changeGlobal?: number;
50
+ }) => Promise<Record<string, unknown>>;
51
+ export declare const streamStop: (c: DaemonClient) => Promise<{
52
+ ok: boolean;
53
+ }>;
54
+ export declare const streamStatus: (c: DaemonClient) => Promise<Record<string, unknown>>;
55
+ export declare const frameGet: (c: DaemonClient, args: {
56
+ seq?: number;
57
+ roi?: number[];
58
+ out?: string;
59
+ src?: "auto" | "full" | "ring";
60
+ }) => Promise<Record<string, unknown>>;
61
+ /** 等状态谓词成立:stable=画面静下来 / change=画面动了。取代调用方硬睡时钟。 */
62
+ export declare const waitState: (c: DaemonClient, args?: {
63
+ mode?: "stable" | "change";
64
+ timeout?: number;
65
+ quiet?: number;
66
+ roi?: number[];
67
+ threshold?: number;
68
+ }) => Promise<Record<string, unknown>>;
69
+ /** 两帧在若干 ROI 上的差分:回答"这一步改了哪几格"。 */
70
+ export declare const frameDiff: (c: DaemonClient, args: {
71
+ a: number;
72
+ b: number;
73
+ rois: number[][];
74
+ }) => Promise<Record<string, unknown>>;
75
+ export declare const colorProbe: (c: DaemonClient, args: {
76
+ seq?: number;
77
+ roi?: number[];
78
+ }) => Promise<Record<string, unknown>>;
79
+ /** 裁剪源 = 关键帧库留存帧(离线解析出来的身份 + 路径;daemon 会复核 sha256 与像素尺寸)。 */
80
+ export interface CropKfSource {
81
+ id: string;
82
+ path: string;
83
+ sha256: string;
84
+ ctrlW: number;
85
+ ctrlH: number;
86
+ captureSeq: number | null;
87
+ capturedAt: string | null;
88
+ }
89
+ export declare const tplCrop: (c: DaemonClient, args: {
90
+ seq?: number;
91
+ roi?: number[];
92
+ point?: number[];
93
+ pad?: number;
94
+ out?: string;
95
+ resourceDir?: string;
96
+ cross?: boolean;
97
+ kfSource?: CropKfSource;
98
+ prov?: boolean;
99
+ /** 出处侧车改写到该目录下(保持同名);缺省写在模板旁 */
100
+ provOut?: string;
101
+ /** 热缓存路径下把本次实际用的那一帧升格进库,出处随即带上可复核的 `kf:` 身份 */
102
+ keepSource?: boolean;
103
+ /** snap 收紧阈值的**测量用覆盖**(定标 A/B);缺省即生产常数 */
104
+ snapTol?: number;
105
+ snapFrac?: number;
106
+ }) => Promise<Record<string, unknown>>;
107
+ export declare const annotate: (c: DaemonClient, args: {
108
+ seq?: number;
109
+ out?: string;
110
+ resourceDir?: string;
111
+ kfSource?: CropKfSource;
112
+ /** 候选上限(缺省 30;截断按横向分带轮转,`mergedTotal` 才是去重后的真实规模) */
113
+ somLimit?: number;
114
+ /** 边缘源门槛的**测量用覆盖**(定标 A/B);缺省即生产常数 */
115
+ somEdgeZ?: number;
116
+ somEdgeMin?: number;
117
+ /** 连通域合并 IoU 的测量用覆盖 */
118
+ somIoU?: number;
119
+ /** 巨框剔除阈值(画面面积比,缺省 0.12;OCR 框不参与)——背景/立绘/整屏变化区不该占表 */
120
+ somMaxAreaRatio?: number;
121
+ /** 只保留落在该区域(控制器空间)内的候选,且在**取上限之前**过滤:
122
+ * 区域内的候选只跟自己竞争。上限截断的正解是收小搜索面,不是把上限调大。 */
123
+ roi?: number[];
124
+ /** 检测面:full(缺省,有 L0 时用控制器分辨率原图) | small(强制降采样小图,快但小目标会漏) */
125
+ somScale?: "full" | "small";
126
+ }) => Promise<Record<string, unknown>>;
127
+ export declare const calibrate: (c: DaemonClient, args?: {
128
+ frames?: number;
129
+ interval?: number;
130
+ }) => Promise<Record<string, unknown>>;
131
+ export declare const runStop: (c: DaemonClient) => Promise<{
132
+ ok: boolean;
133
+ }>;
134
+ export declare const l0Status: (c: DaemonClient) => Promise<Record<string, unknown>>;
135
+ export declare const kfPromote: (c: DaemonClient, args: {
136
+ seq?: number;
137
+ latest?: boolean;
138
+ note?: string;
139
+ }) => Promise<Record<string, unknown>>;
140
+ export declare function run(c: DaemonClient, args: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
141
+ export declare const input: (c: DaemonClient, payload: Record<string, unknown>, timeoutMs?: number) => Promise<Record<string, unknown>>;
142
+ export declare const recoTest: (c: DaemonClient, args: Record<string, unknown>) => Promise<Record<string, unknown>>;
143
+ /** 控制器类型 → 需要扫描的设备类型。控制器没声明时按第一个控制器推断。 */
144
+ export declare function deviceKindFor(loaded: LoadedInterface, controllerName: string | null): 'adb' | 'win32';
145
+ export interface ProjectPlan {
146
+ loaded: LoadedInterface;
147
+ plan: ControllerPlan | null;
148
+ controllerName: string | null;
149
+ resource: ReturnType<typeof resolveResourcePaths> | null;
150
+ error?: string;
151
+ }
152
+ /**
153
+ * 载入项目 → 扫设备 → 规划控制器 → 解析资源路径。
154
+ *
155
+ * 为什么不猜设备:interface.json 的 controller[] 已经声明了 Adb/Win32、窗口类名与标题正则、
156
+ * 截图与输入方法、以及识别缩放(display_short_side)——猜错不只是连错窗口,还会把 roi 与
157
+ * 模板的坐标系一起猜错。interface.json 的读法只有 src/interface/ 一份,别处不许再解析一遍。
158
+ */
159
+ export declare function planProject(c: DaemonClient, dir: string, overrides?: {
160
+ controller?: string;
161
+ hwnd?: string;
162
+ gamepadHwnd?: string;
163
+ address?: string;
164
+ resource?: string;
165
+ manualTarget?: string;
166
+ }): Promise<ProjectPlan>;
@@ -0,0 +1,43 @@
1
+ export declare const KF_SCHEMA = 1;
2
+ export interface KfRecord {
3
+ id: string;
4
+ file: string;
5
+ sha256: string;
6
+ session: {
7
+ daemon: string;
8
+ gen: number;
9
+ kind: string | null;
10
+ target: string | null;
11
+ };
12
+ capturedAt: string;
13
+ captureSeq: number;
14
+ ctrlW: number;
15
+ ctrlH: number;
16
+ smallW: number | null;
17
+ smallH: number | null;
18
+ scale: number | null;
19
+ source: string;
20
+ note: string | null;
21
+ }
22
+ export interface KfManifest {
23
+ schema: number;
24
+ libraryId: string;
25
+ next: number;
26
+ frames: KfRecord[];
27
+ }
28
+ export type KfResolveStatus = 'available' | 'missing' | 'corrupt' | 'unsupported';
29
+ export declare function defaultFramesDir(): string;
30
+ /** 读 manifest。坏文件/未知 schema 明确报错,不默认为当前版。 */
31
+ export declare function loadManifest(dir: string): {
32
+ manifest?: KfManifest;
33
+ error?: string;
34
+ };
35
+ /** 按完整 ID 帧解析:available 时给出经过完整性校验的路径;missing/corrupt/unsupported 各自可诊断。 */
36
+ export declare function resolveFrame(dir: string, id: string): {
37
+ status: KfResolveStatus;
38
+ record?: KfRecord;
39
+ path?: string;
40
+ reason?: string;
41
+ };
42
+ /** list 用:一行人类可读摘要(与 --json 的 data 各自完整,不共用裁剪)。 */
43
+ export declare function describeRecord(r: KfRecord): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 会话装配:一次性命令如何在同一个 daemon 里先连上设备、再干活、最后收尾。
3
+ *
4
+ * 为什么需要它:daemon 的连接是进程态——命令跑完进程就退出,连接随之消失。所以涉及设备的
5
+ * 命令(screencap / frame / run / click…)必须能在同一次调用里先完成连接,否则它们只会在
6
+ * REPL 里可用。传入 --project(推荐,走 interface.json 规划)或 --kind/--target(手动)
7
+ * 即可;两者都不给时交给命令自己处理。
8
+ */
9
+ import type { DaemonClient } from '../client/daemon.js';
10
+ import { type ProjectPlan } from './actions.js';
11
+ export interface SessionOptions {
12
+ /** Maa 项目目录:按 interface.json 的 controller[] 规划连接 */
13
+ project?: string;
14
+ /** 项目模式下指定控制器名 */
15
+ controller?: string;
16
+ resource?: string;
17
+ /** 项目模式下的窗口 / adb 地址覆盖(持久化选择由面板负责,CLI 不做持久化) */
18
+ hwnd?: string;
19
+ address?: string;
20
+ /** 手动路径 */
21
+ kind?: string;
22
+ target?: string;
23
+ shortSide?: number;
24
+ longSide?: number;
25
+ rawSize?: boolean;
26
+ }
27
+ export interface SessionState {
28
+ connected: boolean;
29
+ plan?: ProjectPlan;
30
+ session?: Record<string, unknown> | null;
31
+ notes?: string[];
32
+ /** 本次连接重建了控制器、因而停掉了上一轮的帧流(daemon 回执 streamStopped):调用方要接着看帧就重开流 */
33
+ streamStopped?: boolean;
34
+ }
35
+ export declare class SessionError extends Error {
36
+ readonly code: string;
37
+ constructor(message: string, code: string);
38
+ }
39
+ /** 需要设备时连一次;项目模式下先规划控制器再连(拒绝猜设备)。 */
40
+ export declare function ensureSession(client: DaemonClient, o: SessionOptions): Promise<SessionState>;
41
+ /**
42
+ * 离线资源解析:只读 interface.json 拿资源目录,不扫设备、不连接。
43
+ *
44
+ * 为什么需要它:从关键帧库留存帧裁模板这条动线**不需要设备**(这正是"不可复现状态"的意义——
45
+ * 设备早已不在那个画面上)。但自匹配要加载资源,而资源声明在项目里。项目此刻只是资源的
46
+ * 来源,不是连接的对象,所以走 interface 的规划函数而不走 planProject(后者要扫设备)。
47
+ */
48
+ export declare function offlineResource(project: string, controller?: string, resource?: string): {
49
+ paths: string[];
50
+ controllerName: string | null;
51
+ error?: string;
52
+ };
53
+ /** 连接结果的行渲染(human 输出与 --json 共用一份事实)。 */ export declare function describeSession(s: SessionState): string[];
@@ -0,0 +1,20 @@
1
+ export interface UpdateState {
2
+ /** 上次成功查询的时间(epoch ms) */
3
+ checkedAt: number;
4
+ /** 上次查到的 latest */
5
+ latest: string;
6
+ }
7
+ export declare const UPDATE_TTL_MS: number;
8
+ /** registry 的 packument URL:scoped 名只编码斜杠(`@scope` 保持字面) */
9
+ export declare function packumentUrl(name: string): string;
10
+ /** 语义化版本比较(只按 x.y.z 数值段;预发布标记按 semver 规则"有则更低")。返回 a-b 的符号。 */
11
+ export declare function compareVersions(a: string, b: string): number;
12
+ /** 缓存是否还新鲜(没有缓存、或时间戳比 TTL 旧 → 该查) */
13
+ export declare function shouldCheck(state: UpdateState | null, now: number, ttlMs?: number): boolean;
14
+ export declare function statePath(runDir?: string): string;
15
+ export declare function readState(file?: string): UpdateState | null;
16
+ export declare function writeState(state: UpdateState, file?: string): void;
17
+ /** 自动检查是否被环境变量关掉 */
18
+ export declare function updateCheckDisabled(env?: NodeJS.ProcessEnv): boolean;
19
+ /** 查 registry 的 latest。任何失败都返回 null(静默)。 */
20
+ export declare function fetchLatest(name: string, timeoutMs?: number): Promise<string | null>;
@@ -0,0 +1,3 @@
1
+ import type { Command } from './protocol.js';
2
+ export declare function topLevelUsage(): string[];
3
+ export declare function commandUsage(cmd: Command): string[];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 更新检查:本机版本 vs registry 上的 latest。
3
+ *
4
+ * 设计取舍(刻意做成"只报不换"):
5
+ * - **不自动切换运行时**:agent 的长任务跑到一半换版本,比"晚一天升级"风险大得多。这里只提示。
6
+ * - **只在交互终端自动查**:管道/agent 调用不为它付代价(`version --check` 是显式请求,不受此限)。
7
+ * - **限频**:结果落盘缓存(默认 24h),不每次敲命令都打网络。
8
+ * - **可关**:`MAAFW_LIVE_NO_UPDATE_CHECK=1`。
9
+ * - **失败一律静默**:离线、代理、registry 抖动都不该影响 `version` 本身——它没有变得"失败"。
10
+ */
11
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { defaultRunDir } from './client/daemon.js';
14
+ export const UPDATE_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const FETCH_TIMEOUT_MS = 3000;
16
+ /** registry 的 packument URL:scoped 名只编码斜杠(`@scope` 保持字面) */
17
+ export function packumentUrl(name) {
18
+ return 'https://registry.npmjs.org/' + name.replace('/', '%2F');
19
+ }
20
+ /** 语义化版本比较(只按 x.y.z 数值段;预发布标记按 semver 规则"有则更低")。返回 a-b 的符号。 */
21
+ export function compareVersions(a, b) {
22
+ const parse = (v) => {
23
+ const [core, ...rest] = String(v).trim().split('-');
24
+ const nums = core.split('.').map((x) => {
25
+ const n = Number(x);
26
+ return Number.isFinite(n) ? n : 0;
27
+ });
28
+ while (nums.length < 3)
29
+ nums.push(0);
30
+ return { nums: nums.slice(0, 3), pre: rest.length ? rest.join('-') : null };
31
+ };
32
+ const A = parse(a), B = parse(b);
33
+ for (let i = 0; i < 3; i++) {
34
+ if (A.nums[i] !== B.nums[i])
35
+ return A.nums[i] > B.nums[i] ? 1 : -1;
36
+ }
37
+ /* 主版本相同时:预发布低于正式版(1.0.0-rc < 1.0.0) */
38
+ if (A.pre === B.pre)
39
+ return 0;
40
+ if (A.pre === null)
41
+ return 1;
42
+ if (B.pre === null)
43
+ return -1;
44
+ return A.pre > B.pre ? 1 : -1;
45
+ }
46
+ /** 缓存是否还新鲜(没有缓存、或时间戳比 TTL 旧 → 该查) */
47
+ export function shouldCheck(state, now, ttlMs = UPDATE_TTL_MS) {
48
+ if (!state || typeof state.checkedAt !== 'number' || !Number.isFinite(state.checkedAt))
49
+ return true;
50
+ if (typeof state.latest !== 'string' || state.latest === '')
51
+ return true;
52
+ return now - state.checkedAt >= ttlMs;
53
+ }
54
+ export function statePath(runDir = defaultRunDir()) {
55
+ return join(runDir, 'version-check.json');
56
+ }
57
+ export function readState(file = statePath()) {
58
+ try {
59
+ const j = JSON.parse(readFileSync(file, 'utf8'));
60
+ return j && typeof j.checkedAt === 'number' && typeof j.latest === 'string' ? j : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ export function writeState(state, file = statePath()) {
67
+ try {
68
+ mkdirSync(dirname(file), { recursive: true });
69
+ writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
70
+ }
71
+ catch {
72
+ /* 缓存写不进去不影响任何事 */
73
+ }
74
+ }
75
+ /** 自动检查是否被环境变量关掉 */
76
+ export function updateCheckDisabled(env = process.env) {
77
+ const v = String(env.MAAFW_LIVE_NO_UPDATE_CHECK ?? '').trim();
78
+ return v !== '' && v !== '0' && v.toLowerCase() !== 'false';
79
+ }
80
+ /** 查 registry 的 latest。任何失败都返回 null(静默)。 */
81
+ export async function fetchLatest(name, timeoutMs = FETCH_TIMEOUT_MS) {
82
+ try {
83
+ const res = await fetch(packumentUrl(name), {
84
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
85
+ signal: AbortSignal.timeout(timeoutMs),
86
+ });
87
+ if (!res.ok)
88
+ return null;
89
+ const j = (await res.json());
90
+ const latest = j['dist-tags']?.latest;
91
+ return typeof latest === 'string' && latest !== '' ? latest : null;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ //# sourceMappingURL=update-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"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"}
package/lib/usage.js ADDED
@@ -0,0 +1,50 @@
1
+ /** 帮助文本。全局选项的说明集中在这里,命令只声明自己的选项。 */
2
+ import { COMMANDS } from './commands/index.js';
3
+ import { cliPackage } from './pkg.js';
4
+ const GLOBAL_HELP = [
5
+ ['--json', '输出统一 JSON 信封(stdout 保持纯 JSON)'],
6
+ ['--yes', '授权标志(当前版本无确认步骤,照传)'],
7
+ ['--no-interactive', '防交互挂住自动化(当前版本无提问,照传)'],
8
+ ['--no-color', '关闭颜色(管道下自动关闭)'],
9
+ ['--cwd <dir>', '指定工作目录'],
10
+ ['--dry-run', '预留未实现:当前不改变行为,仍会落盘'],
11
+ ['--limit <n>', '预留未实现:当前不限制输出条数'],
12
+ ['--verbose', '预留未实现:当前无额外输出'],
13
+ ['-h, --help', '显示帮助'],
14
+ ['-V, --version', '显示版本'],
15
+ ];
16
+ function pad(s, n) {
17
+ return s.length >= n ? s : s + ' '.repeat(n - s.length);
18
+ }
19
+ export function topLevelUsage() {
20
+ const pkg = cliPackage();
21
+ const nameWidth = Math.max(...COMMANDS.map((c) => c.name.length), 7);
22
+ return [
23
+ `${pkg.name} ${pkg.version} — MaaFramework 设备运行时与观测底座`,
24
+ '',
25
+ '用法:',
26
+ ' maafw-live <command> [options]',
27
+ '',
28
+ '命令:',
29
+ ...COMMANDS.map((c) => ` ${pad(c.name, nameWidth + 2)}${c.summary}`),
30
+ '',
31
+ '全局选项:',
32
+ ...GLOBAL_HELP.map(([f, d]) => ` ${pad(f, 20)}${d}`),
33
+ ];
34
+ }
35
+ export function commandUsage(cmd) {
36
+ const lines = [cmd.summary, '', '用法:', ' ' + cmd.usage];
37
+ const own = Object.entries(cmd.options ?? {});
38
+ if (own.length > 0) {
39
+ lines.push('', '选项:');
40
+ for (const [flag, spec] of own) {
41
+ const isBool = spec.type === 'boolean';
42
+ lines.push(` ${pad('--' + flag + (isBool ? '' : ' <value>'), 20)}`);
43
+ }
44
+ }
45
+ lines.push('', '全局选项:');
46
+ for (const [f, d] of GLOBAL_HELP)
47
+ lines.push(` ${pad(f, 20)}${d}`);
48
+ return lines;
49
+ }
50
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@windsland52/maafw-live",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "MaaFramework 设备交互与实时观测底座:常驻 daemon + 可复用客户端 + 确定性 CLI。不绑定宿主。",
5
+ "type": "module",
6
+ "bin": {
7
+ "maafw-live": "bin/maafw-live.mjs"
8
+ },
9
+ "main": "./lib/index.js",
10
+ "types": "./lib/types/index.d.ts",
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "docs",
15
+ "skills"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Windsland52/maafw-live.git"
20
+ },
21
+ "homepage": "https://github.com/Windsland52/maafw-live#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/Windsland52/maafw-live/issues"
24
+ },
25
+ "keywords": [
26
+ "maaframework",
27
+ "maa",
28
+ "automation",
29
+ "agent",
30
+ "adb",
31
+ "win32",
32
+ "cli",
33
+ "daemon"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "exports": {
39
+ ".": {
40
+ "types": "./lib/types/index.d.ts",
41
+ "default": "./lib/index.js"
42
+ },
43
+ "./client": {
44
+ "types": "./lib/types/client/daemon.d.ts",
45
+ "default": "./lib/client/daemon.js"
46
+ },
47
+ "./actions": {
48
+ "types": "./lib/types/runtime/actions.d.ts",
49
+ "default": "./lib/runtime/actions.js"
50
+ },
51
+ "./keyframes": {
52
+ "types": "./lib/types/runtime/keyframes.d.ts",
53
+ "default": "./lib/runtime/keyframes.js"
54
+ },
55
+ "./image": {
56
+ "types": "./lib/types/image.d.ts",
57
+ "default": "./lib/image.js"
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
61
+ "engines": {
62
+ "node": ">=22.13"
63
+ },
64
+ "license": "MIT",
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.json && node scripts/copy-daemon.mjs",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit",
68
+ "verify": "node scripts/verify-cli.mjs",
69
+ "test": "npm run build && node scripts/run-tests.mjs",
70
+ "accept:offline": "node scripts/accept-kf-offline.mjs",
71
+ "accept:crop": "node scripts/accept-crop-fromkf.mjs",
72
+ "survey:crop": "node scripts/survey-crop.mjs",
73
+ "survey:som": "node scripts/survey-som.mjs",
74
+ "accept": "node scripts/accept-emulator.mjs"
75
+ },
76
+ "dependencies": {
77
+ "@maaxyz/maa-node": "^5.14.2"
78
+ },
79
+ "devDependencies": {
80
+ "@types/node": "^24.13.3",
81
+ "typescript": "^5.9.0"
82
+ }
83
+ }
@@ -0,0 +1,51 @@
1
+ # maafw-live Skills
2
+
3
+ 本目录是本仓随 npm 包一起发布的 Agent Skill。每个 skill 一个目录,含 `SKILL.md` 与可选的 `references/`。
4
+
5
+ ## 安装
6
+
7
+ Skill 与 CLI 分开安装:装 CLI 不等于装 skill(反之亦然)。
8
+
9
+ ```bash
10
+ # CLI(skill 里所有命令都要它;装 scoped 包名,敲裸命令名)
11
+ npm install --global @windsland52/maafw-live@latest
12
+ maafw-live version
13
+
14
+ # skill(标准 skills CLI;列仓库里有哪些:--list)
15
+ npx skills add https://github.com/Windsland52/maafw-live --list
16
+ npx skills add https://github.com/Windsland52/maafw-live --skill maafw-live --global
17
+ ```
18
+
19
+ 本地开发时用 checkout 路径:`npx skills add . --skill maafw-live`(本地安装不跟随远端更新,改完重跑)。
20
+
21
+ ## Skill 与 CLI 的版本关系
22
+
23
+ Skill 随包发布但**不写自己的版本号**——写死的版本每次发版都要改,而且从仓库装的副本仍然对不上。
24
+ 所以对齐用**比字节**,CLI 自带四条命令:
25
+
26
+ ```bash
27
+ maafw-live skill # 包内 skill 位置与逐文件指纹
28
+ maafw-live skill --check <安装根目录> # 比对 <根>/maafw-live 与包内副本:same/different/missing/extra,有漂移退出 3
29
+ maafw-live skill --install <根目录> # 把包内副本逐字节写到 <根>/maafw-live(离线,不联网)
30
+ maafw-live skill --print [--format json] # 输出包内副本内容(harness 自取,不想用 skills CLI 时)
31
+ ```
32
+
33
+ `extra`(目标目录多出来的文件,例如 skills CLI 放进去的 agent 元数据)**不计漂移**;仅行尾不同(CRLF ↔ LF)
34
+ 仍算 `different`,但会标注成因,避免把工具改写误判成内容分叉。日常安装/更新仍推荐 skills CLI
35
+ (它维护各 agent 的目录与符号链接),`--install` 是给"自己管一份副本"的 harness 用的。
36
+
37
+ Skill 与文档冲突时,**以本仓 `docs/` 为准**:
38
+
39
+ | 要知道什么 | 唯一出处 |
40
+ | :--- | :--- |
41
+ | daemon 命令、参数、返回字段 | [`docs/daemon-protocol.md`](../docs/daemon-protocol.md) |
42
+ | 关键帧留存与引用语义(L0/L1/L2、升格、出处) | [`docs/keyframe-retention-contract-v0.md`](../docs/keyframe-retention-contract-v0.md) |
43
+ | CLI 命令面、退出码、JSON 信封 | [`README.md`](../README.md) |
44
+ | 字段/协议/版本的原始语义(不属于本工具) | MaaLLMWiki(版本锁定原文) |
45
+
46
+ ## 可用 Skill
47
+
48
+ ### `maafw-live`
49
+
50
+ MaaFramework 应用的实时观测、识别实测、模板资产与状态证据留存。设备在线时给确定性观测;
51
+ 设备不在那个画面上(或根本没连)时,从关键帧库留存帧离线裁出带出处的模板。