latent-opencode 0.1.0-alpha.0 → 0.1.0-alpha.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 (3) hide show
  1. package/dist/tui.js +225 -0
  2. package/package.json +17 -5
  3. package/src/tui.tsx +0 -179
package/dist/tui.js ADDED
@@ -0,0 +1,225 @@
1
+ import { effect as _$effect } from "@opentui/solid";
2
+ import { memo as _$memo } from "@opentui/solid";
3
+ import { insert as _$insert } from "@opentui/solid";
4
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
5
+ import { insertNode as _$insertNode } from "@opentui/solid";
6
+ import { setProp as _$setProp } from "@opentui/solid";
7
+ import { createElement as _$createElement } from "@opentui/solid";
8
+ /**
9
+ * Latent for OpenCode — a TUI-only plugin.
10
+ *
11
+ * One labelled sponsored line in the prompt footer while the agent works,
12
+ * billed by the same AdController as LatentCode (10 s view floor, one
13
+ * impression per ad, tracked clicks, kill switch). It only draws in the
14
+ * terminal: nothing here reads or rewrites the model's request or reply, and
15
+ * only the local classifier's slug leaves the machine.
16
+ *
17
+ * opencode plugin add latent-opencode
18
+ * /latent status, wallet, on/off
19
+ * /latent-login connect the wallet Earn pays into (shared ~/.latent-protocol)
20
+ * /latent-open open the sponsored link (tracked redirect)
21
+ */
22
+ import { Plugin } from "@opencode/plugin/tui";
23
+ import { createSignal } from "solid-js";
24
+ import { randomUUID } from "node:crypto";
25
+ import { AGENT_OPENCODE, AdController, FocusTracker, adBadge, adsEnabled, httpAdApi, loadConfig, loginWithAuthLink, openUrl, resolveServer, resolveWallet, saveConfig } from "./runtime.js";
26
+ const TICK_MS = 250;
27
+ const EXECUTION_END = ["succeeded", "failed", "interrupted"];
28
+ function short(w) {
29
+ return `${w.slice(0, 6)}…${w.slice(-4)}`;
30
+ }
31
+
32
+ /** The session id of an execution event, whatever envelope the host passes. */
33
+ function sessionOf(e) {
34
+ return e?.data?.sessionID ?? e?.details?.data?.sessionID ?? e?.sessionID ?? "default";
35
+ }
36
+ export default Plugin.define({
37
+ id: "latent.ads",
38
+ setup(context) {
39
+ const server = resolveServer();
40
+ const api = httpAdApi(server, {
41
+ agent: AGENT_OPENCODE
42
+ });
43
+ const cfg = loadConfig();
44
+ const controller = new AdController({
45
+ api,
46
+ wallet: resolveWallet(cfg),
47
+ enabled: adsEnabled(cfg, "opencode_ads"),
48
+ sessionId: randomUUID(),
49
+ // A plugin does not own stdin, so terminal focus is unknown: dwell counts
50
+ // as focused and impressions say focus_tracked=false.
51
+ focus: new FocusTracker(),
52
+ cwd: context.location?.directory
53
+ });
54
+ const [snap, setSnap] = createSignal(controller.snapshot());
55
+ const unsubscribe = controller.subscribe(() => setSnap(controller.snapshot()));
56
+ const timer = setInterval(() => controller.tick(), TICK_MS);
57
+
58
+ // Working = at least one session executing.
59
+ const running = new Set();
60
+ const sync = () => controller.setWorking(running.size > 0);
61
+ const offs = [context.data.on("session.execution.started", e => {
62
+ running.add(sessionOf(e));
63
+ sync();
64
+ }), ...EXECUTION_END.map(k => context.data.on(`session.execution.${k}`, e => {
65
+ running.delete(sessionOf(e));
66
+ sync();
67
+ }))];
68
+ const t = context.theme;
69
+ const muted = t?.text?.muted ?? t?.text?.base;
70
+ const sponsoredLine = () => {
71
+ const s = snap();
72
+ if (s.status === "no-wallet") return (() => {
73
+ var _el$ = _$createElement("text");
74
+ _$insertNode(_el$, _$createTextNode(`◆ Latent: /latent-login to earn while you wait`));
75
+ _$setProp(_el$, "fg", muted);
76
+ return _el$;
77
+ })();
78
+ const ad = s.current;
79
+ if (s.status !== "ok" || !ad) return null;
80
+ const badge = adBadge(ad.title || ad.body);
81
+ return (() => {
82
+ var _el$3 = _$createElement("text"),
83
+ _el$4 = _$createTextNode(`◆ Sponsored │ `),
84
+ _el$5 = _$createElement("span"),
85
+ _el$6 = _$createTextNode(` `);
86
+ _$insertNode(_el$3, _el$4);
87
+ _$insertNode(_el$3, _el$5);
88
+ _$insertNode(_el$3, _el$6);
89
+ _$setProp(_el$3, "fg", muted);
90
+ _$insert(_el$5, () => ` ${badge.letter} `);
91
+ _$insert(_el$3, () => ad.title || ad.body, null);
92
+ _$insert(_el$3, (() => {
93
+ var _c$ = _$memo(() => !!(ad.title && ad.body));
94
+ return () => _c$() ? ` — ${ad.body}` : "";
95
+ })(), null);
96
+ _$insert(_el$3, (() => {
97
+ var _c$2 = _$memo(() => !!ad.shownUrl);
98
+ return () => _c$2() ? ` · ${ad.shownUrl}` : "";
99
+ })(), null);
100
+ _$insert(_el$3, (() => {
101
+ var _c$3 = _$memo(() => !!ad.link);
102
+ return () => _c$3() ? ` · /latent-open ${ad.ctaText}` : "";
103
+ })(), null);
104
+ _$insert(_el$3, (() => {
105
+ var _c$4 = _$memo(() => !!(s.billed && ad.earn > 0));
106
+ return () => _c$4() ? ` +$${ad.earn.toFixed(4)}` : "";
107
+ })(), null);
108
+ _$effect(_$p => _$setProp(_el$5, "style", {
109
+ bg: badge.color,
110
+ fg: "#111827"
111
+ }, _$p));
112
+ return _el$3;
113
+ })();
114
+ };
115
+ // Called inside a JSX expression so Solid re-runs it whenever the snapshot changes
116
+ // (a component body would run once and never update).
117
+ const unslot = context.ui.slot({
118
+ append: "prompt.footer.status",
119
+ render: () => _$memo(sponsoredLine)
120
+ });
121
+ let loggingIn = false;
122
+ const login = async () => {
123
+ if (loggingIn) return;
124
+ loggingIn = true;
125
+ try {
126
+ const result = await loginWithAuthLink({
127
+ server,
128
+ log: () => {},
129
+ onLink: ({
130
+ url,
131
+ code
132
+ }) => {
133
+ void context.ui.dialog.alert({
134
+ title: "Latent login",
135
+ message: `Open ${url}\nCode: ${code}`
136
+ });
137
+ },
138
+ open: openUrl,
139
+ load: loadConfig,
140
+ save: patch => void saveConfig(patch)
141
+ });
142
+ context.ui.dialog.clear();
143
+ controller.setWallet(result.wallet);
144
+ context.ui.toast.show({
145
+ title: "Latent",
146
+ message: `Earning into ${short(result.wallet)}`,
147
+ variant: "success"
148
+ });
149
+ } catch (e) {
150
+ context.ui.toast.show({
151
+ title: "Latent",
152
+ message: e instanceof Error ? e.message : String(e),
153
+ variant: "error"
154
+ });
155
+ } finally {
156
+ loggingIn = false;
157
+ }
158
+ };
159
+ const status = async () => {
160
+ const s = controller.snapshot();
161
+ const wallet = resolveWallet(loadConfig());
162
+ const balance = wallet ? await api.balance(wallet) : null;
163
+ const choice = await context.ui.dialog.select({
164
+ title: wallet ? `Latent — ${short(wallet)}${balance !== null ? ` · $${balance.toFixed(4)}` : ""}` : "Latent — no wallet",
165
+ options: [{
166
+ title: `Sponsored line: ${s.status === "disabled" ? "off" : "on"}`,
167
+ value: "toggle",
168
+ description: `${s.impressions} paid views this session`
169
+ }, {
170
+ title: wallet ? "Switch wallet" : "Connect wallet",
171
+ value: "login"
172
+ }]
173
+ });
174
+ if (choice === "toggle") {
175
+ const on = s.status === "disabled";
176
+ saveConfig({
177
+ opencode_ads: on
178
+ });
179
+ controller.setEnabled(on);
180
+ } else if (choice === "login") void login();
181
+ };
182
+ context.keymap.layer(() => ({
183
+ mode: "global",
184
+ commands: [{
185
+ id: "latent.status",
186
+ title: "Latent: status / on-off",
187
+ group: "Latent",
188
+ palette: true,
189
+ slash: {
190
+ name: "latent"
191
+ },
192
+ run: () => void status()
193
+ }, {
194
+ id: "latent.login",
195
+ title: "Latent: connect wallet",
196
+ group: "Latent",
197
+ palette: true,
198
+ slash: {
199
+ name: "latent-login"
200
+ },
201
+ run: () => void login()
202
+ }, {
203
+ id: "latent.open",
204
+ title: "Latent: open sponsored link",
205
+ group: "Latent",
206
+ palette: true,
207
+ slash: {
208
+ name: "latent-open"
209
+ },
210
+ run: () => {
211
+ const link = controller.click();
212
+ if (link) openUrl(link);else context.ui.toast.show({
213
+ message: "No sponsored line on screen"
214
+ });
215
+ }
216
+ }]
217
+ }));
218
+ return () => {
219
+ clearInterval(timer);
220
+ unsubscribe();
221
+ offs.forEach(off => off());
222
+ unslot?.();
223
+ };
224
+ }
225
+ });
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "latent-opencode",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "Earn from one labelled sponsored line in your own OpenCode terminal UI. Never reads or changes the model's request or reply.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
8
- "./tui": "./src/tui.tsx"
8
+ "./tui": "./dist/tui.js"
9
9
  },
