├── .gitignore
├── .github
├── FUNDING.yml
└── workflows
│ ├── package.yml
│ └── publish.yml
├── images
└── ziit-logo.png
├── .vscode
└── launch.json
├── tsconfig.json
├── src
├── log.ts
├── extension.ts
├── status-bar.ts
├── config.ts
└── heartbeat.ts
├── README.md
├── package.json
├── bun.lock
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: 0pandadev
--------------------------------------------------------------------------------
/images/ziit-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0PandaDEV/ziit-vscode/main/images/ziit-logo.png
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Extension",
6 | "type": "extensionHost",
7 | "request": "launch",
8 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
9 | "outFiles": ["${workspaceFolder}/dist/**/*.js"]
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext", "DOM"],
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "moduleDetection": "force",
7 | "jsx": "react-jsx",
8 | "allowJs": true,
9 |
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "verbatimModuleSyntax": true,
13 | "noEmit": true,
14 |
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "noFallthroughCasesInSwitch": true,
18 |
19 | "noUnusedLocals": false,
20 | "noUnusedParameters": false,
21 | "noPropertyAccessFromIndexSignature": false
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/log.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 |
3 | let outputChannel: vscode.OutputChannel;
4 |
5 | function getOutputChannel() {
6 | if (!outputChannel) {
7 | outputChannel = vscode.window.createOutputChannel("Ziit");
8 | }
9 | return outputChannel;
10 | }
11 |
12 | export function showOutputChannel(): void {
13 | getOutputChannel().show();
14 | }
15 |
16 | export function log(message: string): void {
17 | const channel = getOutputChannel();
18 | channel.appendLine(`[${new Date().toLocaleString()}] ${message}`);
19 | }
20 |
21 | export function error(message: string): void {
22 | const channel = getOutputChannel();
23 | channel.appendLine(`[${new Date().toLocaleString()}] ERROR: ${message}`);
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/package.yml:
--------------------------------------------------------------------------------
1 | name: Package Extension
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: '20'
20 |
21 | - name: Install dependencies
22 | run: |
23 | npm install
24 | npm install -g @vscode/vsce
25 |
26 | - name: Build & Package
27 | run: |
28 | npm run build
29 | vsce package
30 |
31 | - name: Upload VSIX
32 | uses: actions/upload-artifact@v4
33 | with:
34 | name: extension
35 | path: "*.vsix"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | The VS Code extension for the time tracking software Ziit.
7 |
8 |
9 |
20 |
21 | ## Installation
22 |
23 | 1. Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=pandadev.ziit)
24 | 2. Install from [OpenVSX Registry](https://open-vsx.org/extension/pandadev/ziit)
25 |
26 | ## Setup
27 |
28 | 1. Install the Ziit extension from the VS Code marketplace
29 | 2. Open VS Code and press Cmd + Shift + P (or Ctrl + Shift + P on Windows)
30 | 3. Type "Ziit: Set Instance" and press Enter
31 | 4. Paste your Ziit instance URL
32 | 5. Open VS Code and press Cmd + Shift + P (or Ctrl + Shift + P on Windows)
33 | 6. Type "Ziit: Set API Key" and press Enter
34 | 7. Paste your API key and press Enter
35 | 8. Begin coding, and your time will be tracked automatically!
36 |
37 | ## Commands
38 |
39 | - `ziit.setApiKey`: Set your Ziit API key
40 | - `ziit.setBaseUrl`: Set your Ziit instance URL
41 | - `ziit.openDashboard`: Open your Ziit dashboard
42 | - `ziit.showOutput`: Show Ziit output channel
43 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Extension
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 | with:
16 | ref: ${{ github.event.inputs.ref || github.ref }}
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: '20'
22 | registry-url: 'https://registry.npmjs.org'
23 |
24 | - name: Install dependencies
25 | run: |
26 | npm install
27 | npm install -g @vscode/vsce
28 |
29 | - name: Build & Package
30 | run: |
31 | npm run build
32 | vsce package
33 |
34 | - name: Upload VSIX
35 | uses: actions/upload-artifact@v4
36 | with:
37 | name: extension
38 | path: "*.vsix"
39 | retention-days: 1
40 |
41 | publish-vscode:
42 | needs: build
43 | runs-on: ubuntu-latest
44 | steps:
45 | - name: Download VSIX
46 | uses: actions/download-artifact@v4
47 | with:
48 | name: extension
49 |
50 | - name: Setup Node.js
51 | uses: actions/setup-node@v4
52 | with:
53 | node-version: '20'
54 |
55 | - name: Install vsce
56 | run: npm install -g @vscode/vsce
57 |
58 | - name: Publish to VS Code Marketplace
59 | run: vsce publish -p ${{ secrets.VSCE_PAT }} --packagePath *.vsix
60 |
61 | publish-ovsx:
62 | needs: build
63 | runs-on: ubuntu-latest
64 | steps:
65 | - name: Download VSIX
66 | uses: actions/download-artifact@v4
67 | with:
68 | name: extension
69 |
70 | - name: Setup Node.js
71 | uses: actions/setup-node@v4
72 | with:
73 | node-version: '20'
74 |
75 | - name: Publish to Open VSX Registry
76 | run: npx ovsx publish *.vsix -p ${{ secrets.OVSX_PAT }}
77 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import { log, showOutputChannel } from "./log";
3 | import { HeartbeatManager } from "./heartbeat";
4 | import { StatusBarManager } from "./status-bar";
5 | import { setApiKey, setBaseUrl, initializeAndSyncConfig } from "./config";
6 |
7 | export async function activate(context: vscode.ExtensionContext) {
8 | await initializeAndSyncConfig();
9 |
10 | log("Ziit extension activated");
11 |
12 | const statusBarManager = new StatusBarManager();
13 | context.subscriptions.push(statusBarManager);
14 |
15 | const heartbeatManager = new HeartbeatManager(context, statusBarManager);
16 | context.subscriptions.push(heartbeatManager);
17 |
18 | heartbeatManager.fetchDailySummary();
19 |
20 | context.subscriptions.push(
21 | vscode.workspace.onDidChangeConfiguration((event) => {
22 | if (event.affectsConfiguration("ziit.apiKey")) {
23 | log("API key changed in settings, validating...");
24 | }
25 | })
26 | );
27 |
28 | const openDashboardCommand = vscode.commands.registerCommand(
29 | "ziit.openDashboard",
30 | async () => {
31 | const config = vscode.workspace.getConfiguration("ziit");
32 | const baseUrl = config.get("baseUrl");
33 | if (baseUrl) {
34 | vscode.env.openExternal(vscode.Uri.parse(`${baseUrl}/`));
35 | } else {
36 | vscode.window.showErrorMessage("No base URL configured for Ziit");
37 | }
38 | }
39 | );
40 |
41 | const setApiKeyCommand = vscode.commands.registerCommand(
42 | "ziit.setApiKey",
43 | async () => {
44 | await setApiKey();
45 | }
46 | );
47 |
48 | const setBaseUrlCommand = vscode.commands.registerCommand(
49 | "ziit.setBaseUrl",
50 | async () => {
51 | await setBaseUrl();
52 | }
53 | );
54 |
55 | const showOutputCommand = vscode.commands.registerCommand(
56 | "ziit.showOutput",
57 | () => {
58 | showOutputChannel();
59 | }
60 | );
61 |
62 | context.subscriptions.push(
63 | openDashboardCommand,
64 | setApiKeyCommand,
65 | setBaseUrlCommand,
66 | showOutputCommand
67 | );
68 | }
69 |
70 | export function deactivate() {
71 | log("Ziit extension deactivated");
72 | }
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ziit",
3 | "displayName": "Ziit",
4 | "description": "The swiss army knive for coding time tracking.",
5 | "publisher": "PandaDEV",
6 | "version": "1.2.2",
7 | "keywords": [
8 | "ziit",
9 | "vscode",
10 | "extension",
11 | "api",
12 | "visualization",
13 | "education",
14 | "analytics",
15 | "code",
16 | "code time",
17 | "codetime",
18 | "flow",
19 | "focus",
20 | "metrics",
21 | "productivity",
22 | "time",
23 | "timer",
24 | "time tracker",
25 | "time tracking",
26 | "tracker",
27 | "tracking",
28 | "worktime"
29 | ],
30 | "homepage": "https://pandadev.net",
31 | "bugs": {
32 | "url": "https://github.com/0pandadev/ziit-vscode/issues",
33 | "email": "contact@pandadev.net"
34 | },
35 | "icon": "images/ziit-logo.png",
36 | "engines": {
37 | "vscode": "^1.100.0"
38 | },
39 | "files": [
40 | "dist",
41 | "images",
42 | "package.json",
43 | "README.md",
44 | "LICENSE"
45 | ],
46 | "categories": [
47 | "Visualization",
48 | "Education"
49 | ],
50 | "activationEvents": [
51 | "onStartupFinished",
52 | "onDebug"
53 | ],
54 | "main": "./dist/extension.js",
55 | "contributes": {
56 | "commands": [
57 | {
58 | "command": "ziit.setApiKey",
59 | "title": "Ziit: Set API Key"
60 | },
61 | {
62 | "command": "ziit.setBaseUrl",
63 | "title": "Ziit: Set Instance"
64 | },
65 | {
66 | "command": "ziit.openDashboard",
67 | "title": "Ziit: Open Dashboard"
68 | },
69 | {
70 | "command": "ziit.showOutput",
71 | "title": "Ziit: Show Output"
72 | }
73 | ],
74 | "configuration": {
75 | "title": "Ziit",
76 | "properties": {
77 | "ziit.apiKey": {
78 | "type": "string",
79 | "description": "API key for Ziit server authentication"
80 | },
81 | "ziit.baseUrl": {
82 | "type": "string",
83 | "default": "https://ziit.app",
84 | "description": "Base URL for the Ziit server instance"
85 | }
86 | }
87 | }
88 | },
89 | "scripts": {
90 | "build": "esbuild ./src/extension.ts --bundle --outdir=dist --external:vscode --format=cjs --platform=node",
91 | "dev": "esbuild ./src/extension.ts --bundle --outdir=dist --external:vscode --format=cjs --platform=node --watch"
92 | },
93 | "devDependencies": {
94 | "@types/node": "24.10.2",
95 | "@types/vscode": "1.100.0",
96 | "esbuild": "0.27.1"
97 | },
98 | "repository": {
99 | "type": "git",
100 | "url": "https://github.com/0pandadev/ziit-vscode.git"
101 | },
102 | "license": "GPL-3.0"
103 | }
104 |
--------------------------------------------------------------------------------
/src/status-bar.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 |
3 | export class StatusBarManager {
4 | private statusBarItem: vscode.StatusBarItem;
5 | private totalSeconds: number = 0;
6 | private trackingStartTime: number = 0;
7 | private isTracking: boolean = false;
8 | private updateInterval: NodeJS.Timeout | null = null;
9 | private isOnline: boolean = true;
10 | private hasValidApiKey: boolean = true;
11 |
12 | constructor() {
13 | this.statusBarItem = vscode.window.createStatusBarItem(
14 | vscode.StatusBarAlignment.Left,
15 | 100
16 | );
17 | this.statusBarItem.command = "ziit.openDashboard";
18 | this.statusBarItem.show();
19 |
20 | const config = vscode.workspace.getConfiguration("ziit");
21 | if (config.get("statusBarEnabled", true)) {
22 | this.statusBarItem.show();
23 | }
24 |
25 | vscode.workspace.onDidChangeConfiguration((e) => {
26 | if (e.affectsConfiguration("ziit.statusBarEnabled")) {
27 | const config = vscode.workspace.getConfiguration("ziit");
28 | if (config.get("statusBarEnabled", true)) {
29 | this.statusBarItem.show();
30 | } else {
31 | this.statusBarItem.hide();
32 | }
33 | }
34 | });
35 |
36 | this.setupUpdateInterval();
37 | }
38 |
39 | private setupUpdateInterval(): void {
40 | this.updateInterval = setInterval(() => {
41 | this.updateStatusBar(true);
42 | }, 60000);
43 | }
44 |
45 | public startTracking(): void {
46 | if (!this.isTracking) {
47 | this.isTracking = true;
48 | this.trackingStartTime = Date.now();
49 | this.updateStatusBar(true);
50 | }
51 | }
52 |
53 | public stopTracking(): void {
54 | if (this.isTracking) {
55 | this.isTracking = false;
56 | this.updateStatusBar(true);
57 | }
58 | }
59 |
60 | public updateTime(hours: number, minutes: number): void {
61 | this.totalSeconds = hours * 3600 + minutes * 60;
62 | this.updateStatusBar(true);
63 | }
64 |
65 | public setOnlineStatus(isOnline: boolean): void {
66 | this.isOnline = isOnline;
67 | this.updateStatusBar(true);
68 | }
69 |
70 | public setApiKeyStatus(isValid: boolean): void {
71 | this.hasValidApiKey = isValid;
72 | this.updateStatusBar(true);
73 | }
74 |
75 | private updateStatusBar(forceUpdate: boolean = false): void {
76 | const config = vscode.workspace.getConfiguration("ziit");
77 | if (!config.get("statusBarEnabled", true)) {
78 | return;
79 | }
80 |
81 | if (!this.hasValidApiKey) {
82 | this.statusBarItem.text = "$(error) Unconfigured";
83 | this.statusBarItem.tooltip = "Invalid or missing API key. Click to configure.";
84 | this.statusBarItem.color = new vscode.ThemeColor("errorForeground");
85 | return;
86 | }
87 |
88 | let displaySeconds = this.totalSeconds;
89 |
90 | if (this.isTracking) {
91 | const elapsedSeconds = Math.floor(
92 | (Date.now() - this.trackingStartTime) / 1000
93 | );
94 | displaySeconds += elapsedSeconds;
95 | }
96 |
97 | const hours = Math.floor(displaySeconds / 3600);
98 | const minutes = Math.floor((displaySeconds % 3600) / 60);
99 |
100 | if (forceUpdate) {
101 | this.statusBarItem.color = new vscode.ThemeColor(
102 | "statusBarItem.prominentForeground"
103 | );
104 | setTimeout(() => {
105 | this.statusBarItem.color = undefined;
106 | }, 1000);
107 | }
108 |
109 | if (!this.isOnline) {
110 | this.statusBarItem.text = `$(sync~spin) ${hours} hrs ${minutes} mins (offline)`;
111 | this.statusBarItem.tooltip = "Working offline. Changes will be synced when online.";
112 | this.statusBarItem.color = new vscode.ThemeColor("statusBarItem.warningForeground");
113 | return;
114 | }
115 |
116 | this.statusBarItem.text = `$(clock) ${hours} hrs ${minutes} mins`;
117 | this.statusBarItem.tooltip = "Ziit: Today's coding time. Click to open dashboard.";
118 | this.statusBarItem.color = undefined;
119 | }
120 |
121 | public dispose(): void {
122 | if (this.updateInterval) {
123 | clearInterval(this.updateInterval);
124 | }
125 | this.statusBarItem.dispose();
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/bun.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1,
3 | "workspaces": {
4 | "": {
5 | "name": "ziit",
6 | "devDependencies": {
7 | "@types/node": "24.10.2",
8 | "@types/vscode": "1.100.0",
9 | "esbuild": "0.27.1",
10 | },
11 | },
12 | },
13 | "packages": {
14 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
15 |
16 | "@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="],
17 |
18 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="],
19 |
20 | "@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="],
21 |
22 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="],
23 |
24 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="],
25 |
26 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="],
27 |
28 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="],
29 |
30 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="],
31 |
32 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="],
33 |
34 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="],
35 |
36 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="],
37 |
38 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="],
39 |
40 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="],
41 |
42 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="],
43 |
44 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="],
45 |
46 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="],
47 |
48 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="],
49 |
50 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="],
51 |
52 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="],
53 |
54 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="],
55 |
56 | "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="],
57 |
58 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="],
59 |
60 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="],
61 |
62 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="],
63 |
64 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
65 |
66 | "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="],
67 |
68 | "@types/vscode": ["@types/vscode@1.100.0", "", {}, "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw=="],
69 |
70 | "esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="],
71 |
72 | "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import { log } from "./log";
3 | import * as fs from "fs/promises";
4 | import * as os from "os";
5 | import * as path from "path";
6 |
7 | function getConfigDir(): string {
8 | const xdgConfigHome = process.env.XDG_CONFIG_HOME;
9 | if (xdgConfigHome) {
10 | return path.join(xdgConfigHome, "ziit");
11 | }
12 | return path.join(os.homedir(), ".config", "ziit");
13 | }
14 |
15 | const CONFIG_DIR = getConfigDir();
16 | const CONFIG_FILE_NAME = "config.json";
17 | const CONFIG_FILE_PATH = path.join(CONFIG_DIR, CONFIG_FILE_NAME);
18 |
19 | const LEGACY_CONFIG_FILE_NAME = ".ziit.json";
20 | const LEGACY_CONFIG_FILE_PATH = path.join(
21 | os.homedir(),
22 | LEGACY_CONFIG_FILE_NAME,
23 | );
24 | const OLD_CONFIG_FILE_NAME = ".ziit.cfg";
25 | const OLD_CONFIG_FILE_PATH = path.join(os.homedir(), OLD_CONFIG_FILE_NAME);
26 |
27 | interface ZiitConfig {
28 | apiKey?: string;
29 | baseUrl?: string;
30 | }
31 |
32 | async function ensureConfigDir(): Promise {
33 | try {
34 | await fs.mkdir(CONFIG_DIR, { recursive: true });
35 | } catch (error: any) {
36 | log(`Error creating config directory: ${error.message}`);
37 | }
38 | }
39 |
40 | async function migrateLegacyConfigs(): Promise {
41 | try {
42 | await fs.access(CONFIG_FILE_PATH);
43 | log("New config file already exists, skipping migration");
44 | return;
45 | } catch {
46 | log("New config file not found, checking for legacy configs to migrate");
47 | }
48 |
49 | let migratedConfig: ZiitConfig = {};
50 | let migrationSource = "";
51 |
52 | try {
53 | await fs.access(LEGACY_CONFIG_FILE_PATH);
54 | const content = await fs.readFile(LEGACY_CONFIG_FILE_PATH, "utf-8");
55 | migratedConfig = JSON.parse(content);
56 | migrationSource = LEGACY_CONFIG_FILE_PATH;
57 | log("Found legacy .ziit.json config file for migration");
58 | } catch {
59 | try {
60 | await fs.access(OLD_CONFIG_FILE_PATH);
61 | const content = await fs.readFile(OLD_CONFIG_FILE_PATH, "utf-8");
62 | let apiKey: string | undefined;
63 | let baseUrl: string | undefined;
64 | const lines = content.split(/\r?\n/);
65 | for (const line of lines) {
66 | const trimmed = line.trim();
67 | if (trimmed.startsWith("api_key")) {
68 | apiKey = trimmed.split("=")[1]?.trim();
69 | }
70 | if (trimmed.startsWith("base_url")) {
71 | baseUrl = trimmed.split("=")[1]?.trim().replace(/\\:/g, ":");
72 | }
73 | }
74 | if (apiKey) migratedConfig.apiKey = apiKey;
75 | if (baseUrl) migratedConfig.baseUrl = baseUrl;
76 | migrationSource = OLD_CONFIG_FILE_PATH;
77 | log("Found legacy .ziit.cfg config file for migration");
78 | } catch {
79 | return;
80 | }
81 | }
82 |
83 | if (migrationSource) {
84 | try {
85 | await ensureConfigDir();
86 | await fs.writeFile(
87 | CONFIG_FILE_PATH,
88 | JSON.stringify(migratedConfig, null, 2),
89 | );
90 |
91 | try {
92 | if (migrationSource === LEGACY_CONFIG_FILE_PATH) {
93 | await fs.unlink(LEGACY_CONFIG_FILE_PATH);
94 | log(
95 | `Migrated config from ${LEGACY_CONFIG_FILE_PATH} to ${CONFIG_FILE_PATH}`,
96 | );
97 | } else if (migrationSource === OLD_CONFIG_FILE_PATH) {
98 | await fs.unlink(OLD_CONFIG_FILE_PATH);
99 | log(
100 | `Migrated config from ${OLD_CONFIG_FILE_PATH} to ${CONFIG_FILE_PATH}`,
101 | );
102 | }
103 | } catch (cleanupError: any) {
104 | log(
105 | `Warning: Could not remove old config file: ${cleanupError.message}`,
106 | );
107 | }
108 |
109 | vscode.window.showInformationMessage(
110 | "Ziit configuration has been migrated to the new location. " +
111 | `New location: ${CONFIG_FILE_PATH}`,
112 | );
113 | } catch (error: any) {
114 | log(`Error during migration: ${error.message}`);
115 | vscode.window.showErrorMessage(
116 | `Failed to migrate Ziit configuration: ${error.message}`,
117 | );
118 | }
119 | }
120 | }
121 |
122 | async function readConfigFile(): Promise {
123 | await migrateLegacyConfigs();
124 | try {
125 | const content = await fs.readFile(CONFIG_FILE_PATH, "utf-8");
126 | return JSON.parse(content);
127 | } catch (error: any) {
128 | if (error.code === "ENOENT") {
129 | throw error;
130 | } else {
131 | log(`Error reading config file: ${error.message}`);
132 | vscode.window.showErrorMessage(
133 | `Error reading Ziit config file: ${error.message}`,
134 | );
135 | return {};
136 | }
137 | }
138 | }
139 |
140 | async function writeConfigFile(config: ZiitConfig): Promise {
141 | try {
142 | await ensureConfigDir();
143 | await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
144 | log(`Config file updated (${CONFIG_FILE_PATH})`);
145 | } catch (error: any) {
146 | log(`Error writing config file: ${error.message}`);
147 | vscode.window.showErrorMessage(
148 | `Failed to write Ziit config file: ${error.message}`,
149 | );
150 | }
151 | }
152 |
153 | async function getConfigValue(
154 | key: keyof ZiitConfig,
155 | ): Promise {
156 | const vscodeConfig = vscode.workspace.getConfiguration("ziit");
157 |
158 | const workspaceValue = vscodeConfig.inspect(key)?.workspaceValue;
159 | if (workspaceValue !== undefined) {
160 | return workspaceValue;
161 | }
162 |
163 | const userValue = vscodeConfig.inspect(key)?.globalValue;
164 | if (userValue !== undefined) {
165 | return userValue;
166 | }
167 |
168 | try {
169 | const fileConfig: ZiitConfig = await readConfigFile();
170 | if (fileConfig[key] !== undefined) {
171 | return fileConfig[key] as T;
172 | }
173 | } catch (error: any) {
174 | if (error.code !== "ENOENT") {
175 | log(`Error reading config file in getConfigValue: ${error.message}`);
176 | }
177 | }
178 |
179 | return undefined;
180 | }
181 |
182 | async function updateConfigValue(
183 | key: keyof ZiitConfig,
184 | value: T,
185 | ): Promise {
186 | let currentConfig: ZiitConfig = {};
187 | try {
188 | currentConfig = await readConfigFile();
189 | } catch (error: any) {
190 | if (error.code !== "ENOENT") {
191 | log(`Error reading config file before update: ${error.message}`);
192 | }
193 | }
194 | const newConfig = { ...currentConfig, [key]: value };
195 | await writeConfigFile(newConfig);
196 | await vscode.workspace.getConfiguration("ziit").update(key, value, true);
197 | log(
198 | `${key} updated in config file (${CONFIG_FILE_PATH}) and VS Code settings.`,
199 | );
200 | }
201 |
202 | export async function setApiKey(): Promise {
203 | const apiKey = await vscode.window.showInputBox({
204 | prompt: "Enter your Ziit API key",
205 | placeHolder: "API Key",
206 | password: true,
207 | });
208 | if (!apiKey) {
209 | log("API key setting cancelled");
210 | return;
211 | }
212 | await updateConfigValue("apiKey", apiKey);
213 | vscode.window.showInformationMessage("Ziit API key has been updated");
214 | }
215 |
216 | export async function setBaseUrl(): Promise {
217 | const currentBaseUrl = await getBaseUrl();
218 | const baseUrl = await vscode.window.showInputBox({
219 | prompt: "Enter your Ziit instance URL",
220 | placeHolder: "https://ziit.app",
221 | value: currentBaseUrl,
222 | });
223 | if (!baseUrl) {
224 | log("Base URL setting cancelled");
225 | return;
226 | }
227 | await updateConfigValue("baseUrl", baseUrl);
228 | vscode.window.showInformationMessage("Ziit instance URL has been updated");
229 | }
230 |
231 | export async function getApiKey(): Promise {
232 | return getConfigValue("apiKey");
233 | }
234 |
235 | export async function getBaseUrl(): Promise {
236 | return (await getConfigValue("baseUrl")) ?? "https://ziit.app";
237 | }
238 |
239 | export async function initializeAndSyncConfig(): Promise {
240 | log(
241 | `Initializing or syncing config file (${CONFIG_FILE_PATH}) with VS Code settings...`,
242 | );
243 | let fileConfig: ZiitConfig;
244 | let fileNeedsCreation = false;
245 | try {
246 | fileConfig = await readConfigFile();
247 | log(`Config file found (${CONFIG_FILE_PATH})`);
248 | } catch (error: any) {
249 | if (error.code === "ENOENT") {
250 | log(`Config file not found at ${CONFIG_FILE_PATH}. Will create it.`);
251 | fileNeedsCreation = true;
252 | fileConfig = {};
253 | } else {
254 | return;
255 | }
256 | }
257 | const vscodeConfig = vscode.workspace.getConfiguration("ziit");
258 | if (fileNeedsCreation) {
259 | log("Populating new config file from current VS Code settings...");
260 | const initialConfig: ZiitConfig = {};
261 | for (const key of ["apiKey", "baseUrl"]) {
262 | const value = vscodeConfig.get(key);
263 | if (value !== undefined) {
264 | initialConfig[key as keyof ZiitConfig] = value as any;
265 | }
266 | }
267 | await writeConfigFile(initialConfig);
268 | fileConfig = initialConfig;
269 | log(`Config file created and populated (${CONFIG_FILE_PATH})`);
270 | }
271 | let updated = false;
272 | for (const key of ["apiKey", "baseUrl"]) {
273 | const fileValue = fileConfig[key as keyof ZiitConfig];
274 | const vscodeValue = vscodeConfig.get(key);
275 | const inspect = vscodeConfig.inspect(key);
276 | if (fileValue !== undefined) {
277 | if (vscodeValue !== fileValue) {
278 | await vscodeConfig.update(key, fileValue, true);
279 | log(`Synced VS Code setting '${key}' from config file value.`);
280 | updated = true;
281 | }
282 | } else {
283 | const defaultValue = inspect?.defaultValue;
284 | if (vscodeValue !== undefined && vscodeValue !== defaultValue) {
285 | await vscodeConfig.update(key, undefined, true);
286 | log(
287 | `Reset VS Code setting '${key}' to default as it's not in config file.`,
288 | );
289 | updated = true;
290 | }
291 | }
292 | }
293 | if (updated) {
294 | log("VS Code settings synced.");
295 | } else {
296 | log("No VS Code settings needed syncing.");
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/src/heartbeat.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import { log } from "./log";
3 | import * as https from "https";
4 | import * as http from "http";
5 | import * as fs from "fs";
6 | import * as path from "path";
7 | import * as os from "os";
8 | import { StatusBarManager } from "./status-bar";
9 | import { getApiKey, getBaseUrl } from "./config";
10 |
11 | interface Heartbeat {
12 | timestamp: string;
13 | project?: string;
14 | language?: string;
15 | file?: string;
16 | branch?: string;
17 | editor: string;
18 | os: string;
19 | }
20 |
21 | export class HeartbeatManager {
22 | private lastHeartbeat: number = 0;
23 | private lastFile: string = "";
24 | private heartbeatInterval: number = 120000;
25 | private userInactivityThresholdMilliseconds: number = 15 * 60 * 1000;
26 | private activeDocumentInfo: { file: string; language: string } | null = null;
27 | private statusBar: StatusBarManager | null = null;
28 | private heartbeatCount: number = 0;
29 | private successCount: number = 0;
30 | private failureCount: number = 0;
31 | private offlineHeartbeats: Heartbeat[] = [];
32 | private offlineQueuePath: string;
33 | private isOnline: boolean = true;
34 | private hasValidApiKey: boolean = true;
35 | private lastActivity: number = Date.now();
36 | private todayLocalTotalSeconds: number = 0;
37 | private isWindowFocused: boolean = true;
38 | private unsyncedLocalSeconds: number = 0;
39 | private activityAccumulatorIntervalId: NodeJS.Timeout | null = null;
40 | private lastTimeAccumulated: number = Date.now();
41 |
42 | constructor(
43 | private context: vscode.ExtensionContext,
44 | statusBar?: StatusBarManager,
45 | ) {
46 | this.statusBar = statusBar || null;
47 |
48 | const xdgConfigHome = process.env.XDG_CONFIG_HOME;
49 | const configDir = xdgConfigHome
50 | ? path.join(xdgConfigHome, "ziit")
51 | : path.join(os.homedir(), ".config", "ziit");
52 |
53 | if (!fs.existsSync(configDir)) {
54 | fs.mkdirSync(configDir, { recursive: true });
55 | }
56 |
57 | this.offlineQueuePath = path.join(configDir, "offline_heartbeats.json");
58 | this.migrateOfflineHeartbeats();
59 | this.loadOfflineHeartbeats();
60 | this.initialize();
61 | }
62 |
63 | private initialize(): void {
64 | this.isWindowFocused = vscode.window.state.focused;
65 | this.lastActivity = Date.now();
66 | this.lastTimeAccumulated = Date.now();
67 |
68 | this.activityAccumulatorIntervalId = setInterval(() => {
69 | const now = Date.now();
70 | if (this.isWindowFocused) {
71 | const timeSinceLastInteraction = now - this.lastActivity;
72 | if (
73 | timeSinceLastInteraction < this.userInactivityThresholdMilliseconds
74 | ) {
75 | const elapsedSeconds = Math.floor(
76 | (now - this.lastTimeAccumulated) / 1000,
77 | );
78 | if (elapsedSeconds > 0) {
79 | this.unsyncedLocalSeconds += elapsedSeconds;
80 | }
81 | }
82 | }
83 | this.lastTimeAccumulated = now;
84 | }, 5000);
85 |
86 | this.context.subscriptions.push({
87 | dispose: () => {
88 | if (this.activityAccumulatorIntervalId) {
89 | clearInterval(this.activityAccumulatorIntervalId);
90 | }
91 | },
92 | });
93 |
94 | this.registerEventListeners();
95 | this.scheduleHeartbeat();
96 | this.syncOfflineHeartbeats();
97 |
98 | if (this.statusBar) {
99 | this.statusBar.setOnlineStatus(this.isOnline);
100 | this.statusBar.setApiKeyStatus(this.hasValidApiKey);
101 | if (this.isWindowFocused) {
102 | this.statusBar.startTracking();
103 | }
104 | }
105 | }
106 |
107 | private registerEventListeners(): void {
108 | log("Registering event listeners for editor changes");
109 |
110 | vscode.window.onDidChangeActiveTextEditor(
111 | this.handleActiveEditorChange,
112 | null,
113 | this.context.subscriptions,
114 | );
115 |
116 | vscode.workspace.onDidChangeTextDocument(
117 | this.handleDocumentChange,
118 | null,
119 | this.context.subscriptions,
120 | );
121 |
122 | vscode.workspace.onDidSaveTextDocument(
123 | this.handleDocumentSave,
124 | null,
125 | this.context.subscriptions,
126 | );
127 |
128 | vscode.window.onDidChangeWindowState(
129 | this.handleWindowStateChange,
130 | null,
131 | this.context.subscriptions,
132 | );
133 |
134 | if (vscode.window.activeTextEditor) {
135 | this.handleActiveEditorChange(vscode.window.activeTextEditor);
136 | }
137 | }
138 |
139 | private recordUserInteraction(): void {
140 | this.lastActivity = Date.now();
141 | if (this.statusBar && this.isWindowFocused) {
142 | this.statusBar.startTracking();
143 | }
144 | }
145 |
146 | private handleActiveEditorChange = (
147 | editor: vscode.TextEditor | undefined,
148 | ): void => {
149 | if (editor) {
150 | log(
151 | `Editor changed: ${editor.document.uri.fsPath} (${editor.document.languageId})`,
152 | );
153 | this.activeDocumentInfo = {
154 | file: path.basename(editor.document.uri.fsPath),
155 | language: editor.document.languageId,
156 | };
157 | this.recordUserInteraction();
158 | this.sendHeartbeat(true).then(() => this.fetchDailySummary());
159 | }
160 | };
161 |
162 | private handleDocumentChange = (
163 | event: vscode.TextDocumentChangeEvent,
164 | ): void => {
165 | const activeEditor = vscode.window.activeTextEditor;
166 | if (activeEditor && activeEditor.document === event.document) {
167 | this.activeDocumentInfo = {
168 | file: path.basename(event.document.uri.fsPath),
169 | language: event.document.languageId,
170 | };
171 | this.recordUserInteraction();
172 | const now = Date.now();
173 | const fileChanged = this.lastFile !== event.document.uri.fsPath;
174 | const timeThresholdPassed =
175 | now - this.lastHeartbeat >= this.heartbeatInterval;
176 | if (fileChanged || timeThresholdPassed) {
177 | this.sendHeartbeat().then(() => this.fetchDailySummary());
178 | }
179 | }
180 | };
181 |
182 | private handleDocumentSave = (document: vscode.TextDocument): void => {
183 | const activeEditor = vscode.window.activeTextEditor;
184 | if (activeEditor && activeEditor.document === document) {
185 | this.recordUserInteraction();
186 | this.sendHeartbeat(true).then(() => this.fetchDailySummary());
187 | }
188 | };
189 |
190 | private handleWindowStateChange = (windowState: vscode.WindowState): void => {
191 | const wasFocused = this.isWindowFocused;
192 | this.isWindowFocused = windowState.focused;
193 | log(`Window focus state changed: ${wasFocused} -> ${this.isWindowFocused}`);
194 | if (!this.isWindowFocused && wasFocused) {
195 | if (this.statusBar) {
196 | this.statusBar.stopTracking();
197 | }
198 | this.fetchDailySummary();
199 | } else if (this.isWindowFocused && !wasFocused) {
200 | this.lastActivity = Date.now();
201 | log(
202 | `Window focused, activity timer reset at ${new Date(
203 | this.lastActivity,
204 | ).toLocaleTimeString()}`,
205 | );
206 | if (this.statusBar) {
207 | this.statusBar.startTracking();
208 | }
209 | this.fetchDailySummary();
210 | }
211 | };
212 |
213 | private scheduleHeartbeat(): void {
214 | log(
215 | `Setting up heartbeat schedule with interval: ${this.heartbeatInterval}ms and inactivity threshold: ${this.userInactivityThresholdMilliseconds}ms`,
216 | );
217 | setInterval(() => {
218 | const now = Date.now();
219 | const userIsEffectivelyActive =
220 | this.isWindowFocused &&
221 | now - this.lastActivity < this.userInactivityThresholdMilliseconds;
222 | if (this.activeDocumentInfo && userIsEffectivelyActive) {
223 | this.sendHeartbeat().then(() => this.fetchDailySummary());
224 | if (this.statusBar && this.isWindowFocused) {
225 | this.statusBar.startTracking();
226 | }
227 | } else {
228 | const reason = !this.activeDocumentInfo
229 | ? "no active document"
230 | : !this.isWindowFocused
231 | ? "window not focused"
232 | : "user inactive";
233 | log(
234 | `Skipping heartbeat (${reason}). Focused: ${
235 | this.isWindowFocused
236 | }, SufficientlyRecentInteraction: ${
237 | now - this.lastActivity < this.userInactivityThresholdMilliseconds
238 | }, ActiveDoc: ${!!this.activeDocumentInfo}`,
239 | );
240 | if (this.statusBar) {
241 | this.statusBar.stopTracking();
242 | }
243 | }
244 | }, this.heartbeatInterval);
245 | setInterval(
246 | () => {
247 | this.fetchDailySummary();
248 | log(
249 | `Heartbeat stats - Total: ${this.heartbeatCount}, Success: ${this.successCount}, Failed: ${this.failureCount}, Offline: ${this.offlineHeartbeats.length}`,
250 | );
251 | },
252 | 15 * 60 * 1000,
253 | );
254 | }
255 |
256 | private migrateOfflineHeartbeats(): void {
257 | try {
258 | const legacyOfflinePath = path.join(
259 | os.homedir(),
260 | ".ziit",
261 | "offline_heartbeats.json",
262 | );
263 |
264 | if (
265 | fs.existsSync(legacyOfflinePath) &&
266 | !fs.existsSync(this.offlineQueuePath)
267 | ) {
268 | const legacyData = fs.readFileSync(legacyOfflinePath, "utf8");
269 | fs.writeFileSync(this.offlineQueuePath, legacyData, "utf8");
270 | fs.unlinkSync(legacyOfflinePath);
271 | log(
272 | `Migrated offline heartbeats from ${legacyOfflinePath} to ${this.offlineQueuePath}`,
273 | );
274 |
275 | try {
276 | const legacyDir = path.dirname(legacyOfflinePath);
277 | const dirContents = fs.readdirSync(legacyDir);
278 | if (dirContents.length === 0) {
279 | fs.rmdirSync(legacyDir);
280 | log(`Removed empty legacy directory: ${legacyDir}`);
281 | }
282 | } catch (cleanupError) {
283 | log(
284 | `Could not remove legacy directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
285 | );
286 | }
287 | }
288 | } catch (error) {
289 | log(
290 | `Error migrating offline heartbeats: ${
291 | error instanceof Error ? error.message : String(error)
292 | }`,
293 | );
294 | }
295 | }
296 |
297 | public async fetchDailySummary(): Promise {
298 | const apiKey = await getApiKey();
299 | const baseUrl = await getBaseUrl();
300 | if (!apiKey || !baseUrl) {
301 | return;
302 | }
303 | try {
304 | const url = new URL("/api/external/stats", baseUrl);
305 | url.searchParams.append("timeRange", "today");
306 | const now = new Date();
307 | const timezoneOffsetMinutes = now.getTimezoneOffset();
308 | const timezoneOffsetSeconds = timezoneOffsetMinutes * 60 * -1;
309 | url.searchParams.append(
310 | "midnightOffsetSeconds",
311 | timezoneOffsetSeconds.toString(),
312 | );
313 | url.searchParams.append("t", Date.now().toString());
314 | const requestOptions = {
315 | hostname: url.hostname,
316 | port: url.port || (url.protocol === "https:" ? 443 : 80),
317 | path: url.pathname + url.search,
318 | method: "GET",
319 | headers: {
320 | Authorization: `Bearer ${apiKey}`,
321 | },
322 | protocol: url.protocol,
323 | };
324 | const apiResponse = await this.makeRequest<{
325 | summaries: Array<{
326 | date: string;
327 | totalSeconds: number;
328 | projects: Record;
329 | languages: Record;
330 | editors: Record;
331 | os: Record;
332 | hourlyData: Array<{ seconds: number }>;
333 | }>;
334 | timezone: string;
335 | }>(requestOptions);
336 | this.setOnlineStatus(true);
337 | this.setApiKeyStatus(true);
338 | if (
339 | apiResponse &&
340 | apiResponse.summaries &&
341 | apiResponse.summaries.length > 0
342 | ) {
343 | const todaySummary = apiResponse.summaries[0];
344 | this.todayLocalTotalSeconds = todaySummary.totalSeconds;
345 | if (this.statusBar) {
346 | const hours = Math.floor(this.todayLocalTotalSeconds / 3600);
347 | const minutes = Math.floor((this.todayLocalTotalSeconds % 3600) / 60);
348 | this.statusBar.updateTime(hours, minutes);
349 | }
350 | this.unsyncedLocalSeconds = 0;
351 | } else {
352 | if (this.statusBar) {
353 | this.statusBar.updateTime(0, 0);
354 | }
355 | }
356 | } catch (error) {
357 | if (error instanceof Error && error.message.includes("401")) {
358 | this.setApiKeyStatus(false);
359 | log(`Error fetching daily summary: Invalid API key`);
360 | } else {
361 | this.setOnlineStatus(false);
362 | log(`Error fetching daily summary: ${error}`);
363 | }
364 | if (
365 | this.statusBar &&
366 | (this.todayLocalTotalSeconds > 0 || this.unsyncedLocalSeconds > 0)
367 | ) {
368 | const totalSeconds =
369 | this.todayLocalTotalSeconds + this.unsyncedLocalSeconds;
370 | const hours = Math.floor(totalSeconds / 3600);
371 | const minutes = Math.floor((totalSeconds % 3600) / 60);
372 | this.statusBar.updateTime(hours, minutes);
373 | }
374 | }
375 | }
376 |
377 | private async syncOfflineHeartbeats(): Promise {
378 | log(
379 | "Syncing offline heartbeats to the contected ziit instance: " +
380 | (await getBaseUrl()),
381 | );
382 |
383 | if (!this.isOnline || this.offlineHeartbeats.length === 0) return;
384 | const apiKey = await getApiKey();
385 | const baseUrl = await getBaseUrl();
386 | if (!apiKey || !baseUrl) {
387 | return;
388 | }
389 |
390 | this.offlineHeartbeats = this.offlineHeartbeats.map((heartbeat) => ({
391 | ...heartbeat,
392 | timestamp:
393 | typeof heartbeat.timestamp === "number"
394 | ? new Date(heartbeat.timestamp).toISOString()
395 | : heartbeat.timestamp,
396 | }));
397 |
398 | while (this.offlineHeartbeats.length > 0) {
399 | const batch = this.offlineHeartbeats.slice(0, 1000);
400 | this.offlineHeartbeats = this.offlineHeartbeats.slice(1000);
401 |
402 | try {
403 | const data = JSON.stringify(batch);
404 | const url = new URL("/api/external/batch", baseUrl);
405 |
406 | const requestOptions = {
407 | hostname: url.hostname,
408 | port: url.port || (url.protocol === "https:" ? 443 : 80),
409 | path: url.pathname + url.search,
410 | method: "POST",
411 | headers: {
412 | "Content-Type": "application/json",
413 | "Content-Length": Buffer.byteLength(data),
414 | Authorization: `Bearer ${apiKey}`,
415 | },
416 | protocol: url.protocol,
417 | };
418 |
419 | await new Promise((resolve, reject) => {
420 | const req = (url.protocol === "https:" ? https : http).request(
421 | requestOptions,
422 | (res) => {
423 | if (
424 | res.statusCode &&
425 | res.statusCode >= 200 &&
426 | res.statusCode < 300
427 | ) {
428 | resolve();
429 | } else if (res.statusCode === 401) {
430 | this.setApiKeyStatus(false);
431 | reject(
432 | new Error(`Invalid API key (status code: ${res.statusCode})`),
433 | );
434 | } else {
435 | reject(new Error(`Failed with status code: ${res.statusCode}`));
436 | }
437 | },
438 | );
439 |
440 | req.on("error", (err) => {
441 | this.setOnlineStatus(false);
442 | reject(err);
443 | });
444 | req.write(data);
445 | req.end();
446 | });
447 |
448 | this.setOnlineStatus(true);
449 | this.setApiKeyStatus(true);
450 | this.saveOfflineHeartbeats();
451 |
452 | this.unsyncedLocalSeconds = 0;
453 | } catch (error) {
454 | log(
455 | `Error syncing offline heartbeats batch: ${
456 | error instanceof Error ? error.message : String(error)
457 | }`,
458 | );
459 | this.offlineHeartbeats = [...batch, ...this.offlineHeartbeats];
460 | this.saveOfflineHeartbeats();
461 |
462 | if (error instanceof Error && error.message.includes("API key")) {
463 | this.setApiKeyStatus(false);
464 | } else {
465 | this.setOnlineStatus(false);
466 | }
467 | break;
468 | }
469 | }
470 |
471 | if (this.offlineHeartbeats.length === 0) {
472 | this.fetchDailySummary();
473 | }
474 | }
475 |
476 | private async getGitBranch(): Promise {
477 | const workspaceFolders = vscode.workspace.workspaceFolders;
478 | if (!workspaceFolders || workspaceFolders.length === 0) return undefined;
479 |
480 | try {
481 | const gitExtension = vscode.extensions.getExtension("vscode.git");
482 | if (!gitExtension) return undefined;
483 |
484 | const git = gitExtension.exports.getAPI(1);
485 | const repository = git.repositories[0];
486 | if (!repository) return undefined;
487 |
488 | return repository.state.HEAD?.name;
489 | } catch (error) {
490 | log(`Error getting git branch: ${error}`);
491 | return undefined;
492 | }
493 | }
494 |
495 | private async sendHeartbeat(force: boolean = false): Promise {
496 | const activeEditor = vscode.window.activeTextEditor;
497 | if (!activeEditor || !this.activeDocumentInfo) return;
498 | const now = Date.now();
499 | const fileChanged = this.lastFile !== activeEditor.document.uri.fsPath;
500 | const timeThresholdPassed =
501 | now - this.lastHeartbeat >= this.heartbeatInterval;
502 | if (!force && !fileChanged && !timeThresholdPassed) {
503 | return;
504 | }
505 | this.lastFile = activeEditor.document.uri.fsPath;
506 | this.lastHeartbeat = now;
507 | this.heartbeatCount++;
508 | const project = await this.getProjectName(activeEditor.document.uri);
509 | if (!project) {
510 | log("No project name found for the current file, skipping heartbeat");
511 | return;
512 | }
513 | const apiKey = await getApiKey();
514 | const baseUrl = await getBaseUrl();
515 | if (!apiKey || !baseUrl) {
516 | return;
517 | }
518 | const branch = await this.getGitBranch();
519 | const heartbeat: Heartbeat = {
520 | timestamp: new Date().toISOString(),
521 | project,
522 | language: this.activeDocumentInfo.language,
523 | file: this.activeDocumentInfo.file,
524 | branch,
525 | editor: vscode.env.appName,
526 | os:
527 | process.platform === "win32"
528 | ? "Windows"
529 | : process.platform === "darwin"
530 | ? "macOS"
531 | : "Linux",
532 | };
533 | if (!this.isOnline) {
534 | this.offlineHeartbeats.push(heartbeat);
535 | this.saveOfflineHeartbeats();
536 | return;
537 | }
538 | try {
539 | const data = JSON.stringify(heartbeat);
540 | const url = new URL(`${baseUrl}/api/external/heartbeat`);
541 | const requestOptions = {
542 | hostname: url.hostname,
543 | port: url.port || (url.protocol === "https:" ? 443 : 80),
544 | path: url.pathname + url.search,
545 | method: "POST",
546 | headers: {
547 | "Content-Type": "application/json",
548 | "Content-Length": Buffer.byteLength(data),
549 | Authorization: `Bearer ${apiKey}`,
550 | },
551 | };
552 | await new Promise((resolve, reject) => {
553 | const req = (url.protocol === "https:" ? https : http).request(
554 | requestOptions,
555 | (res) => {
556 | if (
557 | res.statusCode &&
558 | res.statusCode >= 200 &&
559 | res.statusCode < 300
560 | ) {
561 | this.successCount++;
562 | this.setOnlineStatus(true);
563 | this.setApiKeyStatus(true);
564 | this.unsyncedLocalSeconds = 0;
565 | log(
566 | `Heartbeat sent successfully for ${heartbeat.file} (${heartbeat.language}) in project ${heartbeat.project}`,
567 | );
568 | resolve();
569 | } else if (res.statusCode === 401) {
570 | this.setApiKeyStatus(false);
571 | reject(
572 | new Error(`Invalid API key (status code: ${res.statusCode})`),
573 | );
574 | } else {
575 | this.failureCount++;
576 | reject(new Error(`Failed with status code: ${res.statusCode}`));
577 | }
578 | },
579 | );
580 | req.on("error", (err) => {
581 | this.failureCount++;
582 | this.setOnlineStatus(false);
583 | reject(err);
584 | });
585 | req.write(data);
586 | req.end();
587 | });
588 | } catch (error) {
589 | if (error instanceof Error && error.message.includes("API key")) {
590 | this.setApiKeyStatus(false);
591 | } else {
592 | this.setOnlineStatus(false);
593 | }
594 | this.offlineHeartbeats.push(heartbeat);
595 | this.saveOfflineHeartbeats();
596 | log(
597 | `Failed to send heartbeat: ${
598 | error instanceof Error ? error.message : String(error)
599 | }`,
600 | );
601 | }
602 | }
603 |
604 | private loadOfflineHeartbeats(): void {
605 | try {
606 | if (fs.existsSync(this.offlineQueuePath)) {
607 | const data = fs.readFileSync(this.offlineQueuePath, "utf8");
608 | this.offlineHeartbeats = JSON.parse(data);
609 | }
610 | } catch (error) {
611 | log(
612 | `Error loading offline heartbeats: ${
613 | error instanceof Error ? error.message : String(error)
614 | }`,
615 | );
616 | this.offlineHeartbeats = [];
617 | }
618 | }
619 |
620 | private saveOfflineHeartbeats(): void {
621 | try {
622 | fs.writeFileSync(
623 | this.offlineQueuePath,
624 | JSON.stringify(this.offlineHeartbeats),
625 | "utf8",
626 | );
627 | } catch (error) {
628 | log(
629 | `Error saving offline heartbeats: ${
630 | error instanceof Error ? error.message : String(error)
631 | }`,
632 | );
633 | }
634 | }
635 |
636 | private makeRequest(options: http.RequestOptions): Promise {
637 | return new Promise((resolve, reject) => {
638 | const req = (options.protocol === "https:" ? https : http).request(
639 | options,
640 | (res) => {
641 | let data = "";
642 | res.on("data", (chunk) => {
643 | data += chunk;
644 | });
645 | res.on("end", () => {
646 | if (
647 | res.statusCode &&
648 | res.statusCode >= 200 &&
649 | res.statusCode < 300
650 | ) {
651 | try {
652 | resolve(JSON.parse(data));
653 | } catch (error) {
654 | reject(
655 | new Error(
656 | `Invalid JSON response: ${
657 | error instanceof Error ? error.message : String(error)
658 | }`,
659 | ),
660 | );
661 | }
662 | } else if (res.statusCode === 401) {
663 | this.setApiKeyStatus(false);
664 | reject(
665 | new Error(`Invalid API key (status code: ${res.statusCode})`),
666 | );
667 | } else {
668 | reject(
669 | new Error(
670 | `Request failed with status code ${res.statusCode}: ${data}`,
671 | ),
672 | );
673 | }
674 | });
675 | },
676 | );
677 | req.on("error", (error) => {
678 | this.setOnlineStatus(false);
679 | reject(error);
680 | });
681 | req.end();
682 | });
683 | }
684 |
685 | private async getProjectName(
686 | fileUri: vscode.Uri,
687 | ): Promise {
688 | try {
689 | const gitExtension = vscode.extensions.getExtension<{
690 | getAPI(version: number): any;
691 | }>("vscode.git");
692 | if (!gitExtension) {
693 | log("Git extension not found.");
694 | return this.getProjectNameFromWorkspaceFolder(fileUri);
695 | }
696 |
697 | if (!gitExtension.isActive) {
698 | await gitExtension.activate();
699 | log("Git extension activated.");
700 | }
701 |
702 | const git = gitExtension.exports.getAPI(1);
703 | if (!git) {
704 | log("Git API not available.");
705 | return this.getProjectNameFromWorkspaceFolder(fileUri);
706 | }
707 |
708 | const repository = git.getRepository(fileUri);
709 | if (!repository) {
710 | log(`No Git repository found containing the file: ${fileUri.fsPath}`);
711 | return this.getProjectNameFromWorkspaceFolder(fileUri);
712 | }
713 |
714 | log(
715 | `Found repository for file ${fileUri.fsPath}: ${repository.rootUri.fsPath}`,
716 | );
717 | const remotes = repository.state.remotes;
718 |
719 | const getProjectNameFromUrl = (url: string): string | undefined => {
720 | try {
721 | const lastSeparator = Math.max(
722 | url.lastIndexOf("/"),
723 | url.lastIndexOf(":"),
724 | );
725 | if (lastSeparator === -1) return undefined;
726 | let name = url.substring(lastSeparator + 1);
727 | if (name.endsWith(".git")) name = name.slice(0, -4);
728 | return name || undefined;
729 | } catch (e) {
730 | log(`Error parsing git remote URL ${url}: ${e}`);
731 | return undefined;
732 | }
733 | };
734 |
735 | const getProjectNameFromLocalPath = (repo: any): string | undefined => {
736 | const repoPath = repo?.rootUri?.fsPath;
737 | if (repoPath) {
738 | let name = path.basename(repoPath);
739 | if (name.endsWith(".git")) name = name.slice(0, -4);
740 | return name || undefined;
741 | }
742 | return undefined;
743 | };
744 |
745 | if (remotes.length > 0) {
746 | const originRemote = remotes.find(
747 | (remote: any) => remote.name === "origin",
748 | );
749 | const remoteToUse = originRemote || remotes[0];
750 | const remoteUrl = remoteToUse.fetchUrl || remoteToUse.pushUrl;
751 | if (remoteUrl) {
752 | const projectName = getProjectNameFromUrl(remoteUrl);
753 | if (projectName) return projectName;
754 | }
755 | }
756 |
757 | const localProjectName = getProjectNameFromLocalPath(repository);
758 | if (localProjectName) return localProjectName;
759 |
760 | return undefined;
761 | } catch (error) {
762 | log(
763 | `Error getting project name from Git: ${
764 | error instanceof Error ? error.message : String(error)
765 | }`,
766 | );
767 | return this.getProjectNameFromWorkspaceFolder(fileUri);
768 | }
769 | }
770 |
771 | private getProjectNameFromWorkspaceFolder(
772 | fileUri: vscode.Uri,
773 | ): string | undefined {
774 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri);
775 | log(
776 | `Falling back to workspace folder name for ${fileUri.fsPath}. Found: ${workspaceFolder?.name}`,
777 | );
778 | return workspaceFolder?.name;
779 | }
780 |
781 | private setOnlineStatus(isOnline: boolean): void {
782 | if (this.isOnline !== isOnline) {
783 | this.isOnline = isOnline;
784 | if (this.statusBar) {
785 | this.statusBar.setOnlineStatus(isOnline);
786 | }
787 | log(`Online status changed to: ${isOnline ? "online" : "offline"}`);
788 | this.syncOfflineHeartbeats();
789 | }
790 | }
791 |
792 | private setApiKeyStatus(isValid: boolean): void {
793 | if (this.hasValidApiKey !== isValid) {
794 | this.hasValidApiKey = isValid;
795 | if (this.statusBar) {
796 | this.statusBar.setApiKeyStatus(isValid);
797 | }
798 | log(`API key status changed to: ${isValid ? "valid" : "invalid"}`);
799 | }
800 | }
801 |
802 | public dispose(): void {
803 | this.saveOfflineHeartbeats();
804 | if (this.activityAccumulatorIntervalId) {
805 | clearInterval(this.activityAccumulatorIntervalId);
806 | this.activityAccumulatorIntervalId = null;
807 | }
808 | }
809 | }
810 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------