adsinagents 0.1.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.
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # Compile AdLine native helpers (statusline + frontmost) with swiftc.
3
+ # Usage: build.sh [OUT_DIR] (default: ./build)
4
+ # Exits 3 if swiftc is unavailable so callers can fall back to the Node statusline.
5
+ set -euo pipefail
6
+
7
+ HERE="$(cd "$(dirname "$0")" && pwd)"
8
+ OUT="${1:-$HERE/build}"
9
+
10
+ if ! xcrun --find swiftc >/dev/null 2>&1 && ! command -v swiftc >/dev/null 2>&1; then
11
+ echo "swiftc not found (install Xcode Command Line Tools: xcode-select --install)" >&2
12
+ exit 3
13
+ fi
14
+
15
+ mkdir -p "$OUT"
16
+
17
+ echo "Compiling adsinagents-statusline..." >&2
18
+ swiftc -O "$HERE/statusline.swift" -o "$OUT/adsinagents-statusline"
19
+
20
+ echo "Compiling frontmost..." >&2
21
+ swiftc -O "$HERE/frontmost.swift" -o "$OUT/frontmost"
22
+
23
+ echo "Built: $OUT/adsinagents-statusline, $OUT/frontmost" >&2
@@ -0,0 +1,51 @@
1
+ // AdLine frontmost probe — prints the frontmost app bundle id, screen-lock
2
+ // state, and display-awake state as one JSON line, then exits. Used by the
3
+ // daemon's focus poller (≤ 1 Hz) to decide terminal-focus for viewability.
4
+ //
5
+ // Output: {"bundleId":"com.googlecode.iterm2","locked":false,"screenAwake":true}
6
+ // Falls back gracefully: unknown fields default to safe values.
7
+
8
+ import Foundation
9
+ import AppKit
10
+ import CoreGraphics
11
+
12
+ func frontmostBundleId() -> String {
13
+ if let app = NSWorkspace.shared.frontmostApplication {
14
+ return app.bundleIdentifier ?? ""
15
+ }
16
+ return ""
17
+ }
18
+
19
+ /// Screen locked? CGSessionCopyCurrentDictionary exposes CGSSessionScreenIsLocked.
20
+ func screenLocked() -> Bool {
21
+ guard let dict = CGSessionCopyCurrentDictionary() as? [String: Any] else {
22
+ return false
23
+ }
24
+ if let locked = dict["CGSSessionScreenIsLocked"] as? Int {
25
+ return locked == 1
26
+ }
27
+ if let locked = dict["CGSSessionScreenIsLocked"] as? Bool {
28
+ return locked
29
+ }
30
+ return false
31
+ }
32
+
33
+ /// Display asleep? If the display is off we should not count impressions.
34
+ func displayAwake() -> Bool {
35
+ // CGDisplayIsActive on the main display. True when awake/active.
36
+ let main = CGMainDisplayID()
37
+ return CGDisplayIsActive(main) != 0 && CGDisplayIsAsleep(main) == 0
38
+ }
39
+
40
+ let payload: [String: Any] = [
41
+ "bundleId": frontmostBundleId(),
42
+ "locked": screenLocked(),
43
+ "screenAwake": displayAwake(),
44
+ ]
45
+
46
+ if let data = try? JSONSerialization.data(withJSONObject: payload),
47
+ let str = String(data: data, encoding: .utf8) {
48
+ print(str)
49
+ } else {
50
+ print("{\"bundleId\":\"\",\"locked\":false,\"screenAwake\":true}")
51
+ }
@@ -0,0 +1,120 @@
1
+ // AdsInAgents statusline — native macOS build. Reads two small JSON files + stdin,
2
+ // prints one status line, exits. Target: < 5ms wall (vs ~50ms for Node).
3
+ //
4
+ // HARD CONSTRAINTS:
5
+ // - Zero network. Zero deps beyond Foundation.
6
+ // - Never throw to the terminal: any failure => print prior (or empty), exit 0.
7
+ //
8
+ // Reads:
9
+ // - ~/.adsinagents/current-ad.json (Ad, written by daemon)
10
+ // - ~/.adsinagents/daemon-state.json (DaemonState)
11
+ // - stdin (Claude Code session JSON, only used to
12
+ // feed the chained prior statusline)
13
+ // Env:
14
+ // - ADSINAGENTS_PRIOR_STATUSLINE (optional shell cmd to chain)
15
+ // - TERM_PROGRAM / FORCE_HYPERLINK (OSC 8 capability)
16
+ // - ADSINAGENTS_HOME (override ~/.adsinagents, for tests)
17
+
18
+ import Foundation
19
+
20
+ let STATE_STALE_MS: Double = 30_000
21
+
22
+ func adlineDir() -> String {
23
+ if let override = ProcessInfo.processInfo.environment["ADSINAGENTS_HOME"], !override.isEmpty {
24
+ return override
25
+ }
26
+ let home = ProcessInfo.processInfo.environment["HOME"] ?? NSHomeDirectory()
27
+ return home + "/.adsinagents"
28
+ }
29
+
30
+ func readJSON(_ path: String) -> [String: Any]? {
31
+ guard let data = FileManager.default.contents(atPath: path) else { return nil }
32
+ return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
33
+ }
34
+
35
+ func readStdin() -> String {
36
+ let data = FileHandle.standardInput.readDataToEndOfFile()
37
+ return String(data: data, encoding: .utf8) ?? ""
38
+ }
39
+
40
+ func supportsOSC8() -> Bool {
41
+ let env = ProcessInfo.processInfo.environment
42
+ if env["FORCE_HYPERLINK"] == "1" { return true }
43
+ if env["TERM_PROGRAM"] == "Apple_Terminal" { return false }
44
+ return true
45
+ }
46
+
47
+ /// Run the user's pre-existing statusline command, feeding it the session JSON.
48
+ func priorStatusline(_ stdinJSON: String) -> String {
49
+ guard let cmd = ProcessInfo.processInfo.environment["ADSINAGENTS_PRIOR_STATUSLINE"],
50
+ !cmd.isEmpty else { return "" }
51
+ let proc = Process()
52
+ proc.executableURL = URL(fileURLWithPath: "/bin/sh")
53
+ proc.arguments = ["-c", cmd]
54
+ let outPipe = Pipe()
55
+ let inPipe = Pipe()
56
+ proc.standardOutput = outPipe
57
+ proc.standardError = FileHandle.nullDevice
58
+ proc.standardInput = inPipe
59
+ do {
60
+ try proc.run()
61
+ if let d = stdinJSON.data(using: .utf8) { inPipe.fileHandleForWriting.write(d) }
62
+ inPipe.fileHandleForWriting.closeFile()
63
+ let out = outPipe.fileHandleForReading.readDataToEndOfFile()
64
+ proc.waitUntilExit()
65
+ return (String(data: out, encoding: .utf8) ?? "")
66
+ .trimmingCharacters(in: .whitespacesAndNewlines)
67
+ } catch {
68
+ return ""
69
+ }
70
+ }
71
+
72
+ func osc8(_ url: String, _ text: String) -> String {
73
+ return "\u{1b}]8;;\(url)\u{07}\(text)\u{1b}]8;;\u{07}"
74
+ }
75
+
76
+ func emit(_ line: String) {
77
+ print(line)
78
+ }
79
+
80
+ // --- main ---
81
+
82
+ let stdinJSON = readStdin()
83
+ let prior = priorStatusline(stdinJSON)
84
+
85
+ func printPriorAndExit() -> Never {
86
+ emit(prior)
87
+ exit(0)
88
+ }
89
+
90
+ let dir = adlineDir()
91
+ guard let state = readJSON(dir + "/daemon-state.json") else { printPriorAndExit() }
92
+
93
+ let updatedAt = (state["updatedAt"] as? Double) ?? (state["updatedAt"] as? Int).map(Double.init) ?? 0
94
+ let nowMs = Date().timeIntervalSince1970 * 1000
95
+ let stale = (nowMs - updatedAt) > STATE_STALE_MS
96
+ let disabled = (state["disabled"] as? Bool) ?? false
97
+ let adVisible = (state["adVisible"] as? Bool) ?? false
98
+ let port = (state["port"] as? Int) ?? (state["port"] as? Double).map(Int.init) ?? 0
99
+
100
+ if stale || disabled || !adVisible { printPriorAndExit() }
101
+
102
+ guard let ad = readJSON(dir + "/current-ad.json"),
103
+ let id = ad["id"] as? String,
104
+ let text = ad["text"] as? String,
105
+ !id.isEmpty, !text.isEmpty else { printPriorAndExit() }
106
+
107
+ let brand = (ad["brandName"] as? String) ?? ""
108
+ let label = brand.isEmpty ? text : "\(text) — \(brand)"
109
+ let core = "\u{2736} \(label) \u{2197}" // ✶ … ↗
110
+ let clickURL = "http://127.0.0.1:\(port)/click/\(id)"
111
+ let linked = supportsOSC8() ? osc8(clickURL, core) : core
112
+ let segment = "Ad: \(linked) · sponsored"
113
+
114
+ let final: String
115
+ if prior.isEmpty {
116
+ final = segment
117
+ } else {
118
+ final = "\(prior) \u{2502} \(segment)" // │
119
+ }
120
+ emit(final)
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "adsinagents",
3
+ "version": "0.1.0",
4
+ "description": "Get paid while you build. A terminal-native ad layer for Claude Code that pays you for every verified impression.",
5
+ "homepage": "https://adsinagents.com",
6
+ "license": "UNLICENSED",
7
+ "type": "module",
8
+ "bin": {
9
+ "adsinagents": "./cli/dist/index.js",
10
+ "adsinagents-daemon": "./daemon/dist/main.js",
11
+ "adsinagents-statusline": "./statusline/dist/index.js"
12
+ },
13
+ "files": [
14
+ "cli",
15
+ "daemon",
16
+ "statusline",
17
+ "native",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "os": [
24
+ "darwin"
25
+ ],
26
+ "dependencies": {
27
+ "better-sqlite3": "^11.7.0"
28
+ }
29
+ }