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.
- package/README.md +22 -0
- package/cli/dist/index.js +5005 -0
- package/daemon/dist/main.js +5336 -0
- package/native/build.sh +23 -0
- package/native/frontmost.swift +51 -0
- package/native/statusline.swift +120 -0
- package/package.json +29 -0
- package/statusline/dist/index.js +4282 -0
package/native/build.sh
ADDED
|
@@ -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
|
+
}
|