screenpipe-mcp 0.18.12 → 0.18.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -656,6 +656,82 @@ const TOOLS = [
656
656
  required: ["action"],
657
657
  },
658
658
  },
659
+ // ----- Pipes: scheduled AI automations ----------------------------------
660
+ // The pipe lifecycle. The rich how-to (prompt format, schedule syntax,
661
+ // presets, how a pipe should query screenpipe) lives in the
662
+ // screenpipe://guide/pipes resource — keep these descriptions short and
663
+ // point there, per progressive disclosure.
664
+ {
665
+ name: "list-pipes",
666
+ description: "List the user's pipes (scheduled AI automations) with their enabled state + schedule. " +
667
+ "USE WHEN: the user asks what automations/pipes exist, or before you create or edit one.",
668
+ annotations: { title: "List Pipes", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
669
+ inputSchema: { type: "object", properties: {} },
670
+ },
671
+ {
672
+ name: "create-pipe",
673
+ description: "Create a pipe — a scheduled AI automation that runs a markdown prompt on a schedule (e.g. 'every day at 9am'). " +
674
+ "Writes ~/.screenpipe/pipes/<name>/pipe.md, installs it, enables it, and (by default) runs it once to test. " +
675
+ "USE WHEN: the user wants to automate a recurring task (daily summary, reminder, report, monitor, sync). " +
676
+ "IMPORTANT: read the screenpipe://guide/pipes resource FIRST — it documents the prompt format, schedule syntax, presets, and how the pipe prompt should query screenpipe. After creating, check pipe-logs to confirm the test run worked.",
677
+ annotations: { title: "Create Pipe", readOnlyHint: false, openWorldHint: false, idempotentHint: false },
678
+ inputSchema: {
679
+ type: "object",
680
+ properties: {
681
+ name: {
682
+ type: "string",
683
+ description: "kebab-case id, e.g. 'daily-time-report'. Becomes the folder name + pipe id.",
684
+ },
685
+ prompt: {
686
+ type: "string",
687
+ description: "The markdown instructions the AI agent runs every scheduled execution. Be specific: what to query (which screenpipe endpoints + time range), how to process it, and what to output (write a note, send a notification, push to a connection). screenpipe prepends a context header (time range, timezone, OS, API base + key) before each run, so no template variables are needed. See screenpipe://guide/pipes.",
688
+ },
689
+ schedule: {
690
+ type: "string",
691
+ description: "When to run: 'every 30m', 'every 1h', 'every day at 9am', 'every monday at 9am', or a cron expression like '0 9 * * *'.",
692
+ },
693
+ enabled: { type: "boolean", description: "Enable on creation (default true).", default: true },
694
+ preset: {
695
+ type: "array",
696
+ items: { type: "string" },
697
+ description: "Optional AI model preset name(s), e.g. ['Primary','Fallback']. Omit to use the default preset.",
698
+ },
699
+ history: {
700
+ type: "boolean",
701
+ description: "Feed the previous run's output back in as context on the next run (default false).",
702
+ default: false,
703
+ },
704
+ run_now: {
705
+ type: "boolean",
706
+ description: "Run once immediately after creating, to test it (default true).",
707
+ default: true,
708
+ },
709
+ },
710
+ required: ["name", "prompt", "schedule"],
711
+ },
712
+ },
713
+ {
714
+ name: "run-pipe",
715
+ description: "Run a pipe once immediately (a test run), independent of its schedule. " +
716
+ "USE WHEN: you just created/edited a pipe and want to verify it, or the user says 'run X now'. Then read pipe-logs to see what it did.",
717
+ annotations: { title: "Run Pipe", readOnlyHint: false, openWorldHint: false, idempotentHint: false },
718
+ inputSchema: {
719
+ type: "object",
720
+ properties: { name: { type: "string", description: "The pipe id/name." } },
721
+ required: ["name"],
722
+ },
723
+ },
724
+ {
725
+ name: "pipe-logs",
726
+ description: "Get a pipe's recent execution logs / output. " +
727
+ "USE WHEN: debugging why a pipe misbehaved, or reading the result of a test run.",
728
+ annotations: { title: "Pipe Logs", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
729
+ inputSchema: {
730
+ type: "object",
731
+ properties: { name: { type: "string", description: "The pipe id/name." } },
732
+ required: ["name"],
733
+ },
734
+ },
659
735
  ];
660
736
  // ---------------------------------------------------------------------------
661
737
  // Enterprise team tools — registered only when a team API token is present.
@@ -760,6 +836,12 @@ const RESOURCES = [
760
836
  description: "How to use screenpipe tools effectively — search strategy, progressive disclosure, and common patterns",
761
837
  mimeType: "text/markdown",
762
838
  },
839
+ {
840
+ uri: "screenpipe://guide/pipes",
841
+ name: "Creating Pipes",
842
+ description: "How to create pipes (scheduled AI automations): the pipe.md prompt format, schedule syntax, presets, how the prompt should query screenpipe, and the create→run→logs lifecycle. Read before using create-pipe.",
843
+ mimeType: "text/markdown",
844
+ },
763
845
  ];