10
- "files": ["src/tui.tsx", "dist/runtime.js", "README.md"],
10
+ "files": [
11
+ "dist/tui.js",
12
+ "dist/runtime.js",
13
+ "README.md"
14
+ ],
11
15
  "scripts": {
12
16
  "build": "node scripts/build.mjs",
13
17
  "prepublishOnly": "npm run build"
@@ -20,6 +24,14 @@
20
24
  "@opentui/solid": ">=0.6.6",
21
25
  "solid-js": ">=1.9.0"
22
26
  },
23
- "repository": { "type": "git", "url": "git+https://github.com/enzoonchain/latentcode.git", "directory": "opencode-plugin" },
24
- "keywords": ["opencode", "opencode-plugin", "latent"]
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/enzoonchain/latentcode.git",
30
+ "directory": "opencode-plugin"
31
+ },
32
+ "keywords": [
33
+ "opencode",
34
+ "opencode-plugin",
35
+ "latent"
36
+ ]
25
37
  }
package/src/tui.tsx DELETED
@@ -1,179 +0,0 @@
1
- /**
2
- * Latent for OpenCode — a TUI-only plugin.
3
- *
4
- * One labelled sponsored line in the prompt footer while the agent works,
5
- * billed by the same AdController as LatentCode (10 s view floor, one
6
- * impression per ad, tracked clicks, kill switch). It only draws in the
7
- * terminal: nothing here reads or rewrites the model's request or reply, and
8
- * only the local classifier's slug leaves the machine.
9
- *
10
- * opencode plugin add latent-opencode
11
- * /latent status, wallet, on/off
12
- * /latent-login connect the wallet Earn pays into (shared ~/.latent-protocol)
13
- * /latent-open open the sponsored link (tracked redirect)
14
- */
15
- import { Plugin } from "@opencode/plugin/tui";
16
- import { createSignal } from "solid-js";
17
- import { randomUUID } from "node:crypto";
18
- import {
19
- AGENT_OPENCODE,
20
- AdController,
21
- FocusTracker,
22
- adBadge,
23
- adsEnabled,
24
- httpAdApi,
25
- loadConfig,
26
- loginWithAuthLink,
27
- openUrl,
28
- resolveServer,
29
- resolveWallet,
30
- saveConfig,
31
- type AdSnapshot,
32
- } from "../dist/runtime.js";
33
-
34
- const TICK_MS = 250;
35
- const EXECUTION_END = ["succeeded", "failed", "interrupted"] as const;
36
-
37
- function short(w: string): string {
38
- return `${w.slice(0, 6)}…${w.slice(-4)}`;
39
- }
40
-
41
- /** The session id of an execution event, whatever envelope the host passes. */
42
- function sessionOf(e: any): string {
43
- return e?.data?.sessionID ?? e?.details?.data?.sessionID ?? e?.sessionID ?? "default";
44
- }
45
-
46
- export default Plugin.define({
47
- id: "latent.ads",
48
- setup(context) {
49
- const server = resolveServer();
50
- const api = httpAdApi(server, { agent: AGENT_OPENCODE });
51
- const cfg = loadConfig();
52
- const controller = new AdController({
53
- api,
54
- wallet: resolveWallet(cfg),
55
- enabled: adsEnabled(cfg, "opencode_ads"),
56
- sessionId: randomUUID(),
57
- // A plugin does not own stdin, so terminal focus is unknown: dwell counts
58
- // as focused and impressions say focus_tracked=false.
59
- focus: new FocusTracker(),
60
- cwd: context.location?.directory,
61
- });
62
-
63
- const [snap, setSnap] = createSignal<AdSnapshot>(controller.snapshot());
64
- const unsubscribe = controller.subscribe(() => setSnap(controller.snapshot()));
65
- const timer = setInterval(() => controller.tick(), TICK_MS);
66
-
67
- // Working = at least one session executing.
68
- const running = new Set<string>();
69
- const sync = () => controller.setWorking(running.size > 0);
70
- const offs = [
71
- context.data.on("session.execution.started" as any, (e: any) => {
72
- running.add(sessionOf(e));
73
- sync();
74
- }),
75
- ...EXECUTION_END.map((k) =>
76
- context.data.on(`session.execution.${k}` as any, (e: any) => {
77
- running.delete(sessionOf(e));
78
- sync();
79
- }),
80
- ),
81
- ];
82
-
83
- const t = context.theme as any;
84
- const muted = t?.text?.muted ?? t?.text?.base;
85
- const sponsoredLine = () => {
86
- const s = snap();
87
- if (s.status === "no-wallet") return <text fg={muted}>◆ Latent: /latent-login to earn while you wait</text>;
88
- const ad = s.current;
89
- if (s.status !== "ok" || !ad) return null;
90
- const badge = adBadge(ad.title || ad.body);
91
- return (
92
- <text fg={muted}>
93
- ◆ Sponsored │ <span style={{ bg: badge.color, fg: "#111827" }}>{` ${badge.letter} `}</span> {ad.title || ad.body}
94
- {ad.title && ad.body ? ` — ${ad.body}` : ""}
95
- {ad.shownUrl ? ` · ${ad.shownUrl}` : ""}
96
- {ad.link ? ` · /latent-open ${ad.ctaText}` : ""}
97
- {s.billed && ad.earn > 0 ? ` +$${ad.earn.toFixed(4)}` : ""}
98
- </text>
99
- );
100
- };
101
- // Called inside a JSX expression so Solid re-runs it whenever the snapshot changes
102
- // (a component body would run once and never update).
103
- const unslot = context.ui.slot({ append: "prompt.footer.status", render: () => <>{sponsoredLine()}</> });
104
-
105
- let loggingIn = false;
106
- const login = async () => {
107
- if (loggingIn) return;
108
- loggingIn = true;
109
- try {
110
- const result = await loginWithAuthLink({
111
- server,
112
- log: () => {},
113
- onLink: ({ url, code }) => {
114
- void context.ui.dialog.alert({ title: "Latent login", message: `Open ${url}\nCode: ${code}` });
115
- },
116
- open: openUrl,
117
- load: loadConfig,
118
- save: (patch) => void saveConfig(patch),
119
- });
120
- context.ui.dialog.clear();
121
- controller.setWallet(result.wallet);
122
- context.ui.toast.show({ title: "Latent", message: `Earning into ${short(result.wallet)}`, variant: "success" });
123
- } catch (e) {
124
- context.ui.toast.show({ title: "Latent", message: e instanceof Error ? e.message : String(e), variant: "error" });
125
- } finally {
126
- loggingIn = false;
127
- }
128
- };
129
-
130
- const status = async () => {
131
- const s = controller.snapshot();
132
- const wallet = resolveWallet(loadConfig());
133
- const balance = wallet ? await api.balance(wallet) : null;
134
- const choice = await context.ui.dialog.select({
135
- title: wallet ? `Latent — ${short(wallet)}${balance !== null ? ` · $${balance.toFixed(4)}` : ""}` : "Latent — no wallet",
136
- options: [
137
- {
138
- title: `Sponsored line: ${s.status === "disabled" ? "off" : "on"}`,
139
- value: "toggle",
140
- description: `${s.impressions} paid views this session`,
141
- },
142
- { title: wallet ? "Switch wallet" : "Connect wallet", value: "login" },
143
- ],
144
- });
145
- if (choice === "toggle") {
146
- const on = s.status === "disabled";
147
- saveConfig({ opencode_ads: on });
148
- controller.setEnabled(on);
149
- } else if (choice === "login") void login();
150
- };
151
-
152
- context.keymap.layer(() => ({
153
- mode: "global",
154
- commands: [
155
- { id: "latent.status", title: "Latent: status / on-off", group: "Latent", palette: true, slash: { name: "latent" }, run: () => void status() },
156
- { id: "latent.login", title: "Latent: connect wallet", group: "Latent", palette: true, slash: { name: "latent-login" }, run: () => void login() },
157
- {
158
- id: "latent.open",
159
- title: "Latent: open sponsored link",
160
- group: "Latent",
161
- palette: true,
162
- slash: { name: "latent-open" },
163
- run: () => {
164
- const link = controller.click();
165
- if (link) openUrl(link);
166
- else context.ui.toast.show({ message: "No sponsored line on screen" });
167
- },
168
- },
169
- ],
170
- }));
171
-
172
- return () => {
173
- clearInterval(timer);
174
- unsubscribe();
175
- offs.forEach((off) => off());
176
- unslot?.();
177
- };
178
- },
179
- });