@zenalexa/unicli 0.225.0 → 0.225.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/AGENTS.md +4 -4
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/adapters/_electron/desktop-shared.d.ts.map +1 -1
  5. package/dist/adapters/_electron/desktop-shared.js +2 -1
  6. package/dist/adapters/_electron/desktop-shared.js.map +1 -1
  7. package/dist/adapters/_electron/shared.d.ts +6 -0
  8. package/dist/adapters/_electron/shared.d.ts.map +1 -1
  9. package/dist/adapters/_electron/shared.js +9 -0
  10. package/dist/adapters/_electron/shared.js.map +1 -1
  11. package/dist/adapters/antigravity/extra.js +4 -1
  12. package/dist/adapters/antigravity/extra.js.map +1 -1
  13. package/dist/adapters/chatwise/extra.js +4 -1
  14. package/dist/adapters/chatwise/extra.js.map +1 -1
  15. package/dist/adapters/codex/codex.js +3 -1
  16. package/dist/adapters/codex/codex.js.map +1 -1
  17. package/dist/adapters/codex/extra.js +4 -1
  18. package/dist/adapters/codex/extra.js.map +1 -1
  19. package/dist/adapters/codex/projects.d.ts.map +1 -1
  20. package/dist/adapters/codex/projects.js +3 -1
  21. package/dist/adapters/codex/projects.js.map +1 -1
  22. package/dist/adapters/cursor/cursor.js +6 -1
  23. package/dist/adapters/cursor/cursor.js.map +1 -1
  24. package/dist/adapters/discord-app/discord-app.js +10 -1
  25. package/dist/adapters/discord-app/discord-app.js.map +1 -1
  26. package/dist/adapters/notion-app/notion-app.js +11 -1
  27. package/dist/adapters/notion-app/notion-app.js.map +1 -1
  28. package/dist/adapters/spotify/api.js +36 -6
  29. package/dist/adapters/spotify/api.js.map +1 -1
  30. package/dist/adapters/xiaohongshu/browser-state.d.ts +2 -1
  31. package/dist/adapters/xiaohongshu/browser-state.d.ts.map +1 -1
  32. package/dist/adapters/xiaohongshu/browser-state.js +56 -2
  33. package/dist/adapters/xiaohongshu/browser-state.js.map +1 -1
  34. package/dist/adapters/xiaohongshu/feed.d.ts +24 -0
  35. package/dist/adapters/xiaohongshu/feed.d.ts.map +1 -0
  36. package/dist/adapters/xiaohongshu/feed.js +82 -0
  37. package/dist/adapters/xiaohongshu/feed.js.map +1 -0
  38. package/dist/browser/cdp-client.d.ts +5 -1
  39. package/dist/browser/cdp-client.d.ts.map +1 -1
  40. package/dist/browser/cdp-client.js +24 -16
  41. package/dist/browser/cdp-client.js.map +1 -1
  42. package/dist/browser/daemon.js +29 -7
  43. package/dist/browser/daemon.js.map +1 -1
  44. package/dist/browser/launcher.d.ts.map +1 -1
  45. package/dist/browser/launcher.js +22 -8
  46. package/dist/browser/launcher.js.map +1 -1
  47. package/dist/browser/local-profiles.d.ts +2 -0
  48. package/dist/browser/local-profiles.d.ts.map +1 -1
  49. package/dist/browser/local-profiles.js +42 -2
  50. package/dist/browser/local-profiles.js.map +1 -1
  51. package/dist/browser/page.d.ts +2 -2
  52. package/dist/browser/page.d.ts.map +1 -1
  53. package/dist/browser/page.js +2 -2
  54. package/dist/browser/page.js.map +1 -1
  55. package/dist/browser/protocol.d.ts +7 -0
  56. package/dist/browser/protocol.d.ts.map +1 -1
  57. package/dist/browser/protocol.js +5 -0
  58. package/dist/browser/protocol.js.map +1 -1
  59. package/dist/commands/browser/index.d.ts.map +1 -1
  60. package/dist/commands/browser/index.js +26 -4
  61. package/dist/commands/browser/index.js.map +1 -1
  62. package/dist/commands/do.d.ts +15 -13
  63. package/dist/commands/do.d.ts.map +1 -1
  64. package/dist/commands/do.js +36 -21
  65. package/dist/commands/do.js.map +1 -1
  66. package/dist/discovery/aliases.d.ts.map +1 -1
  67. package/dist/discovery/aliases.js +26 -0
  68. package/dist/discovery/aliases.js.map +1 -1
  69. package/dist/discovery/intents.d.ts +31 -4
  70. package/dist/discovery/intents.d.ts.map +1 -1
  71. package/dist/discovery/intents.js +166 -3
  72. package/dist/discovery/intents.js.map +1 -1
  73. package/dist/discovery/loader.d.ts.map +1 -1
  74. package/dist/discovery/loader.js +3 -0
  75. package/dist/discovery/loader.js.map +1 -1
  76. package/dist/discovery/search.d.ts.map +1 -1
  77. package/dist/discovery/search.js +10 -1
  78. package/dist/discovery/search.js.map +1 -1
  79. package/dist/engine/kernel/stages.d.ts.map +1 -1
  80. package/dist/engine/kernel/stages.js +14 -4
  81. package/dist/engine/kernel/stages.js.map +1 -1
  82. package/dist/engine/objective/catalog.d.ts +23 -0
  83. package/dist/engine/objective/catalog.d.ts.map +1 -0
  84. package/dist/engine/objective/catalog.js +42 -0
  85. package/dist/engine/objective/catalog.js.map +1 -0
  86. package/dist/engine/objective/delivery.d.ts +18 -0
  87. package/dist/engine/objective/delivery.d.ts.map +1 -0
  88. package/dist/engine/objective/delivery.js +64 -0
  89. package/dist/engine/objective/delivery.js.map +1 -0
  90. package/dist/engine/objective/index.d.ts +20 -0
  91. package/dist/engine/objective/index.d.ts.map +1 -0
  92. package/dist/engine/objective/index.js +20 -0
  93. package/dist/engine/objective/index.js.map +1 -0
  94. package/dist/engine/objective/media-playback.d.ts +17 -0
  95. package/dist/engine/objective/media-playback.d.ts.map +1 -0
  96. package/dist/engine/objective/media-playback.js +186 -0
  97. package/dist/engine/objective/media-playback.js.map +1 -0
  98. package/dist/engine/objective/output.d.ts +20 -0
  99. package/dist/engine/objective/output.d.ts.map +1 -0
  100. package/dist/engine/objective/output.js +88 -0
  101. package/dist/engine/objective/output.js.map +1 -0
  102. package/dist/engine/objective/planner.d.ts +17 -0
  103. package/dist/engine/objective/planner.d.ts.map +1 -0
  104. package/dist/engine/objective/planner.js +60 -0
  105. package/dist/engine/objective/planner.js.map +1 -0
  106. package/dist/engine/objective/types.d.ts +66 -0
  107. package/dist/engine/objective/types.d.ts.map +1 -0
  108. package/dist/engine/objective/types.js +16 -0
  109. package/dist/engine/objective/types.js.map +1 -0
  110. package/dist/engine/steps/browser-helpers.d.ts.map +1 -1
  111. package/dist/engine/steps/browser-helpers.js +34 -0
  112. package/dist/engine/steps/browser-helpers.js.map +1 -1
  113. package/dist/manifest-compact.txt +3 -2
  114. package/dist/manifest.json +42 -17
  115. package/package.json +3 -1
  116. package/server.json +2 -2
  117. package/skills/unicli/SKILL.md +1 -1
  118. package/skills/unicli-claude-code/SKILL.md +1 -1
  119. package/skills/unicli-hermes/SKILL.md +1 -1
  120. package/src/adapters/_electron/desktop-shared.ts +5 -1
  121. package/src/adapters/_electron/shared.ts +15 -0
  122. package/src/adapters/antigravity/extra.ts +10 -1
  123. package/src/adapters/chatwise/extra.ts +10 -1
  124. package/src/adapters/codex/codex.ts +6 -0
  125. package/src/adapters/codex/extra.ts +10 -1
  126. package/src/adapters/codex/projects.ts +9 -1
  127. package/src/adapters/cursor/cursor.ts +9 -0
  128. package/src/adapters/discord-app/discord-app.ts +16 -1
  129. package/src/adapters/macos/brightness.yaml +6 -3
  130. package/src/adapters/macos/calendar-list.yaml +9 -11
  131. package/src/adapters/macos/calendar-today.yaml +1 -1
  132. package/src/adapters/macos/safari-url.yaml +8 -4
  133. package/src/adapters/maoyan/hot.yaml +1 -1
  134. package/src/adapters/notion-app/notion-app.ts +17 -1
  135. package/src/adapters/spotify/api.ts +54 -8
  136. package/src/adapters/weibo/trending.yaml +2 -0
  137. package/src/adapters/xiaohongshu/browser-state.ts +59 -2
  138. package/src/adapters/xiaohongshu/feed.ts +103 -0