764
846
  server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
765
847
  return { resources: RESOURCES };
@@ -829,6 +911,7 @@ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) =
829
911
  - "Find when I was on Twitter" → search-content with app_name='Arc' (or the browser name), q='twitter'
830
912
  - "Remember that I prefer X" → update-memory with content describing the preference
831
913
  - "What do you remember about X?" → search-content with content_type='memory', q='X'
914
+ - "Automate X every day / on a schedule" → read the screenpipe://guide/pipes resource, then create-pipe (a scheduled AI automation)
832
915
 
833
916
  ## Deep Links
834
917
 
@@ -836,6 +919,64 @@ When referencing specific moments in results, create clickable links:
836
919
  - Frame: [10:30 AM — Chrome](screenpipe://frame/{frame_id}) — use frame_id from search results
837
920
  - Timeline: [meeting at 3pm](screenpipe://timeline?timestamp=2024-01-15T15:00:00Z) — use exact timestamp from results
838
921
  Never fabricate IDs or timestamps — only use values from actual results.
922
+ `,
923
+ },
924
+ ],
925
+ };
926
+ }
927
+ if (uri === "screenpipe://guide/pipes") {
928
+ return {
929
+ contents: [
930
+ {
931
+ uri,
932
+ mimeType: "text/markdown",
933
+ text: `# Creating Pipes — scheduled AI automations
934
+
935
+ A **pipe** is a markdown prompt that an AI agent runs on a schedule. Each pipe is a folder \`~/.screenpipe/pipes/<name>/pipe.md\` with YAML frontmatter + a prompt body. Use the **create-pipe** tool — it writes the file, installs, enables, and (by default) runs it once to test. Manage with **list-pipes**, **run-pipe**, and **pipe-logs**.
936
+
937
+ ## pipe.md anatomy
938
+
939
+ \`\`\`markdown
940
+ ---
941
+ schedule: every day at 9am
942
+ enabled: true
943
+ preset: ["Primary", "Fallback"] # optional model preset(s); omit for default
944
+ history: false # optional; feed prior run's output back in
945
+ ---
946
+
947
+ Your instructions here. This prompt is what the AI agent executes on schedule.
948
+ \`\`\`
949
+
950
+ **schedule** (required): \`every 30m\` · \`every 1h\` · \`every day at 9am\` · \`every monday at 9am\` · or cron \`0 9 * * *\`.
951
+
952
+ screenpipe **prepends a context header** before every run (current time range, timezone, OS, API base URL + auth). So the prompt does NOT need template variables or to hardcode the key — it just says what to do.
953
+
954
+ ## Writing a good pipe prompt
955
+
956
+ Make the prompt do three things, concretely:
957
+ 1. **Query** the relevant window of activity. Prefer the same endpoints these MCP tools wrap:
958
+ - \`GET /activity-summary?start_time=...&end_time=now\` — apps/windows/durations. **Let this endpoint own all time math; never sum minutes in the prompt (the model drifts).**
959
+ - \`GET /search?q=...&content_type=all&start_time=...\` — specific screen text, audio transcripts, memories.
960
+ - \`GET /memories?...\`, \`GET /meetings?...\` for curated facts / meetings.
961
+ Always pass \`start_time\` — never scan the whole history.
962
+ 2. **Process / summarize** the results.
963
+ 3. **Output** somewhere: write a note/file, send a desktop notification (\`POST\` the Tauri sidecar on port 11435 \`/notify\`), or push to a configured connection (Telegram/Slack/Discord/Email — see the CLI \`connection\` commands).
964
+
965
+ Keep each pipe to **one bounded job**. A focused "summarize my day and write it to a note" beats a vague "monitor everything".
966
+
967
+ ## Lifecycle
968
+
969
+ - **create-pipe** → writes pipe.md + installs + enables (+ optional \`run_now\` test).
970
+ - **run-pipe** → run once now to test, independent of schedule.
971
+ - **pipe-logs** → read the output / debug.
972
+ - To change config later: \`POST /pipes/<name>/config\` with e.g. \`{ "schedule": "every 1h", "enabled": true }\`.
973
+
974
+ ## Example
975
+
976
+ A daily time-audit pipe:
977
+ - name: \`daily-time-report\`
978
+ - schedule: \`every day at 6pm\`
979
+ - prompt: "Call /activity-summary for today (start_time='today', end_time=now). Group time by app and project. Write a concise markdown report of where my time went and the top 3 time sinks, then send it as a desktop notification with a link to the timeline."
839
980
  `,
840
981
  },
841
982
  ],