@@ -1,12 +1,20 @@
1
1
  import { cli, Strategy } from "../../registry.js";
2
- import { connectElectronApp } from "../_electron/shared.js";
2
+ import {
3
+ connectElectronApp,
4
+ electronAppCommandMeta,
5
+ } from "../_electron/shared.js";
3
6
  import { intArg } from "../_shared/browser-tools.js";
4
7
 
8
+ const CODEX_EXTRA_COMMAND_META = electronAppCommandMeta(
9
+ "src/adapters/codex/extra.ts",
10
+ );
11
+
5
12
  cli({
6
13
  site: "codex",
7
14
  name: "history",
8
15
  description: "List Codex desktop conversation threads",
9
16
  strategy: Strategy.PUBLIC,
17
+ ...CODEX_EXTRA_COMMAND_META,
10
18
  args: [{ name: "limit", type: "int", default: 20 }],
11
19
  columns: ["title"],
12
20
  func: async (_page, kwargs) => {
@@ -27,6 +35,7 @@ cli({
27
35
  name: "export",
28
36
  description: "Export the current Codex desktop thread as Markdown text",
29
37
  strategy: Strategy.PUBLIC,
38
+ ...CODEX_EXTRA_COMMAND_META,
30
39
  columns: ["content"],
31
40
  func: async () => {
32
41
  const page = await connectElectronApp("codex");
@@ -7,7 +7,14 @@
7
7
  */
8
8
 
9
9
  import { cli, Strategy } from "../../registry.js";
10
- import { connectElectronApp } from "../_electron/shared.js";
10
+ import {
11
+ connectElectronApp,
12
+ electronAppCommandMeta,
13
+ } from "../_electron/shared.js";
14
+
15
+ const CODEX_PROJECTS_COMMAND_META = electronAppCommandMeta(
16
+ "src/adapters/codex/projects.ts",
17
+ );
11
18
 
12
19
  interface CodexConversation {
13
20
  index: number;
@@ -141,6 +148,7 @@ cli({
141
148
  description: "List Codex projects and visible conversations from the sidebar",
142
149
  domain: "localhost",
143
150
  strategy: Strategy.PUBLIC,
151
+ ...CODEX_PROJECTS_COMMAND_META,
144
152
  browser: false,
145
153
  args: [
146
154
  {
@@ -9,9 +9,14 @@ import { writeFileSync } from "node:fs";
9
9
  import {
10
10
  registerAIChatCommands,
11
11
  connectElectronApp,
12
+ electronAppCommandMeta,
12
13
  } from "../_electron/shared.js";
13
14
  import { cli, Strategy } from "../../registry.js";
14
15
 
16
+ const CURSOR_COMMAND_META = electronAppCommandMeta(
17
+ "src/adapters/cursor/cursor.ts",
18
+ );
19
+
15
20
  registerAIChatCommands("cursor", {
16
21
  inputSelector:
17
22
  ".chat-input textarea, [data-testid='chat-input'] textarea, .composer-input textarea",
@@ -28,6 +33,7 @@ cli({
28
33
  name: "composer",
29
34
  description: "Open Cursor Composer mode with a prompt",
30
35
  strategy: Strategy.PUBLIC,
36
+ ...CURSOR_COMMAND_META,
31
37
  args: [
32
38
  {
33
39
  name: "prompt",
@@ -52,6 +58,7 @@ cli({
52
58
  name: "extract-code",
53
59
  description: "Extract code blocks from the last Cursor response",
54
60
  strategy: Strategy.PUBLIC,
61
+ ...CURSOR_COMMAND_META,
55
62
  func: async () => {
56
63
  const p = await connectElectronApp("cursor");
57
64
  const code = await p.evaluate(`
@@ -75,6 +82,7 @@ cli({
75
82
  name: "export",
76
83
  description: "Export the current Cursor conversation to a Markdown file",
77
84
  strategy: Strategy.PUBLIC,
85
+ ...CURSOR_COMMAND_META,
78
86
  args: [
79
87
  {
80
88
  name: "output",
@@ -117,6 +125,7 @@ cli({
117
125
  name: "history",
118
126
  description: "List recent chat sessions from the Cursor sidebar",
119
127
  strategy: Strategy.PUBLIC,
128
+ ...CURSOR_COMMAND_META,
120
129
  func: async () => {
121
130
  const p = await connectElectronApp("cursor");
122
131
  const items = (await p.evaluate(`
@@ -4,15 +4,23 @@
4
4
  * Commands: servers, channels, read, send, search, members, delete, status
5
5
  */
6
6
 
7
- import { connectElectronApp } from "../_electron/shared.js";
7
+ import {
8
+ connectElectronApp,
9
+ electronAppCommandMeta,
10
+ } from "../_electron/shared.js";
8
11
  import { cli, Strategy } from "../../registry.js";
9
12
 
13
+ const DISCORD_APP_COMMAND_META = electronAppCommandMeta(
14
+ "src/adapters/discord-app/discord-app.ts",
15
+ );
16
+
10
17
  // servers -- List Discord servers
11
18
  cli({
12
19
  site: "discord-app",
13
20
  name: "servers",
14
21
  description: "List Discord servers",
15
22
  strategy: Strategy.PUBLIC,
23
+ ...DISCORD_APP_COMMAND_META,
16
24
  func: async () => {
17
25
  const p = await connectElectronApp("discord-app");
18
26
  const servers = await p.evaluate(`
@@ -35,6 +43,7 @@ cli({
35
43
  name: "channels",
36
44
  description: "List channels in current server",
37
45
  strategy: Strategy.PUBLIC,
46
+ ...DISCORD_APP_COMMAND_META,
38
47
  func: async () => {
39
48
  const p = await connectElectronApp("discord-app");
40
49
  const channels = await p.evaluate(`
@@ -55,6 +64,7 @@ cli({
55
64
  name: "read",
56
65
  description: "Read recent messages",
57
66
  strategy: Strategy.PUBLIC,
67
+ ...DISCORD_APP_COMMAND_META,
58
68
  func: async () => {
59
69
  const p = await connectElectronApp("discord-app");
60
70
  const messages = await p.evaluate(`
@@ -79,6 +89,7 @@ cli({
79
89
  name: "send",
80
90
  description: "Send message in current channel",
81
91
  strategy: Strategy.PUBLIC,
92
+ ...DISCORD_APP_COMMAND_META,
82
93
  args: [
83
94
  {
84
95
  name: "message",
@@ -105,6 +116,7 @@ cli({
105
116
  name: "search",
106
117
  description: "Search Discord messages",
107
118
  strategy: Strategy.PUBLIC,
119
+ ...DISCORD_APP_COMMAND_META,
108
120
  args: [
109
121
  {
110
122
  name: "query",
@@ -138,6 +150,7 @@ cli({
138
150
  name: "members",
139
151
  description: "List server members",
140
152
  strategy: Strategy.PUBLIC,
153
+ ...DISCORD_APP_COMMAND_META,
141
154
  func: async () => {
142
155
  const p = await connectElectronApp("discord-app");
143
156
  const members = await p.evaluate(`
@@ -159,6 +172,7 @@ cli({
159
172
  name: "delete",
160
173
  description: "Delete a message by its ID in the active Discord channel",
161
174
  strategy: Strategy.PUBLIC,
175
+ ...DISCORD_APP_COMMAND_META,
162
176
  args: [
163
177
  {
164
178
  name: "message_id",
@@ -257,6 +271,7 @@ cli({
257
271
  name: "status",
258
272
  description: "Discord app status",
259
273
  strategy: Strategy.PUBLIC,
274
+ ...DISCORD_APP_COMMAND_META,
260
275
  func: async () => {
261
276
  const p = await connectElectronApp("discord-app");
262
277
  const title = await p.title();
@@ -15,11 +15,14 @@ pipeline:
15
15
  - |
16
16
  const app = Application.currentApplication();
17
17
  app.includeStandardAdditions = true;
18
- const result = app.doShellScript('ioreg -c AppleBacklightDisplay | grep brightness');
19
- JSON.stringify({ brightness: result });
18
+ const raw = app.doShellScript('ioreg -r -c AppleBacklightDisplay -k brightness 2>/dev/null || true');
19
+ const current = (raw.match(/"brightness"\\s*=\\s*(\\d+)/) || [])[1] || '';
20
+ const max = (raw.match(/"max-brightness"\\s*=\\s*(\\d+)/) || [])[1] || '';
21
+ const percent = current && max ? Math.round(Number(current) / Number(max) * 100) : null;
22
+ JSON.stringify([{ brightness: percent, raw: raw ? raw.split('\\n').slice(0, 8).join('\\n') : '', status: percent === null ? 'unavailable' : 'ok' }]);
20
23
  parse: json
21
24
 
22
- columns: [brightness]
25
+ columns: [brightness, status]
23
26
 
24
27
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
25
28
  capabilities: ["subprocess.exec"]
@@ -28,20 +28,18 @@ pipeline:
28
28
  var calendars = cal.calendars();
29
29
  var result = [];
30
30
  for (var c = 0; c < calendars.length; c++) {
31
- var events = calendars[c].events();
31
+ var events = calendars[c].events.whose({startDate: {_greaterThan: now, _lessThan: end}})();
32
32
  for (var e = 0; e < events.length; e++) {
33
33
  var ev = events[e];
34
34
  var start = ev.startDate();
35
- if (start >= now && start <= end) {
36
- result.push({
37
- calendar: calendars[c].name(),
38
- title: ev.summary(),
39
- start: start.toISOString(),
40
- end: ev.endDate().toISOString(),
41
- location: ev.location() || '',
42
- notes: (ev.description() || '').substring(0, 200)
43
- });
44
- }
35
+ result.push({
36
+ calendar: calendars[c].name(),
37
+ title: ev.summary(),
38
+ start: start.toISOString(),
39
+ end: ev.endDate().toISOString(),
40
+ location: ev.location() || '',
41
+ notes: (ev.description() || '').substring(0, 200)
42
+ });
45
43
  }
46
44
  }
47
45
  result.sort(function(a, b) { return a.start < b.start ? -1 : 1; });
@@ -19,7 +19,7 @@ pipeline:
19
19
  const end = new Date(start.getTime() + 86400000);
20
20
  const out = [];
21
21
  for (const cal of app.calendars()) {
22
- const evts = cal.events.whose({startDate: {_greaterThan: start}, startDate: {_lessThan: end}})();
22
+ const evts = cal.events.whose({startDate: {_greaterThan: start, _lessThan: end}})();
23
23
  for (const e of evts) {
24
24
  out.push({ calendar: cal.name(), summary: e.summary(), start: e.startDate().toISOString(), end: e.endDate().toISOString(), location: e.location() || '' });
25
25
  }
@@ -14,12 +14,16 @@ pipeline:
14
14
  - "-e"
15
15
  - |
16
16
  const app = Application('Safari');
17
- const win = app.windows[0];
18
- const tab = win.currentTab();
19
- JSON.stringify([{ url: tab.url(), title: tab.name() }]);
17
+ const windows = app.windows();
18
+ if (windows.length === 0) {
19
+ JSON.stringify([{ state: 'no_window', url: '', title: '' }]);
20
+ } else {
21
+ const tab = windows[0].currentTab();
22
+ JSON.stringify([{ state: 'ok', url: tab.url(), title: tab.name() }]);
23
+ }
20
24
  parse: json
21
25
 
22
- columns: [url, title]
26
+ columns: [state, url, title]
23
27
 
24
28
  # schema-v2 metadata — injected by `unicli migrate schema-v2`
25
29
  capabilities: ["subprocess.exec"]
@@ -19,7 +19,7 @@ pipeline:
19
19
  headers:
20
20
  Referer: https://piaofang.maoyan.com/dashboard
21
21
 
22
- - select: movieList.data.list
22
+ - select: movieList.list
23
23
 
24
24
  - map:
25
25
  rank: ${{ index + 1 }}
@@ -7,15 +7,23 @@
7
7
  * Commands: search, read, write, new, status, sidebar, favorites, export, screenshot
8
8
  */
9
9
 
10
- import { connectElectronApp } from "../_electron/shared.js";
10
+ import {
11
+ connectElectronApp,
12
+ electronAppCommandMeta,
13
+ } from "../_electron/shared.js";
11
14
  import { cli, Strategy } from "../../registry.js";
12
15
 
16
+ const NOTION_COMMAND_META = electronAppCommandMeta(
17
+ "src/adapters/notion-app/notion-app.ts",
18
+ );
19
+
13
20
  // search -- Quick-find via Cmd+K
14
21
  cli({
15
22
  site: "notion",
16
23
  name: "search",
17
24
  description: "Search in Notion (Cmd+K)",
18
25
  strategy: Strategy.PUBLIC,
26
+ ...NOTION_COMMAND_META,
19
27
  args: [
20
28
  {
21
29
  name: "query",
@@ -50,6 +58,7 @@ cli({
50
58
  name: "read",
51
59
  description: "Read current Notion page content",
52
60
  strategy: Strategy.PUBLIC,
61
+ ...NOTION_COMMAND_META,
53
62
  func: async () => {
54
63
  const p = await connectElectronApp("notion");
55
64
  const content = await p.evaluate(`
@@ -66,6 +75,7 @@ cli({
66
75
  name: "write",
67
76
  description: "Append text to current Notion page",
68
77
  strategy: Strategy.PUBLIC,
78
+ ...NOTION_COMMAND_META,
69
79
  args: [
70
80
  {
71
81
  name: "text",
@@ -97,6 +107,7 @@ cli({
97
107
  name: "new",
98
108
  description: "Create new Notion page",
99
109
  strategy: Strategy.PUBLIC,
110
+ ...NOTION_COMMAND_META,
100
111
  args: [
101
112
  {
102
113
  name: "title",
@@ -121,6 +132,7 @@ cli({
121
132
  name: "status",
122
133
  description: "Notion workspace status",
123
134
  strategy: Strategy.PUBLIC,
135
+ ...NOTION_COMMAND_META,
124
136
  func: async () => {
125
137
  const p = await connectElectronApp("notion");
126
138
  const title = await p.title();
@@ -134,6 +146,7 @@ cli({
134
146
  name: "sidebar",
135
147
  description: "Read Notion sidebar navigation",
136
148
  strategy: Strategy.PUBLIC,
149
+ ...NOTION_COMMAND_META,
137
150
  func: async () => {
138
151
  const p = await connectElectronApp("notion");
139
152
  const items = await p.evaluate(`
@@ -156,6 +169,7 @@ cli({
156
169
  name: "favorites",
157
170
  description: "List Notion favorites",
158
171
  strategy: Strategy.PUBLIC,
172
+ ...NOTION_COMMAND_META,
159
173
  func: async () => {
160
174
  const p = await connectElectronApp("notion");
161
175
  const items = await p.evaluate(`
@@ -180,6 +194,7 @@ cli({
180
194
  name: "export",
181
195
  description: "Export current Notion page as markdown",
182
196
  strategy: Strategy.PUBLIC,
197
+ ...NOTION_COMMAND_META,
183
198
  func: async () => {
184
199
  const p = await connectElectronApp("notion");
185
200
  const content = await p.evaluate(`
@@ -195,6 +210,7 @@ cli({
195
210
  name: "screenshot",
196
211
  description: "Screenshot current Notion page",
197
212
  strategy: Strategy.PUBLIC,
213
+ ...NOTION_COMMAND_META,
198
214
  args: [
199
215
  {
200
216
  name: "path",
@@ -16,6 +16,12 @@ interface SpotifyConfig {
16
16
  SPOTIFY_REDIRECT_URI?: string;
17
17
  }
18
18
 
19
+ interface SpotifyTrack {
20
+ uri: string;
21
+ name: string;
22
+ artist: string;
23
+ }
24
+
19
25
  const TOKEN_PATH = join(homedir(), ".unicli", "spotify-tokens.json");
20
26
  const ENV_PATH = join(homedir(), ".unicli", "spotify.env");
21
27
 
@@ -113,13 +119,26 @@ async function spotifyApi(
113
119
  return response.json();
114
120
  }
115
121
 
116
- async function searchTrack(query: string): Promise<string> {
122
+ async function searchTrack(query: string): Promise<SpotifyTrack> {
117
123
  const data = (await spotifyApi(
118
124
  `/search?type=track&limit=1&q=${encodeURIComponent(query)}`,
119
- )) as { tracks?: { items?: Array<{ uri?: string }> } };
120
- const uri = data.tracks?.items?.[0]?.uri;
121
- if (!uri) throw new Error(`No Spotify track found for query: ${query}`);
122
- return uri;
125
+ )) as {
126
+ tracks?: {
127
+ items?: Array<{
128
+ uri?: string;
129
+ name?: string;
130
+ artists?: Array<{ name?: string }>;
131
+ }>;
132
+ };
133
+ };
134
+ const track = data.tracks?.items?.[0];
135
+ if (!track?.uri)
136
+ throw new Error(`No Spotify track found for query: ${query}`);
137
+ return {
138
+ uri: track.uri,
139
+ name: track.name ?? "",
140
+ artist: track.artists?.map((artist) => artist.name).join(", ") ?? "",
141
+ };
123
142
  }
124
143
 
125
144
  cli({
@@ -235,11 +254,38 @@ cli({
235
254
  args: [{ name: "query", type: "str", required: true, positional: true }],
236
255
  columns: ["ok", "uri"],
237
256
  func: async (_page, kwargs) => {
238
- const uri = await searchTrack(str(kwargs.query));
239
- await spotifyApi(`/me/player/queue?uri=${encodeURIComponent(uri)}`, {
257
+ const track = await searchTrack(str(kwargs.query));
258
+ await spotifyApi(`/me/player/queue?uri=${encodeURIComponent(track.uri)}`, {
240
259
  method: "POST",
241
260
  });
242
- return [{ ok: true, uri }];
261
+ return [{ ok: true, uri: track.uri }];
262
+ },
263
+ });
264
+
265
+ cli({
266
+ site: "spotify",
267
+ name: "play-track",
268
+ description: "Search Spotify for a track query and start playback",
269
+ domain: "api.spotify.com",
270
+ strategy: Strategy.COOKIE,
271
+ args: [{ name: "query", type: "str", required: true, positional: true }],
272
+ columns: ["ok", "query", "track", "artist", "uri"],
273
+ func: async (_page, kwargs) => {
274
+ const query = str(kwargs.query);
275
+ const track = await searchTrack(query);
276
+ await spotifyApi("/me/player/play", {
277
+ method: "PUT",
278
+ body: JSON.stringify({ uris: [track.uri] }),
279
+ });
280
+ return [
281
+ {
282
+ ok: true,
283
+ query,
284
+ track: track.name,
285
+ artist: track.artist,
286
+ uri: track.uri,
287
+ },
288
+ ];
243
289
  },
244
290
  });
245
291
 
@@ -14,6 +14,8 @@ args:
14
14
  pipeline:
15
15
  - fetch:
16
16
  url: https://weibo.com/ajax/side/hotSearch
17
+ headers:
18
+ Referer: https://weibo.com/
17
19
 
18
20
  - select: data.realtime
19
21
 
@@ -2,7 +2,7 @@
2
2
  * @owner Xiaohongshu browser adapters.
3
3
  * @does Detects login, risk-control, and rendered-feed state in XHS web pages.
4
4
  * @needs Browser-backed IPage from Uni-CLI runtime.
5
- * @feeds xiaohongshu.search and xiaohongshu.trending.
5
+ * @feeds xiaohongshu.feed, xiaohongshu.search, and xiaohongshu.trending.
6
6
  * @breaks XHS copy or route changes can require updating page-state detection.
7
7
  */
8
8
 
@@ -62,7 +62,7 @@ export async function assertXhsReadable(
62
62
  assertXhsReadableState(command, await readXhsPageState(page));
63
63
  }
64
64
 
65
- export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
65
+ async function fetchXhsStoreFeedItems(page: IPage): Promise<unknown[]> {
66
66
  const raw = await page.evaluate(`
67
67
  (async () => {
68
68
  const app = document.querySelector('#app')?.__vue_app__;
@@ -93,3 +93,60 @@ export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
93
93
  `);
94
94
  return Array.isArray(raw) ? raw : [];
95
95
  }
96
+
97
+ export async function fetchXhsVisibleFeedItems(
98
+ page: IPage,
99
+ ): Promise<unknown[]> {
100
+ const raw = await page.evaluate(`
101
+ (() => {
102
+ const cleanText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
103
+ const normalizeUrl = (href) => {
104
+ if (!href) return '';
105
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
106
+ if (href.startsWith('/')) return 'https://www.xiaohongshu.com' + href;
107
+ return '';
108
+ };
109
+ const noteIdFromUrl = (url) => {
110
+ const match = url.match(/\\/(?:explore|search_result|note)\\/([^?#/]+)/i);
111
+ return match ? match[1] : '';
112
+ };
113
+ const rows = [];
114
+ const seen = new Set();
115
+ document.querySelectorAll('section.note-item, .note-item').forEach((el) => {
116
+ const link =
117
+ el.querySelector('a[href*="/explore/"]') ||
118
+ el.querySelector('a[href*="/search_result/"]') ||
119
+ el.querySelector('a[href*="/note/"]');
120
+ const url = normalizeUrl(link?.getAttribute('href') || '');
121
+ const id = noteIdFromUrl(url);
122
+ if (!id || seen.has(id)) return;
123
+ seen.add(id);
124
+
125
+ const titleEl = el.querySelector('.title, .note-title, a.title, .footer .title span');
126
+ const authorEl = el.querySelector('a.author .name, .name, .author-name, .nick-name, a.author');
127
+ const likesEl = el.querySelector('.count, .like-count, .like-wrapper .count');
128
+ const isVideo =
129
+ !!el.querySelector('video, .play-icon, .video-icon') ||
130
+ /视频/.test(cleanText(el.textContent));
131
+
132
+ rows.push({
133
+ id,
134
+ note_card: {
135
+ display_title: cleanText(titleEl?.textContent || link?.textContent || ''),
136
+ type: isVideo ? 'video' : 'normal',
137
+ user: { nickname: cleanText(authorEl?.textContent || '') },
138
+ interact_info: { liked_count: cleanText(likesEl?.textContent || '0') },
139
+ },
140
+ });
141
+ });
142
+ return rows;
143
+ })()
144
+ `);
145
+ return Array.isArray(raw) ? raw : [];
146
+ }
147
+
148
+ export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
149
+ const storeItems = await fetchXhsStoreFeedItems(page).catch(() => []);
150
+ if (storeItems.length > 0) return storeItems;
151
+ return fetchXhsVisibleFeedItems(page);
152
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @owner src/adapters/xiaohongshu/feed.ts
3
+ * @does Register Xiaohongshu home-feed extraction over a logged-in browser page.
4
+ * @needs Browser-backed IPage, XHS readable-state checks, visible/store feed extraction.
5
+ * @feeds xiaohongshu.feed command.
6
+ * @breaks XHS feed route or note-card DOM drift returns structured empty_result.
7
+ * @invariants Rows expose stable note title, author, likes, type, and canonical note URL.
8
+ * @side-effects Performs authenticated read navigation to Xiaohongshu explore.
9
+ * @perf One page navigation plus at most one visible DOM extraction per invocation.
10
+ * @concurrency Stateless per invocation.
11
+ * @test tests/unit/xiaohongshu-feed.test.ts
12
+ * @stability stable
13
+ * @since 2026-06-02
14
+ */
15
+
16
+ import { cli, Strategy } from "../../registry.js";
17
+ import type { IPage } from "../../types.js";
18
+ import { socialEmptyError } from "../../social/browser-errors.js";
19
+ import { assertXhsReadable, fetchXhsFeedItems } from "./browser-state.js";
20
+
21
+ export interface XhsFeedRow {
22
+ id: string;
23
+ title: string;
24
+ author: string;
25
+ likes: string;
26
+ type: string;
27
+ url: string;
28
+ }
29
+
30
+ function asRecord(value: unknown): Record<string, unknown> {
31
+ return value && typeof value === "object"
32
+ ? (value as Record<string, unknown>)
33
+ : {};
34
+ }
35
+
36
+ function cleanText(value: unknown): string {
37
+ return String(value ?? "")
38
+ .replace(/\s+/g, " ")
39
+ .trim();
40
+ }
41
+
42
+ export function normalizeXhsFeedRows(
43
+ items: unknown[],
44
+ limit: number,
45
+ ): XhsFeedRow[] {
46
+ return items
47
+ .map((item) => {
48
+ const root = asRecord(item);
49
+ const note = asRecord(root.note_card);
50
+ const user = asRecord(note.user);
51
+ const interact = asRecord(note.interact_info);
52
+ const id = cleanText(root.id);
53
+ const title = cleanText(note.display_title);
54
+ if (!id || !title) return null;
55
+ return {
56
+ id,
57
+ title,
58
+ type: cleanText(note.type) || "normal",
59
+ author: cleanText(user.nickname),
60
+ likes: cleanText(interact.liked_count),
61
+ url: `https://www.xiaohongshu.com/explore/${id}`,
62
+ };
63
+ })
64
+ .filter((row): row is XhsFeedRow => row !== null)
65
+ .slice(0, limit);
66
+ }
67
+
68
+ cli({
69
+ site: "xiaohongshu",
70
+ name: "feed",
71
+ description: "Xiaohongshu home feed",
72
+ domain: "www.xiaohongshu.com",
73
+ strategy: Strategy.COOKIE,
74
+ browser: true,
75
+ browserSession: "user",
76
+ args: [
77
+ {
78
+ name: "limit",
79
+ type: "int",
80
+ default: 20,
81
+ description: "Number of items to return",
82
+ },
83
+ ],
84
+ columns: ["title", "author", "likes", "type", "url"],
85
+ capabilities: ["cdp-browser.navigate", "cdp-browser.evaluate"],
86
+ minimum_capability: "cdp-browser.evaluate",
87
+ async func(page, kwargs) {
88
+ const p = page as IPage;
89
+ const limit = Number(kwargs.limit) || 20;
90
+ await p.goto("https://www.xiaohongshu.com/explore", { settleMs: 2500 });
91
+ await p.wait(2);
92
+ await assertXhsReadable(p, "feed");
93
+
94
+ const rows = normalizeXhsFeedRows(await fetchXhsFeedItems(p), limit);
95
+ if (rows.length > 0) return rows;
96
+
97
+ throw socialEmptyError(
98
+ "xiaohongshu",
99
+ "feed",
100
+ "Xiaohongshu explore loaded no parseable feed rows.",
101
+ );
102
+ },
103
+ });