@@ -1000,6 +1141,124 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
1000
1141
  }
1001
1142
  try {
1002
1143
  switch (name) {
1144
+ case "list-pipes": {
1145
+ const res = await callAPI("/pipes");
1146
+ const data = await res.json();
1147
+ const pipes = Array.isArray(data) ? data : data.data || [];
1148
+ if (!pipes.length) {
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: "text",
1153
+ text: "No pipes yet. Use create-pipe to add a scheduled automation (read the screenpipe://guide/pipes resource first).",
1154
+ },
1155
+ ],
1156
+ };
1157
+ }
1158
+ const rows = pipes.map((p) => {
1159
+ const id = p.id || p.name || p.pipe_id || "?";
1160
+ const cfg = p.config || p;
1161
+ const en = (cfg.enabled ?? p.enabled) ? "on " : "off";
1162
+ const sch = cfg.schedule || p.schedule || "?";
1163
+ return `${en} | ${id} | ${sch}`;
1164
+ });
1165
+ return {
1166
+ content: [
1167
+ { type: "text", text: `pipes (enabled | name | schedule):\n${rows.join("\n")}` },
1168
+ ],
1169
+ };
1170
+ }
1171
+ case "create-pipe": {
1172
+ const pipeName = String(args.name || "").trim();
1173
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(pipeName)) {
1174
+ throw new Error("invalid pipe name — use kebab-case letters/numbers/dashes, e.g. 'daily-time-report'");
1175
+ }
1176
+ const prompt = String(args.prompt || "").trim();
1177
+ const schedule = String(args.schedule || "").trim();
1178
+ if (!prompt)
1179
+ throw new Error("prompt is required");
1180
+ if (!schedule)
1181
+ throw new Error("schedule is required");
1182
+ const enabled = args.enabled !== false;
1183
+ const runNow = args.run_now !== false;
1184
+ const fm = ["---", `schedule: ${schedule}`, `enabled: ${enabled}`];
1185
+ if (Array.isArray(args.preset) && args.preset.length) {
1186
+ fm.push(`preset: ${JSON.stringify(args.preset)}`);
1187
+ }
1188
+ if (args.history === true)
1189
+ fm.push("history: true");
1190
+ fm.push("---", "", prompt, "");
1191
+ const md = fm.join("\n");
1192
+ const dir = path.join(os.homedir(), ".screenpipe", "pipes", pipeName);
1193
+ fs.mkdirSync(dir, { recursive: true });
1194
+ fs.writeFileSync(path.join(dir, "pipe.md"), md, "utf8");
1195
+ const steps = [`wrote ${path.join(dir, "pipe.md")}`];
1196
+ const instRes = await callAPI("/pipes/install", {
1197
+ method: "POST",
1198
+ body: JSON.stringify({ source: dir }),
1199
+ });
1200
+ const inst = await instRes.json();
1201
+ if (inst?.error)
1202
+ throw new Error(`install failed: ${inst.error}`);
1203
+ const pipeId = inst?.name || pipeName;
1204
+ steps.push(`installed as "${pipeId}"`);
1205
+ if (enabled) {
1206
+ await callAPI(`/pipes/${encodeURIComponent(pipeId)}/enable`, {
1207
+ method: "POST",
1208
+ body: JSON.stringify({ enabled: true }),
1209
+ });
1210
+ steps.push("enabled");
1211
+ }
1212
+ let runNote = "";
1213
+ if (runNow) {
1214
+ try {
1215
+ await callAPI(`/pipes/${encodeURIComponent(pipeId)}/run`, { method: "POST" });
1216
+ steps.push("started a test run");
1217
+ runNote = `\n\nA test run was started — read it with pipe-logs (name="${pipeId}").`;
1218
+ }
1219
+ catch (e) {
1220
+ runNote = `\n\nCreated, but the test run couldn't start: ${e.message}. Try run-pipe later.`;
1221
+ }
1222
+ }
1223
+ return {
1224
+ content: [
1225
+ {
1226
+ type: "text",
1227
+ text: `Created pipe "${pipeId}" — schedule: ${schedule}, ${enabled ? "enabled" : "disabled"}.\n` +
1228
+ steps.map((s) => `- ${s}`).join("\n") +
1229
+ runNote,
1230
+ },
1231
+ ],
1232
+ };
1233
+ }
1234
+ case "run-pipe": {
1235
+ const pipeName = String(args.name || "").trim();
1236
+ if (!pipeName)
1237
+ throw new Error("name is required");
1238
+ const res = await callAPI(`/pipes/${encodeURIComponent(pipeName)}/run`, {
1239
+ method: "POST",
1240
+ });
1241
+ const data = await res.json().catch(() => ({}));
1242
+ if (data?.error)
1243
+ throw new Error(String(data.error));
1244
+ return {
1245
+ content: [
1246
+ {
1247
+ type: "text",
1248
+ text: `Started a run of "${pipeName}". Read pipe-logs (name="${pipeName}") for the output.`,
1249
+ },
1250
+ ],
1251
+ };
1252
+ }
1253
+ case "pipe-logs": {
1254
+ const pipeName = String(args.name || "").trim();
1255
+ if (!pipeName)
1256
+ throw new Error("name is required");
1257
+ const res = await callAPI(`/pipes/${encodeURIComponent(pipeName)}/logs`);
1258
+ const text = await res.text();
1259
+ const trimmed = text.length > 6000 ? `…${text.slice(-6000)}` : text;
1260
+ return { content: [{ type: "text", text: trimmed || "(no logs yet)" }] };
1261
+ }
1003
1262
  case "search-content": {
1004
1263
  const includeFrames = args.include_frames === true;
1005
1264
  const normalized = normalizeTimeFields(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenpipe-mcp",
3
- "version": "0.18.12",
3
+ "version": "0.18.13",
4
4
  "mcpName": "io.github.screenpipe/screenpipe-mcp",
5
5
  "description": "MCP server for screenpipe - search your screen recordings and audio transcriptions",
6
6
  "main": "dist/index.js",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.screenpipe/screenpipe-mcp",
4
4
  "title": "screenpipe",
5
- "description": "Search your local screen recordings, audio transcriptions, and computer activity captured by screenpipe.",
5
+ "description": "Search your local screen recordings, audio transcripts, and computer activity from screenpipe.",
6
6
  "repository": {
7
7
  "url": "https://github.com/screenpipe/screenpipe",
8
8
  "source": "github",
package/src/index.ts CHANGED
@@ -659,6 +659,88 @@ const TOOLS: Tool[] = [
659
659
  required: ["action"],
660
660
  },
661
661
  },
662
+ // ----- Pipes: scheduled AI automations ----------------------------------
663
+ // The pipe lifecycle. The rich how-to (prompt format, schedule syntax,
664
+ // presets, how a pipe should query screenpipe) lives in the
665
+ // screenpipe://guide/pipes resource — keep these descriptions short and
666
+ // point there, per progressive disclosure.
667
+ {
668
+ name: "list-pipes",
669
+ description:
670
+ "List the user's pipes (scheduled AI automations) with their enabled state + schedule. " +
671
+ "USE WHEN: the user asks what automations/pipes exist, or before you create or edit one.",
672
+ annotations: { title: "List Pipes", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
673
+ inputSchema: { type: "object", properties: {} },
674
+ },
675
+ {
676
+ name: "create-pipe",
677
+ description:
678
+ "Create a pipe — a scheduled AI automation that runs a markdown prompt on a schedule (e.g. 'every day at 9am'). " +
679
+ "Writes ~/.screenpipe/pipes/<name>/pipe.md, installs it, enables it, and (by default) runs it once to test. " +
680
+ "USE WHEN: the user wants to automate a recurring task (daily summary, reminder, report, monitor, sync). " +
681
+ "IMPORTANT: read the screenpipe://guide/pipes resource FIRST — it documents the prompt format, schedule syntax, presets, and how the pipe prompt should query screenpipe. After creating, check pipe-logs to confirm the test run worked.",
682
+ annotations: { title: "Create Pipe", readOnlyHint: false, openWorldHint: false, idempotentHint: false },
683
+ inputSchema: {
684
+ type: "object",
685
+ properties: {
686
+ name: {
687
+ type: "string",
688
+ description: "kebab-case id, e.g. 'daily-time-report'. Becomes the folder name + pipe id.",
689
+ },
690
+ prompt: {
691
+ type: "string",
692
+ description:
693
+ "The markdown instructions the AI agent runs every scheduled execution. Be specific: what to query (which screenpipe endpoints + time range), how to process it, and what to output (write a note, send a notification, push to a connection). screenpipe prepends a context header (time range, timezone, OS, API base + key) before each run, so no template variables are needed. See screenpipe://guide/pipes.",
694
+ },
695
+ schedule: {
696
+ type: "string",
697
+ description:
698
+ "When to run: 'every 30m', 'every 1h', 'every day at 9am', 'every monday at 9am', or a cron expression like '0 9 * * *'.",
699
+ },
700
+ enabled: { type: "boolean", description: "Enable on creation (default true).", default: true },
701
+ preset: {
702
+ type: "array",
703
+ items: { type: "string" },
704
+ description: "Optional AI model preset name(s), e.g. ['Primary','Fallback']. Omit to use the default preset.",
705
+ },
706
+ history: {
707
+ type: "boolean",
708
+ description: "Feed the previous run's output back in as context on the next run (default false).",
709
+ default: false,
710
+ },
711
+ run_now: {
712
+ type: "boolean",
713
+ description: "Run once immediately after creating, to test it (default true).",
714
+ default: true,
715
+ },
716
+ },
717
+ required: ["name", "prompt", "schedule"],
718
+ },
719
+ },
720
+ {
721
+ name: "run-pipe",
722
+ description:
723
+ "Run a pipe once immediately (a test run), independent of its schedule. " +
724
+ "USE WHEN: you just created/edited a pipe and want to verify it, or the user says 'run X now'. Then read pipe-logs to see what it did.",
725
+ annotations: { title: "Run Pipe", readOnlyHint: false, openWorldHint: false, idempotentHint: false },
726
+ inputSchema: {
727
+ type: "object",
728
+ properties: { name: { type: "string", description: "The pipe id/name." } },
729
+ required: ["name"],
730
+ },
731
+ },
732
+ {
733
+ name: "pipe-logs",
734
+ description:
735
+ "Get a pipe's recent execution logs / output. " +
736
+ "USE WHEN: debugging why a pipe misbehaved, or reading the result of a test run.",
737
+ annotations: { title: "Pipe Logs", readOnlyHint: true, openWorldHint: false, idempotentHint: true },
738
+ inputSchema: {
739
+ type: "object",
740
+ properties: { name: { type: "string", description: "The pipe id/name." } },
741
+ required: ["name"],
742
+ },
743
+ },
662
744
  ];
663
745
 
664
746
  // ---------------------------------------------------------------------------
@@ -772,6 +854,13 @@ const RESOURCES = [
772
854
  description: "How to use screenpipe tools effectively — search strategy, progressive disclosure, and common patterns",
773
855
  mimeType: "text/markdown",
774
856
  },
857
+ {
858
+ uri: "screenpipe://guide/pipes",
859
+ name: "Creating Pipes",
860
+ description:
861
+ "How to create pipes (scheduled AI automations): the pipe.md prompt format, schedule syntax, presets, how the prompt should query screenpipe, and the create→run→logs lifecycle. Read before using create-pipe.",
862
+ mimeType: "text/markdown",
863
+ },
775
864
  ];
776
865
 
777
866
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
@@ -849,6 +938,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
849
938
  - "Find when I was on Twitter" → search-content with app_name='Arc' (or the browser name), q='twitter'
850
939
  - "Remember that I prefer X" → update-memory with content describing the preference
851
940
  - "What do you remember about X?" → search-content with content_type='memory', q='X'
941
+ - "Automate X every day / on a schedule" → read the screenpipe://guide/pipes resource, then create-pipe (a scheduled AI automation)
852
942
 
853
943
  ## Deep Links
854
944
 
@@ -862,6 +952,65 @@ Never fabricate IDs or timestamps — only use values from actual results.
862
952
  };
863
953
  }
864
954
 
955
+ if (uri === "screenpipe://guide/pipes") {
956
+ return {
957
+ contents: [
958
+ {
959
+ uri,
960
+ mimeType: "text/markdown",
961
+ text: `# Creating Pipes — scheduled AI automations
962
+
963
+ A **pipe** is a markdown prompt that an AI agent runs on a schedule. Each pipe is a folder \`~/.screenpipe/pipes/<name>/pipe.md\` with YAML frontmatter + a prompt body. Use the **create-pipe** tool — it writes the file, installs, enables, and (by default) runs it once to test. Manage with **list-pipes**, **run-pipe**, and **pipe-logs**.
964
+
965
+ ## pipe.md anatomy
966
+
967
+ \`\`\`markdown
968
+ ---
969
+ schedule: every day at 9am
970
+ enabled: true
971
+ preset: ["Primary", "Fallback"] # optional model preset(s); omit for default
972
+ history: false # optional; feed prior run's output back in
973
+ ---
974
+
975
+ Your instructions here. This prompt is what the AI agent executes on schedule.
976
+ \`\`\`
977
+
978
+ **schedule** (required): \`every 30m\` · \`every 1h\` · \`every day at 9am\` · \`every monday at 9am\` · or cron \`0 9 * * *\`.
979
+
980
+ screenpipe **prepends a context header** before every run (current time range, timezone, OS, API base URL + auth). So the prompt does NOT need template variables or to hardcode the key — it just says what to do.
981
+
982
+ ## Writing a good pipe prompt
983
+
984
+ Make the prompt do three things, concretely:
985
+ 1. **Query** the relevant window of activity. Prefer the same endpoints these MCP tools wrap:
986
+ - \`GET /activity-summary?start_time=...&end_time=now\` — apps/windows/durations. **Let this endpoint own all time math; never sum minutes in the prompt (the model drifts).**
987
+ - \`GET /search?q=...&content_type=all&start_time=...\` — specific screen text, audio transcripts, memories.
988
+ - \`GET /memories?...\`, \`GET /meetings?...\` for curated facts / meetings.
989
+ Always pass \`start_time\` — never scan the whole history.
990
+ 2. **Process / summarize** the results.
991
+ 3. **Output** somewhere: write a note/file, send a desktop notification (\`POST\` the Tauri sidecar on port 11435 \`/notify\`), or push to a configured connection (Telegram/Slack/Discord/Email — see the CLI \`connection\` commands).
992
+
993
+ Keep each pipe to **one bounded job**. A focused "summarize my day and write it to a note" beats a vague "monitor everything".
994
+
995
+ ## Lifecycle
996
+
997
+ - **create-pipe** → writes pipe.md + installs + enables (+ optional \`run_now\` test).
998
+ - **run-pipe** → run once now to test, independent of schedule.
999
+ - **pipe-logs** → read the output / debug.
1000
+ - To change config later: \`POST /pipes/<name>/config\` with e.g. \`{ "schedule": "every 1h", "enabled": true }\`.
1001
+
1002
+ ## Example
1003
+
1004
+ A daily time-audit pipe:
1005
+ - name: \`daily-time-report\`
1006
+ - schedule: \`every day at 6pm\`
1007
+ - prompt: "Call /activity-summary for today (start_time='today', end_time=now). Group time by app and project. Write a concise markdown report of where my time went and the top 3 time sinks, then send it as a desktop notification with a link to the timeline."
1008
+ `,
1009
+ },
1010
+ ],
1011
+ };
1012
+ }
1013
+
865
1014
  throw new Error(`Unknown resource: ${uri}`);
866
1015
  });
867
1016
 
@@ -1028,6 +1177,129 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1028
1177
 
1029
1178
  try {
1030
1179
  switch (name) {
1180
+ case "list-pipes": {
1181
+ const res = await callAPI("/pipes");
1182
+ const data = await res.json();
1183
+ const pipes: any[] = Array.isArray(data) ? data : data.data || [];
1184
+ if (!pipes.length) {
1185
+ return {
1186
+ content: [
1187
+ {
1188
+ type: "text",
1189
+ text: "No pipes yet. Use create-pipe to add a scheduled automation (read the screenpipe://guide/pipes resource first).",
1190
+ },
1191
+ ],
1192
+ };
1193
+ }
1194
+ const rows = pipes.map((p: any) => {
1195
+ const id = p.id || p.name || p.pipe_id || "?";
1196
+ const cfg = p.config || p;
1197
+ const en = (cfg.enabled ?? p.enabled) ? "on " : "off";
1198
+ const sch = cfg.schedule || p.schedule || "?";
1199
+ return `${en} | ${id} | ${sch}`;
1200
+ });
1201
+ return {
1202
+ content: [
1203
+ { type: "text", text: `pipes (enabled | name | schedule):\n${rows.join("\n")}` },
1204
+ ],
1205
+ };
1206
+ }
1207
+
1208
+ case "create-pipe": {
1209
+ const pipeName = String(args.name || "").trim();
1210
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(pipeName)) {
1211
+ throw new Error(
1212
+ "invalid pipe name — use kebab-case letters/numbers/dashes, e.g. 'daily-time-report'"
1213
+ );
1214
+ }
1215
+ const prompt = String(args.prompt || "").trim();
1216
+ const schedule = String(args.schedule || "").trim();
1217
+ if (!prompt) throw new Error("prompt is required");
1218
+ if (!schedule) throw new Error("schedule is required");
1219
+ const enabled = args.enabled !== false;
1220
+ const runNow = args.run_now !== false;
1221
+
1222
+ const fm: string[] = ["---", `schedule: ${schedule}`, `enabled: ${enabled}`];
1223
+ if (Array.isArray(args.preset) && args.preset.length) {
1224
+ fm.push(`preset: ${JSON.stringify(args.preset)}`);
1225
+ }
1226
+ if (args.history === true) fm.push("history: true");
1227
+ fm.push("---", "", prompt, "");
1228
+ const md = fm.join("\n");
1229
+
1230
+ const dir = path.join(os.homedir(), ".screenpipe", "pipes", pipeName);
1231
+ fs.mkdirSync(dir, { recursive: true });
1232
+ fs.writeFileSync(path.join(dir, "pipe.md"), md, "utf8");
1233
+ const steps: string[] = [`wrote ${path.join(dir, "pipe.md")}`];
1234
+
1235
+ const instRes = await callAPI("/pipes/install", {
1236
+ method: "POST",
1237
+ body: JSON.stringify({ source: dir }),
1238
+ });
1239
+ const inst = await instRes.json();
1240
+ if (inst?.error) throw new Error(`install failed: ${inst.error}`);
1241
+ const pipeId = inst?.name || pipeName;
1242
+ steps.push(`installed as "${pipeId}"`);
1243
+
1244
+ if (enabled) {
1245
+ await callAPI(`/pipes/${encodeURIComponent(pipeId)}/enable`, {
1246
+ method: "POST",
1247
+ body: JSON.stringify({ enabled: true }),
1248
+ });
1249
+ steps.push("enabled");
1250
+ }
1251
+
1252
+ let runNote = "";
1253
+ if (runNow) {
1254
+ try {
1255
+ await callAPI(`/pipes/${encodeURIComponent(pipeId)}/run`, { method: "POST" });
1256
+ steps.push("started a test run");
1257
+ runNote = `\n\nA test run was started — read it with pipe-logs (name="${pipeId}").`;
1258
+ } catch (e) {
1259
+ runNote = `\n\nCreated, but the test run couldn't start: ${(e as Error).message}. Try run-pipe later.`;
1260
+ }
1261
+ }
1262
+
1263
+ return {
1264
+ content: [
1265
+ {
1266
+ type: "text",
1267
+ text:
1268
+ `Created pipe "${pipeId}" — schedule: ${schedule}, ${enabled ? "enabled" : "disabled"}.\n` +
1269
+ steps.map((s) => `- ${s}`).join("\n") +
1270
+ runNote,
1271
+ },
1272
+ ],
1273
+ };
1274
+ }
1275
+
1276
+ case "run-pipe": {
1277
+ const pipeName = String(args.name || "").trim();
1278
+ if (!pipeName) throw new Error("name is required");
1279
+ const res = await callAPI(`/pipes/${encodeURIComponent(pipeName)}/run`, {
1280
+ method: "POST",
1281
+ });
1282
+ const data = await res.json().catch(() => ({}));
1283
+ if (data?.error) throw new Error(String(data.error));
1284
+ return {
1285
+ content: [
1286
+ {
1287
+ type: "text",
1288
+ text: `Started a run of "${pipeName}". Read pipe-logs (name="${pipeName}") for the output.`,
1289
+ },
1290
+ ],
1291
+ };
1292
+ }
1293
+
1294
+ case "pipe-logs": {
1295
+ const pipeName = String(args.name || "").trim();
1296
+ if (!pipeName) throw new Error("name is required");
1297
+ const res = await callAPI(`/pipes/${encodeURIComponent(pipeName)}/logs`);
1298
+ const text = await res.text();
1299
+ const trimmed = text.length > 6000 ? `…${text.slice(-6000)}` : text;
1300
+ return { content: [{ type: "text", text: trimmed || "(no logs yet)" }] };
1301
+ }
1302
+
1031
1303
  case "search-content": {
1032
1304
  const includeFrames = args.include_frames === true;
1033
1305
  const normalized = normalizeTimeFields(args);